Compare commits

..
131 Commits
Author SHA1 Message Date
Levi a7d855f65a release: v1.3.12 2023-09-20 16:33:47 +08:00
Levi f0f16b3c80 ci: disable translate 2023-09-20 16:32:02 +08:00
Levi 3459b474db feat: file open on mobile (#223) 2023-09-20 16:29:08 +08:00
Tracey Lesley d28fcef0a7 feat: default http User-Agent header (#26) 2023-09-13 10:03:26 +08:00
Elliot d694c08635 feat: add zh-TW.json translate (#209) 2023-09-08 09:19:55 +08:00
liwei a7c61a668e chore: update donate doc 2023-09-07 09:32:33 +08:00
Levi 09aea55b49 release: v1.3.11 2023-09-04 13:57:47 +08:00
Levi c0cd2d7255 feat: task file manage (#196)
1. support view task file list
2. support share file on android and iOS
2023-09-04 13:51:30 +08:00
Levi b00f4a1ed0 feat: ios support (#194) 2023-09-04 11:28:07 +08:00
Levi 06766eea56 feat: auto get create url from clipboard (#193) 2023-09-02 11:32:28 +08:00
Levi 0f0c8f27bd feat: adjust size of resource dialog (#192) 2023-09-02 11:26:26 +08:00
liwei 45cf62f88d release: v1.3.10 2023-08-26 09:17:22 +08:00
Levi 6e9d3499d9 fix: drop file crash on Windows (#189) 2023-08-26 08:58:10 +08:00
Levi b6d60f6bdf fix: http download timeout (#188) 2023-08-25 18:42:10 +08:00
Levi e1684e755a fix: go install fail (#184) 2023-08-16 17:05:20 +08:00
Tracey Lesley e63cfca82a fix:current writes map (#182) 2023-08-12 13:45:01 +08:00
Levi 85ef8d3be4 release: v1.3.9 2023-08-01 18:57:15 +08:00
Levi bf2ccdb5a7 fix: torrent file path resolve (#173) 2023-08-01 18:56:39 +08:00
Levi a75bfd6118 release: v1.3.8 2023-07-28 21:50:26 +08:00
Levi a5d8713ff7 feat: torrent file support on web (#171) 2023-07-28 21:48:20 +08:00
Levi 81518d794f feat: web support http basic authorization (#170) 2023-07-27 21:02:33 +08:00
Levi 7c2b81caf7 release: v1.3.7 2023-07-23 10:00:47 +08:00
Levi 2221332243 fix: bt file real time speed (#163) 2023-07-23 09:58:42 +08:00
Levi dc407434d0 fix: file_picker crash on windows (#164) 2023-07-23 00:43:22 +08:00
Levi 1c2e86a991 fix: flutter docker image 2023-07-19 23:35:02 +08:00
Levi c78dc5f18f fix: flutter image 2023-07-19 23:26:53 +08:00
Levi 8ea5abb5c3 fix: Dockerfile flutter version 2023-07-19 23:19:24 +08:00
Levi d1a4067dfa release: v1.3.6 2023-07-19 23:05:01 +08:00
Levi a1928d202f fix: high dpi display (#158) 2023-07-19 23:01:25 +08:00
Levi 7376e85895 feat: create task with advanced options (#157) 2023-07-19 22:52:51 +08:00
Li Wei eb79dd5ac5 chore: update docker command 2023-07-19 06:11:02 +00:00
Li Wei 5b945b16c1 ci: auto translate 2023-07-18 09:17:07 +00:00
Levi 80e60f4190 feat: improve tracker cdn (#155) 2023-07-15 17:01:52 +08:00
Levi 3087796ef5 feat: support flutter 3.10 (#154) 2023-07-15 14:52:59 +08:00
Liam Segura 99de0aaf9b chore: Update README.md (#144) 2023-07-07 20:23:41 +08:00
liwei 6bea61cf0a release: v1.3.5 2023-07-07 16:23:31 +08:00
liwei 6c3553805e fix: include Japanese 2023-07-07 16:16:43 +08:00
liwei adc7c7766d feat: include Japanese 2023-07-07 16:10:50 +08:00
Ikko Eltociear Ashimine 15d0296307 feat: add Japanese support (#139) 2023-07-07 09:55:23 +08:00
Li Wei 32d2e46118 feat: bt fetcher reuse (#142) 2023-07-06 17:21:34 +08:00
Li Wei abe3cec13d fix: client log is not recorded (#141) 2023-07-06 17:15:48 +08:00
liwei 9249613601 release: v1.3.4 2023-07-05 21:56:25 +08:00
Li Wei 5bafb40599 fix: torrent udp tracker panic (#138) 2023-07-05 21:32:50 +08:00
Li Wei 5968d3c14a feat: panic log (#137) 2023-07-05 21:12:55 +08:00
Li Wei fcb54d3acc fix: api token not saved (#135) 2023-07-04 12:18:46 +08:00
Li Wei e2c4567ed0 fix: macos directory open (#127) 2023-07-03 19:02:08 +08:00
Li Wei c3b28c7034 fix: dio timeout don't throw exception (#133) 2023-07-03 18:52:17 +08:00
Li Wei 32061200a5 chore: add download total badge 2023-06-30 04:12:02 +00:00
Li Wei 19aad9bc9c fix: android build 2023-06-28 15:33:50 +08:00
Li Wei 2d0cc1c877 release: v1.3.3 2023-06-28 15:21:06 +08:00
Li Wei dc94a1004c build: fix gomobile 2023-06-28 06:38:34 +00:00
Li Wei 1385f62fb0 ci: fix gomobile android build 2023-06-28 03:09:05 +00:00
Li Wei 34fc886662 fix: http filename resolve (#124) 2023-06-28 10:22:45 +08:00
Li Wei c651263747 chore: update clone git url 2023-06-27 03:41:49 +00:00
Li Wei 96a4526bc8 feat: improve web build (#123) 2023-06-27 10:04:15 +08:00
Shane 97cf8ff0c5 fix: remove cli test args (#122) 2023-06-16 09:09:14 +08:00
liwei 70c16195ef release: v1.3.2 2023-06-09 22:17:27 +08:00
Li Wei eb9d966855 feat: support start and pause all tasks (#121) 2023-06-09 22:15:59 +08:00
liwei 0e3ca912a7 fix: add mac tray template icon 2023-06-08 14:10:55 +08:00
Li Wei b3e372326f feat: add desktop system tray support (#120) 2023-06-07 21:50:40 +08:00
Li Wei 0100f0131d feat: android deeplink and file selector (#118) 2023-06-06 20:39:07 +08:00
Levi e4c3b449a4 chore: update readme 2023-05-23 18:23:51 +08:00
liwei aaa8cd6406 fix: flutter downgrade to v3.7.12 2023-05-11 18:06:25 +08:00
Li Wei 328422b3b8 feat: flutter upgrade to v3.10.0 (#113) 2023-05-11 11:42:55 +08:00
Li Wei 17cce89ef1 release: v1.3.1 2023-05-11 09:19:56 +08:00
Li Wei d9c5ca339f feat: support max running tasks config (#112) 2023-05-10 18:00:58 +08:00
Li Wei 1e08ecd8db fix: file explorer and support select file (#111) 2023-05-04 14:32:37 +08:00
Li Wei 34bcbd9571 fix: gopeed cli negative WaitGroup counter error (#110) 2023-05-04 10:05:32 +08:00
liwei 09bdb5584a ci: fix mac build 2023-04-01 21:29:57 +08:00
liwei 36f5c9569e ci: debug 2023-04-01 21:21:09 +08:00
liwei 3d9edc5133 ci: debug 2023-04-01 21:10:41 +08:00
liwei ed08c05532 ci: fix 2023-04-01 20:58:33 +08:00
liwei 9bd8befcb8 fix: mac universal binary build 2023-04-01 20:49:51 +08:00
liwei 7ba87e48fc fix: mac build 2023-04-01 20:31:55 +08:00
Li Wei af5f739630 ci: macos universal binaries build (#103) 2023-04-01 20:13:08 +08:00
liwei 5558cac263 fix: task view ui 2023-04-01 19:43:47 +08:00
liwei 4128e718e9 ci: fix web build 2023-04-01 17:05:51 +08:00
liwei 6d017b18f5 fix: version check 2023-04-01 16:53:43 +08:00
liwei 35eb089178 ci: fix web build 2023-04-01 16:37:42 +08:00
liwei b5f78f0fbe ci: fix docker build 2023-04-01 16:32:57 +08:00
liwei 628d73f90d ci: fix android build 2023-04-01 16:15:34 +08:00
liwei 7d2d84d844 fix: home page menu icon 2023-04-01 16:09:36 +08:00
liwei 1cbe58d760 ci: fix macos arm build 2023-04-01 15:56:07 +08:00
liwei c77a4094a3 fix: logger file output 2023-04-01 15:52:12 +08:00
Li Wei 33f6325bf7 feat: new version check (#102) 2023-04-01 15:33:58 +08:00
Li Wei 701ea59035 feat: smooth speed calculation (#99) 2023-03-09 10:35:10 +08:00
pharaoh2012 e77441f0de docs: correct the official website link (#98) 2023-03-07 11:01:18 +08:00
Li Wei dd46e7a3eb fix: crash caused by non-resumable http download resources (#97) 2023-03-06 21:09:31 +08:00
liwei f42838810a refactor: common message dialog method 2023-03-06 21:02:24 +08:00
liwei 7fdba12376 fix: tracker cdn retry issue 2023-03-06 17:07:00 +08:00
liwei 20130ae6d0 fix: error code 2023-03-05 15:37:04 +08:00
liwei 4598e9b351 ci: fix release action permissions 2023-03-05 12:10:32 +08:00
Li Wei 2e639e0fef feat: api result add code (#96) 2023-03-05 11:54:39 +08:00
liwei dbbbc204fb doc: add official website link 2023-03-04 13:37:20 +08:00
liwei caab121cc2 ci: fix flutter linux build tool 2023-03-02 14:05:22 +08:00
Li Wei c637b36094 ci: flutter build check (#95) 2023-03-02 13:57:00 +08:00
Neginegiandwuke2022 38ba901956 fix: fluent system theme mode (#94)
* Fix: fluent theme mode fix

---------

Co-authored-by: wuke2022 <[email protected]>
2023-02-28 01:53:37 +08:00
Neginegiandwuke2022 8bae1a5850 feat: complete tree view (#93)
* feat: complete tree view

---------

Co-authored-by: wuke2022 <[email protected]>
2023-02-23 21:34:39 +08:00
Neginegiandwuke2022 56375c0edb fix: treeview,style,file picker (#91)
* fix: tree view,style,file picker

Co-authored-by: wuke2022 <[email protected]>
2023-02-22 20:06:28 +08:00
Neginegiandwuke2022 5b69472ac8 fix: style, performance ,loading bug (#90)
* refactor project in get-cli standard

* use local trackers when github is not connectable
upgrade stylish_bottom_bar to 1.0.0
responsive view for home_view
treeview for file_list_vie
fluent ui integrating

* improve responsive view.
treeView add light theme.fix overflow bugs

* responsive view,treeView,file select
downloaded and downloading pages
file extension icons
fix some minor bugs

* use local trackers when github is not connectable
upgrade stylish_bottom_bar to 1.0.0
responsive view for home_view
treeview for file_list_vie
fluent ui integrating

* improve responsive view.
treeView add light theme.fix overflow bugs

* bottom bar index fix

* refactor project in get-cli standard

* fix:locale fix

* fix: task list stack progress style
fix: translation
fix: tree view
fix: confirm button loading
remove: stylish_bottom_bar(dark mode not supported)

* fix: task list stack progress style
fix: translation
fix: tree view
fix: confirm button loading
remove: stylish_bottom_bar(dark mode not supported)

---------

Co-authored-by: wuke2022 <[email protected]>
2023-02-21 20:17:17 +08:00
Neginegiandwuke2022 dbc9c4c121 fix docs and locales (#86)
* refactor project in get-cli standard

* use local trackers when github is not connectable
upgrade stylish_bottom_bar to 1.0.0
responsive view for home_view
treeview for file_list_vie
fluent ui integrating

* improve responsive view.
treeView add light theme.fix overflow bugs

* responsive view,treeView,file select
downloaded and downloading pages
file extension icons
fix some minor bugs

* use local trackers when github is not connectable
upgrade stylish_bottom_bar to 1.0.0
responsive view for home_view
treeview for file_list_vie
fluent ui integrating

* improve responsive view.
treeView add light theme.fix overflow bugs

* bottom bar index fix

* refactor project in get-cli standard

* fix:locale fix

* docs: add detailed bash for build and dev

---------

Co-authored-by: wuke2022 <[email protected]>
2023-02-19 23:47:28 +08:00
Neginegiandwuke2022 c0e24ce869 fix locale in get-cli standard (#81)
* refactor project in get-cli standard

* use local trackers when github is not connectable
upgrade stylish_bottom_bar to 1.0.0
responsive view for home_view
treeview for file_list_vie
fluent ui integrating

* improve responsive view.
treeView add light theme.fix overflow bugs

* responsive view,treeView,file select
downloaded and downloading pages
file extension icons
fix some minor bugs

* use local trackers when github is not connectable
upgrade stylish_bottom_bar to 1.0.0
responsive view for home_view
treeview for file_list_vie
fluent ui integrating

* improve responsive view.
treeView add light theme.fix overflow bugs

* bottom bar index fix

* refactor project in get-cli standard

---------

Co-authored-by: wuke2022 <[email protected]>
2023-02-19 21:22:20 +08:00
Neginegiandwuke2022 74c24ad860 Refactor project in get-cli standard (#80)
* refactor project in get-cli standard

* use local trackers when github is not connectable
upgrade stylish_bottom_bar to 1.0.0
responsive view for home_view
treeview for file_list_vie
fluent ui integrating

* improve responsive view.
treeView add light theme.fix overflow bugs

* responsive view,treeView,file select
downloaded and downloading pages
file extension icons
fix some minor bugs

* use local trackers when github is not connectable
upgrade stylish_bottom_bar to 1.0.0
responsive view for home_view
treeview for file_list_vie
fluent ui integrating

* improve responsive view.
treeView add light theme.fix overflow bugs

* bottom bar index fix

* refactor project in get-cli standard

---------

Co-authored-by: wuke2022 <[email protected]>
2023-02-19 20:06:28 +08:00
wuke2022 9b1ed15fea feat: ui improvement
flutter 3.7 compatibility
responsive view
beautiful progress bar
select file in treeview
downlading and downloaded page
2023-02-16 11:34:42 +08:00
liwei a875a9258e refactor: change repo url 2023-02-11 21:37:05 +08:00
liwei 0d2ac10af8 fix: rename logic 2023-02-10 21:40:16 +08:00
liwei 978b8bc7cb docs: add full name 2023-02-10 16:20:55 +08:00
liwei 50c3713c90 docs: donate link hash 2023-02-10 16:16:00 +08:00
liwei 2639b6bc65 docs: badge sort 2023-02-10 16:09:01 +08:00
liwei 8b29ceb151 docs: add donate badge 2023-02-10 16:07:43 +08:00
liwei e1c14ad9f8 docs: add donate info 2023-02-10 15:42:29 +08:00
liwei e4f86097e8 fix: bt resolve 2023-02-10 15:27:15 +08:00
Li Wei 0515ce6081 feat: auto rename on duplicate (#76) 2023-02-10 15:18:31 +08:00
liwei 048554df4d ci: fix 2023-02-08 17:29:23 +08:00
Li Wei 2d02486ca6 ci: windows setup add chinese (#75) 2023-02-08 17:18:39 +08:00
liwei 3523f5855a fix: api ok nil response 2023-02-08 14:59:59 +08:00
liwei aa9577a2e0 fix: ignore broken task data 2023-02-08 14:53:49 +08:00
liwei efc9dcca0b fix: download dir config 2023-02-07 23:53:50 +08:00
liwei b062336c09 docs: update CONTRIBUTING.md 2023-02-07 23:30:26 +08:00
liwei baf88a5cb7 ci: build only manual trigger 2023-02-07 18:28:14 +08:00
liwei e208014d34 ci: fix windows 2023-02-07 18:24:03 +08:00
liwei 3ecedd60fd ci: fix 2023-02-07 15:33:02 +08:00
liwei 6fd68e4752 ci: fix input 2023-02-07 15:13:51 +08:00
Li Wei c2910b0e8c ci: get last release include draft (#74) 2023-02-07 15:10:31 +08:00
Li Wei 8ed51787ad feat: add cdn for tracker subscribe urls (#73) 2023-02-07 14:18:59 +08:00
liwei 2e5fd3017c fix: bt fetcher restore 2023-02-06 22:21:25 +08:00
Li Wei 027d29e834 fix: windows version (#72) 2023-02-06 21:06:56 +08:00
Li Wei 1a3c8c026b feat: reuse fetcher (#71) 2023-02-06 20:58:13 +08:00
Persian Prince 17cb59c606 feat: add farsi languages (#70) 2023-02-03 21:35:23 +08:00
liwei e3828c2982 ci: fix 2022-12-17 13:09:15 +08:00
liwei 8cef0bb727 ci: fix android sign 2022-12-17 10:58:32 +08:00
207 changed files with 10888 additions and 2907 deletions

No files matched your search

+27
View File
@@ -0,0 +1,27 @@
## Donate
🙏 Thank you for considering a donation to Gopeed project! Your support helps us continue to develop and maintain our software.
<div style="text-align: center;">
<table>
<tr>
<th>微信</th>
<th>支付宝</th>
<th>Ko-fi</th>
<th>PayPal</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://ko-fi.com/gopeed" target="_blank">Click Me</a></td>
<td><a href="https://paypal.me/monkeyWie" 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

+15
View File
@@ -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
+6 -6
View File
@@ -1,8 +1,8 @@
<!--
请根据以下模版提issue
Please use the following template to create an issue.
-->
### 问题描述(必要)
### 版本号(必要)
### 操作系统(必要)
### 相关截图
### 相关日志
### Description(required)
### App Version(required)
### OS Version(required)
### Snapshots
### Log
+113 -60
View File
@@ -2,8 +2,14 @@ name: build
on:
workflow_dispatch:
release:
types: [published]
env:
GO_VERSION: 1.19
FLUTTER_VERSION: 3.10.6
permissions:
contents: write
jobs:
get-release:
runs-on: ubuntu-latest
@@ -12,24 +18,30 @@ jobs:
upload_url: ${{ steps.get-release.outputs.upload_url }}
steps:
- uses: actions/checkout@v3
- uses: thebritican/[email protected]
- uses: monkeyWie/get-latest-release@v1
id: get-release
with:
myToken: ${{ github.token }}
build-windows:
runs-on: windows-latest
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
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: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/monkeyWie/gopeed/bind/desktop
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"
@@ -43,7 +55,7 @@ jobs:
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.zip
Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$env:VERSION-windows-amd64-portable.zip"
cd build/windows
echo @"
@@ -77,9 +89,12 @@ jobs:
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
@@ -88,6 +103,9 @@ jobs:
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
@@ -96,7 +114,9 @@ jobs:
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-install.exe"
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:
@@ -108,24 +128,12 @@ jobs:
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
- run: mkdir -p ~/image-cache
- id: image-cache
uses: actions/cache@v3
with:
path: ~/image-cache
key: image-cache-xgo-v1.19
- if: steps.image-cache.outputs.cache-hit != 'true'
run: |
docker pull techknowlogick/xgo:go-1.19.x
docker save -o ~/image-cache/xgo.tar techknowlogick/xgo:go-1.19.x
- if: steps.image-cache.outputs.cache-hit == 'true'
run: docker load -i ~/image-cache/xgo.tar
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
@@ -138,27 +146,37 @@ jobs:
needs: [get-release, build-macos-arm64-lib]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
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/macos/Frameworks
path: ui/flutter/lib-arm64
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
run: |
PROJECT_DIR=$(pwd)
cd ui/flutter
# 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
@@ -172,14 +190,7 @@ jobs:
}
EOF
mkdir dist
appdmg appdmg.json dist/Gopeed-$VERSION-macos-arm64.dmg
# amd64
cd $PROJECT_DIR
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o bin/libgopeed.dylib github.com/monkeyWie/gopeed/bind/desktop
cp bin/libgopeed.dylib ui/flutter/build/macos/Build/Products/Release/Gopeed.app/Contents/Frameworks/
cd ui/flutter/build/macos/Build/Products/Release
appdmg appdmg.json dist/Gopeed-$VERSION-macos-amd64.dmg
appdmg appdmg.json dist/Gopeed-$VERSION-macos.dmg
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
@@ -191,31 +202,32 @@ jobs:
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
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
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/monkeyWie/gopeed/bind/desktop
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build linux
mkdir -p debian/gui
cp assets/icon/icon_1024.png debian/gui/gopeed.png
cat>debian/debian.yaml<<EOF
flutter_app:
flutter_app:
command: gopeed
arch: x64
parent: /usr/local/lib
control:
Package: gopeed
Version: ${VERSION:1}
Version: ${VERSION:1}
Architecture: amd64
Essential: no
Priority: optional
@@ -226,7 +238,7 @@ jobs:
cat>debian/gui/gopeed.desktop<<EOF
[Desktop Entry]
Version=${VERSION:1}
Version=${VERSION:1}
Name=Gopeed
GenericName=Gopeed
Comment=High speed downloader that supports all platforms.
@@ -250,28 +262,64 @@ jobs:
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
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-ios:
runs-on: macos-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: |
mkdir ui/flutter/android/app/libs
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/monkeyWie/gopeed/bind/mobile
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
echo $APK_KEYSTORE | base64 -d > android/app/upload-keystore.jks
flutter build apk
flutter build ios --no-codesign
mkdir Payload
cp -r build/ios/iphoneos/Runner.app Payload
zip -r -y Payload.zip Payload/Runner.app
mkdir dist
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-$VERSION-android.apk
mv Payload.zip dist/Gopeed-$VERSION-ios.ipa
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
@@ -283,17 +331,18 @@ jobs:
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
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
flutter build web --web-renderer html
cd ../../
cp -r ui/flutter/build/web cmd/web/dist
mkdir -p dist/zip
@@ -302,9 +351,13 @@ jobs:
export CGO_ENABLED=0
for goos in "${goos_arr[@]}"; do
for goarch in "${goarch_arr[@]}"; do
name=gopeed-web-$VERSION-$goos-$goarch
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/monkeyWie/gopeed/cmd/web \
(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 \
+8 -2
View File
@@ -16,6 +16,12 @@ on:
- "go.sum"
- "Dockerfile"
env:
GO_VERSION: 1.19
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
@@ -25,9 +31,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
go-version: ${{ env.GO_VERSION }}
- uses: release-drafter/release-drafter@v5
id: create_release
env:
+20 -10
View File
@@ -26,6 +26,10 @@ on:
- "go.mod"
- "go.sum"
env:
GO_VERSION: 1.19
FLUTTER_VERSION: 3.10.6
jobs:
# lint:
# name: Lint
@@ -41,30 +45,36 @@ jobs:
# go get -u golang.org/x/lint/golint
# golint -set_exit_status ./...
test:
name: Test
name: Test check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
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
fail_ci_if_error: true
build:
name: Build
name: Build check
runs-on: ubuntu-latest
# needs: [lint, test]
needs: [test]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
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 github.com/monkeyWie/gopeed/cmd/gopeed
run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build linux
+30
View File
@@ -0,0 +1,30 @@
# name: 'translator'
# on:
# issues:
# types: [opened, edited]
# issue_comment:
# types: [created, edited]
# discussion:
# types: [created, edited]
# discussion_comment:
# types: [created, edited]
# pull_request_target:
# types: [opened, edited]
# pull_request_review_comment:
# types: [created, edited]
# jobs:
# translate:
# permissions:
# issues: write
# discussions: write
# pull-requests: write
# runs-on: ubuntu-latest
# steps:
# - uses: lizheming/[email protected]
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# IS_MODIFY_TITLE: true
# APPEND_TRANSLATION: true
+1
View File
@@ -21,6 +21,7 @@ bin/
*.data
*.db
*.log
.DS_Store
+54
View File
@@ -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
```
+54
View File
@@ -0,0 +1,54 @@
# Gopeed コントリビューターガイド
まず最初に、Gopeed への貢献に興味を持っていただきありがとうございます。このガイドは、あなたが Gopeed の
開発に参加するための手助けとなるでしょう。
## ブランチの説明
このプロジェクトのメインブランチは `main` ブランチのみです。Gopeed の開発に参加したい場合は、
まずこのプロジェクトをフォークし、フォークしたプロジェクトで開発を行ってください。開発が完了したら、
このプロジェクトに PR を提出し、`main` ブランチにマージしてください。
## ローカル開発
開発およびデバッグはウェブ上で行うことを推奨する。まずバックエンドのサービスを起動し、
コマンドライン `go run cmd/api/main.go` で起動する。サービスのデフォルトポートは `9999` で、
次にフロントエンドの flutter プロジェクトを `debug` モードで起動して実行します。
## 翻訳
Gopeed の国際化ファイルは `ui/flutter/assets/locales` ディレクトリにあります。
このディレクトリに対応する言語ファイルを追加するだけでよいです。
ロケール編集後にロケールを生成:
```
get generate locales
```
翻訳については `en_us.dart` を参照してください。
## flutter での開発
コミットする前に `dart format ./ui/flutter` を実行し、コードを標準の dart フォーマットにしておくことを忘れないでください
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
```
+48
View File
@@ -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
```
+4 -4
View File
@@ -1,9 +1,9 @@
FROM cirrusci/flutter:3.3.7 AS flutter
FROM instrumentisto/flutter:3.10.6 AS flutter
WORKDIR /app
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
RUN flutter pub get
COPY ./ui/flutter ./
RUN flutter build web
RUN flutter build web --web-renderer html
FROM golang:1.19.3 AS go
WORKDIR /app
@@ -11,11 +11,11 @@ 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/monkeyWie/gopeed/cmd/web
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"]
ENTRYPOINT ["./gopeed"]
+55 -38
View File
@@ -1,17 +1,23 @@
[![Test Status](https://github.com/monkeyWie/gopeed/workflows/test/badge.svg)](https://github.com/monkeyWie/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/monkeyWie/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/monkeyWie/gopeed)
[![Release](https://img.shields.io/github/release/monkeyWie/gopeed.svg?style=flat-square)](https://github.com/monkeyWie/gopeed/releases)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png)
[English](/README.md) | [中文](/README_zh-CN.md)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
## Introduction
Gopeed is a high-speed downloader developed by `Golang`+`Flutter`, which supports (HTTP, BitTorrent, Magnet) protocol downloads and supports all platforms.
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms.
## Installation
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Develop Docs](https://docs.gopeed.com)
## Install
**Supported platforms**
@@ -19,11 +25,11 @@ Gopeed is a high-speed downloader developed by `Golang`+`Flutter`, which support
- [x] macos
- [x] linux
- [x] android
- [ ] ios
- [x] ios
- [x] web
- [x] docker
[To Release](https://github.com/monkeyWie/gopeed/releases/latest)
[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
@@ -32,7 +38,7 @@ Gopeed is a high-speed downloader developed by `Golang`+`Flutter`, which support
use `go install`:
```bash
go install github.com/monkeyWie/gopeed/cmd/gopeed
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
```
### Docker
@@ -40,27 +46,11 @@ go install github.com/monkeyWie/gopeed/cmd/gopeed
#### Start directly
```bash
docker run -d -p 9999:9999 -v /path/to/download:/download liwei2633/gopeed
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
```
#### Using Docker Compose
```yaml
version: '3'
services:
gopeed:
container_name: gopeed
ports:
- 9999:9999
image: liwei2633/gopeed
volumes:
- ~/Downloads:/download
restart: unless-stopped
```
Use the command below to start container:
```bash
docker-compose up -d
```
@@ -68,7 +58,10 @@ docker-compose up -d
#### Access Gopeed
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
@@ -83,26 +76,30 @@ This project is divided into two parts, the front end uses `flutter`, the back e
### Environment
1. Golang 1.19+
2. Flutter 3.0+
2. Flutter 3.10+
### Clone
```bash
git clone [email protected]:monkeyWie/gopeed.git
git clone [email protected]:GopeedLab/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.
First, you need to configure the environment according to the official [Flutter desktop website documention](https://docs.flutter.dev/development/platform-integration/desktop), then you will need to ensure the cgo environment is set up accordingly. For detailed instructions on setting up the cgo environment, please refer to relevant resources available online.
command:
- windows
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/monkeyWie/gopeed/bind/desktop
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build windows
```
@@ -110,7 +107,7 @@ flutter build windows
- macos
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/monkeyWie/gopeed/bind/desktop
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build macos
```
@@ -118,7 +115,7 @@ 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/monkeyWie/gopeed/bind/desktop
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build linux
```
@@ -129,6 +126,7 @@ Same as before, you also need to prepare the `cgo` environment, and then install
```bash
go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind
gomobile init
```
@@ -137,14 +135,20 @@ 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/monkeyWie/gopeed/bind/mobile
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build apk
```
#### Web
- ios
Web platform communicates directly with the backend http server, no additional environment is required.
```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build ios --no-codesign
```
#### Web
command:
@@ -154,8 +158,21 @@ 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/monkeyWie/gopeed/cmd/web
````
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
go run cmd/web/main.go
```
## Credits
### Contributors
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
</a>
### JetBrains
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## License
+179
View File
@@ -0,0 +1,179 @@
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
## はじめに
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。
見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://docs.gopeed.com)
## インストール
**サポートするプラットフォーム**
- [x] windows
- [x] macos
- [x] linux
- [x] android
- [x] ios
- [x] web
- [x] docker
[ダウンロード](https://github.com/GopeedLab/gopeed/releases/latest)
> ヒント: macos を開くのに失敗した場合は、ターミナルで `xattr -d com.apple.quarantine /Applications/Gopeed.app` コマンドを実行してください
### コマンドツール
`go install` を使用:
```bash
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
```
### Docker
#### 直接始める
```bash
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
```
#### Docker Compose の使用
```bash
docker-compose up -d
```
#### Gopeed へのアクセス
docker コンテナが起動しているときは、`http://localhost:9999` からウェブページにアクセスできます。
## 寄付
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](/.donate/index.md#donate)をご検討ください!
## ショーケース
![](_docs/img/ui-demo.png)
## 開発
このプロジェクトは二つの部分に分かれており、フロントエンドでは `flutter` を、バックエンドでは `Golang` を使用し、両者は `http` プロトコルで通信する。ユニックスシステムでは `unix socket` を、ウィンドウズシステムでは `tcp` プロトコルを使用します。
> フロントコードは `ui/flutter` ディレクトリにあります。
### 環境
1. Go 言語 1.19+
2. Flutter 3.10+
### クローン
```bash
git clone [email protected]:GopeedLab/gopeed.git
```
### コントリビュート
[CONTRIBUTING.md](/CONTRIBUTING_ja-JP.md) をご参照ください
### ビルド
#### デスクトップ
まず、[flutter デスクトップ公式サイトドキュメント](https://docs.flutter.dev/development/platform-integration/desktop)に従って環境を設定し、自分で検索できる `cgo` 環境を用意します。
コマンド:
- windows
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build windows
```
- macos
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build macos
```
- linux
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build linux
```
#### モバイル
先ほどと同じように、`cgo` 環境を準備し、`gomobile` をインストールする必要があります:
```bash
go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind
gomobile init
```
コマンド:
- android
```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build apk
```
- ios
```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build ios --no-codesign
```
#### Web
コマンド:
```bash
cd ui/flutter
flutter build web
cd ../../
rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
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
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## ライセンス
[GPLv3](LICENSE)
+53 -36
View File
@@ -1,17 +1,23 @@
[![Test Status](https://github.com/monkeyWie/gopeed/workflows/test/badge.svg)](https://github.com/monkeyWie/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/monkeyWie/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/monkeyWie/gopeed)
[![Release](https://img.shields.io/github/release/monkeyWie/gopeed.svg?style=flat-square)](https://github.com/monkeyWie/gopeed/releases)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png)
[English](/README.md) | [中文](/README_zh-CN.md)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
## 介绍
Gopeed 是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。
Gopeed(全称 Go Speed),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。
## 下载
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [开发文档](https://docs.gopeed.com/zh/)
## 安装
**已支持平台**
@@ -19,11 +25,11 @@ Gopeed 是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、
- [x] macos
- [x] linux
- [x] android
- [ ] ios
- [x] ios
- [x] web
- [x] docker
[点击前往](https://github.com/monkeyWie/gopeed/releases/latest)
[前往下载](https://github.com/GopeedLab/gopeed/releases/latest)
> 注:macos 版本运行如果提示损坏,请在终端执行 `xattr -d com.apple.quarantine /Applications/Gopeed.app` 命令
@@ -32,7 +38,7 @@ Gopeed 是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、
使用`go install`安装:
```bash
go install github.com/monkeyWie/gopeed/cmd/gopeed
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
```
### Docker
@@ -40,27 +46,11 @@ go install github.com/monkeyWie/gopeed/cmd/gopeed
#### 直接运行
```bash
docker run -d -p 9999:9999 -v /path/to/download:/download liwei2633/gopeed
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
```
#### 使用 Docker Compose
```yaml
version: '3'
services:
gopeed:
container_name: gopeed
ports:
- 9999:9999
image: liwei2633/gopeed
volumes:
- ~/Downloads:/download
restart: unless-stopped
```
输入以下指令启动容器:
```bash
docker-compose up -d
```
@@ -68,7 +58,10 @@ docker-compose up -d
#### 访问服务
当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。
> 提示:在设置页面把下载路径修改为 `/download` 以便在宿主机访问下载完的文件。
## 打赏
如果觉得项目对你有帮助,请考虑[打赏](/.donate/index.md#donate)以支持这个项目的发展,非常感谢!
## 界面展示
@@ -83,14 +76,18 @@ docker-compose up -d
### 环境要求
1. Golang 1.19+
2. Flutter 3.0+
2. Flutter 3.10+
### 克隆项目
```bash
git clone [email protected]:monkeyWie/gopeed.git
git clone [email protected]:GopeedLab/gopeed.git
```
### 贡献代码
请参考[贡献指南](CONTRIBUTING_zh-CN.md)
### 编译
#### 桌面端
@@ -102,7 +99,7 @@ git clone [email protected]:monkeyWie/gopeed.git
- windows
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/monkeyWie/gopeed/bind/desktop
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build windows
```
@@ -110,7 +107,7 @@ flutter build windows
- macos
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/monkeyWie/gopeed/bind/desktop
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build macos
```
@@ -118,7 +115,7 @@ 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/monkeyWie/gopeed/bind/desktop
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build linux
```
@@ -129,6 +126,7 @@ flutter build linux
```bash
go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind
gomobile init
```
@@ -137,14 +135,20 @@ 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/monkeyWie/gopeed/bind/mobile
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build apk
```
#### Web 端(推荐本地调试使用)
- ios
Web 端直接与后端 http 服务通讯,不需要额外准备环境。
```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build ios --no-codesign
```
#### Web 端
构建命令:
@@ -154,9 +158,22 @@ 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/monkeyWie/gopeed/cmd/web
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
go run cmd/web/main.go
```
## 感谢
### 贡献者
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
</a>
### JetBrains
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## 开源许可
基于 [GPLv3](LICENSE) 协议开源。
+1
View File
@@ -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

+6 -3
View File
@@ -2,8 +2,9 @@ package main
import (
"fmt"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
)
func main() {
@@ -16,7 +17,9 @@ func main() {
}
}).
Create(&base.Options{
Connections: 8,
Extra: http.OptsExtra{
Connections: 8,
},
})
if err != nil {
panic(err)
+2 -2
View File
@@ -3,8 +3,8 @@ package main
import "C"
import (
"encoding/json"
"github.com/monkeyWie/gopeed/pkg/rest"
"github.com/monkeyWie/gopeed/pkg/rest/model"
"github.com/GopeedLab/gopeed/pkg/rest"
"github.com/GopeedLab/gopeed/pkg/rest/model"
)
func main() {}
+2 -2
View File
@@ -4,8 +4,8 @@ package libgopeed
import "C"
import (
"encoding/json"
"github.com/monkeyWie/gopeed/pkg/rest"
"github.com/monkeyWie/gopeed/pkg/rest/model"
"github.com/GopeedLab/gopeed/pkg/rest"
"github.com/GopeedLab/gopeed/pkg/rest/model"
)
func Start(cfg string) (int, error) {
+3 -16
View File
@@ -2,28 +2,15 @@ package main
import (
_ "embed"
"fmt"
"github.com/monkeyWie/gopeed/pkg/rest"
"github.com/monkeyWie/gopeed/pkg/rest/model"
"net/http"
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
)
//go:embed banner.txt
var banner string
func main() {
cfg := &model.StartConfig{
Network: "tcp",
Address: "127.0.0.1:9999",
Storage: model.StorageBolt,
}
fmt.Println(banner)
srv, listener, err := rest.BuildServer(cfg)
if err != nil {
panic(err)
}
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
panic(err)
}
cmd.Start(cfg)
}
File renamed without changes.
+7 -7
View File
@@ -2,10 +2,10 @@ package main
import (
"fmt"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/download"
"github.com/monkeyWie/gopeed/pkg/protocol/http"
"github.com/monkeyWie/gopeed/pkg/util"
"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"
)
@@ -16,6 +16,7 @@ func main() {
args := parse()
var wg sync.WaitGroup
wg.Add(1)
_, err := download.Boot().
URL(args.url).
Listener(func(event *download.Event) {
@@ -46,7 +47,6 @@ func main() {
if err != nil {
panic(err)
}
wg.Add(1)
wg.Wait()
}
@@ -56,10 +56,10 @@ var (
)
func printProgress(task *download.Task, title string) {
rate := float64(task.Progress.Downloaded) / float64(task.Res.Size)
rate := float64(task.Progress.Downloaded) / float64(task.Meta.Res.Size)
completeWidth := int(progressWidth * rate)
speed := util.ByteFmt(task.Progress.Speed)
totalSize := util.ByteFmt(task.Res.Size)
totalSize := util.ByteFmt(task.Meta.Res.Size)
sb.WriteString(fmt.Sprintf("\r%s [", title))
for i := 0; i < progressWidth; i++ {
if i < completeWidth {
+40
View File
@@ -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)
}
}
-7
View File
@@ -1,7 +0,0 @@
_______ ______ .______ _______ _______ _______
/ _____| / __ \ | _ \ | ____|| ____|| \
| | __ | | | | | |_) | | |__ | |__ | .--. |
| | |_ | | | | | | ___/ | __| | __| | | | |
| |__| | | `--' | | | | |____ | |____ | '--' |
\______| \______/ | _| |_______||_______||_______/
+77
View File
@@ -0,0 +1,77 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
)
type args struct {
Address *string `json:"address"`
Port *int `json:"port"`
Username *string `json:"username"`
Password *string `json:"password"`
ApiToken *string `json:"apiToken"`
configPath *string
}
func parse() *args {
var cliArgs args
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Password")
cliArgs.ApiToken = flag.String("T", "", "API token, that can only be used when basic authentication is enabled.")
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
flag.Parse()
fmt.Println("parse")
// args priority: config file > cli args
cfgArgs := loadConfig(*cliArgs.configPath)
if cfgArgs.Address == nil {
cfgArgs.Address = cliArgs.Address
}
if cfgArgs.Port == nil {
cfgArgs.Port = cliArgs.Port
}
if cfgArgs.Username == nil {
cfgArgs.Username = cliArgs.Username
}
if cfgArgs.Password == nil {
cfgArgs.Password = cliArgs.Password
}
if cfgArgs.ApiToken == nil {
cfgArgs.ApiToken = cliArgs.ApiToken
}
return cfgArgs
}
func loadConfig(path string) *args {
var args args
if !filepath.IsAbs(path) {
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
return &args
}
path = filepath.Join(dir, path)
}
file, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return &args
}
fmt.Println("config file read failed, reason:" + err.Error())
return &args
}
if err = json.Unmarshal(file, &args); err != nil {
fmt.Println("config file parse failed, reason:" + err.Error())
return &args
}
fmt.Printf("config file loaded: %s\n", path)
return &args
}
+24 -20
View File
@@ -6,15 +6,11 @@ package main
import (
"embed"
"fmt"
"github.com/monkeyWie/gopeed/pkg/rest"
"github.com/monkeyWie/gopeed/pkg/rest/model"
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"io/fs"
"net/http"
)
//go:embed banner.txt
var banner string
//go:embed dist/*
var dist embed.FS
@@ -23,20 +19,28 @@ func main() {
if err != nil {
panic(err)
}
args := parse()
var webBasicAuth *model.WebBasicAuth
if isNotBlank(args.Username) && isNotBlank(args.Password) {
webBasicAuth = &model.WebBasicAuth{
Username: *args.Username,
Password: *args.Password,
}
}
cfg := &model.StartConfig{
Network: "tcp",
Address: "0.0.0.0:9999",
Storage: model.StorageBolt,
WebEnable: true,
WebFS: sub,
}
fmt.Println(banner)
srv, listener, err := rest.BuildServer(cfg)
if err != nil {
panic(err)
}
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
panic(err)
Network: "tcp",
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
Storage: model.StorageBolt,
ApiToken: *args.ApiToken,
WebEnable: true,
WebFS: sub,
WebBasicAuth: webBasicAuth,
}
cmd.Start(cfg)
}
func isNotBlank(str *string) bool {
return str != nil && *str != ""
}
+1 -1
View File
@@ -7,5 +7,5 @@ services:
- 9999:9999 # HTTP port (host:container)
image: liwei2633/gopeed
volumes:
- ~/Downloads:/download # replace with your prefer path
- ~/Downloads:/root/Downloads # replace with your prefer path
restart: unless-stopped
+40 -37
View File
@@ -1,76 +1,79 @@
module github.com/monkeyWie/gopeed
module github.com/GopeedLab/gopeed
go 1.19
require (
github.com/anacrolix/torrent v1.46.0
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9
github.com/anacrolix/torrent v1.52.5
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/matoous/go-nanoid/v2 v2.0.0
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
go.etcd.io/bbolt v1.3.6
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
)
require (
crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508 // indirect
github.com/RoaringBitmap/roaring v1.0.1-0.20220510143707-3f418c4f42a4 // indirect
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c // indirect
github.com/RoaringBitmap/roaring v1.2.3 // indirect
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
github.com/anacrolix/chansync v0.3.0 // indirect
github.com/anacrolix/dht/v2 v2.16.2-0.20220311024416-dd658f18fd51 // indirect
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 // 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/generics v0.0.0-20230428105757-683593396d68 // indirect
github.com/anacrolix/go-libutp v1.3.1 // indirect
github.com/anacrolix/log v0.14.0 // 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/multiless v0.3.0 // indirect
github.com/anacrolix/stm v0.4.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/bits-and-blooms/bitset v1.2.2 // 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/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/handlers v1.5.1 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/lispad/go-generics-tools v1.0.0 // indirect
github.com/lispad/go-generics-tools v1.1.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/dtls/v2 v2.2.4 // indirect
github.com/pion/ice/v2 v2.2.6 // indirect
github.com/pion/interceptor v0.1.11 // 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/rtp v1.7.13 // 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/sdp/v3 v3.0.5 // indirect
github.com/pion/srtp/v2 v2.0.9 // 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/pion/transport v0.13.1 // indirect
github.com/pion/transport/v2 v2.0.0 // indirect
github.com/pion/turn/v2 v2.0.8 // indirect
github.com/pion/udp v0.1.4 // indirect
github.com/pion/webrtc/v3 v3.1.42 // 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/mobile v0.0.0-20220928052126-fa6bcb076835 // indirect
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 // 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/tools v0.1.8-0.20211029000441-d6a9af8af023 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
github.com/tidwall/btree v1.6.0 // indirect
go.opentelemetry.io/otel v1.8.0 // indirect
go.opentelemetry.io/otel/trace v1.8.0 // indirect
golang.org/x/crypto v0.5.0 // indirect
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
)
+120 -89
View File
@@ -3,43 +3,46 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797 h1:yDf7ARQc637HoxDho7xjqdvO5ZA2Yb+xzv/fOnnvZzw=
crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk=
crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508 h1:fILCBBFnjnrQ0whVJlGhfv1E/QiaFDNtGFBObEVRnYg=
crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c h1:wvzox0eLO6CKQAMcOqz7oH3UFqMpMmK7kwmwV+22HIs=
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
github.com/RoaringBitmap/roaring v1.0.1-0.20220510143707-3f418c4f42a4 h1:LxR70Si8X8IlYTeKiPReOBt9pla6wEJDlkawfwDdrB0=
github.com/RoaringBitmap/roaring v1.0.1-0.20220510143707-3f418c4f42a4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY=
github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0=
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0/go.mod h1:q37NoqncT41qKc048STsifIt69LfUJ8SrWWcz/yam5k=
github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk=
github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8=
github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI=
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anacrolix/chansync v0.3.0 h1:lRu9tbeuw3wl+PhMu/r+JJCRu5ArFXIluOgdF0ao6/U=
github.com/anacrolix/chansync v0.3.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
github.com/anacrolix/dht/v2 v2.16.2-0.20220311024416-dd658f18fd51 h1:issCwqC43gQ7n0gg9rn0EeVYXnQMI7vlnWub4oidtlU=
github.com/anacrolix/dht/v2 v2.16.2-0.20220311024416-dd658f18fd51/go.mod h1:osiyaNrMLG9dw7wUtVMaII/NdCjlXeHjUcYzXnmop68=
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 h1:8V0K09lrGoeT2KRJNOtspA7q+OMxGwQqK/Ug0IiaaRE=
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444/go.mod h1:MctKM1HS5YYDb3F30NGJxLE+QPuqWoT5ReW/4jt8xew=
github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
github.com/anacrolix/envpprof v1.2.1 h1:25TJe6t/i0AfzzldiGFKCpD+s+dk8lONBcacJZB2rdE=
github.com/anacrolix/envpprof v1.2.1/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
github.com/anacrolix/generics v0.0.0-20220618083756-f99e35403a60 h1:k4/h2B1gGF+PJGyGHxs8nmHHt1pzWXZWBj6jn4OBlRc=
github.com/anacrolix/generics v0.0.0-20220618083756-f99e35403a60/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
github.com/anacrolix/go-libutp v1.2.0 h1:sjxoB+/ARiKUR7IK/6wLWyADIBqGmu1fm0xo+8Yy7u0=
github.com/anacrolix/go-libutp v1.2.0/go.mod h1:RrJ3KcaDcf9Jqp33YL5V/5CBEc6xMc7aJL8wXfuWL50=
github.com/anacrolix/generics v0.0.0-20230428105757-683593396d68 h1:fyXlBfnlFzZSFckJ8QLb2lfmWfY++4RiUnae7ZMuv0A=
github.com/anacrolix/generics v0.0.0-20230428105757-683593396d68/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
github.com/anacrolix/go-libutp v1.3.1 h1:idJzreNLl+hNjGC3ZnUOjujEaryeOGgkwHLqSGoige0=
github.com/anacrolix/go-libutp v1.3.1/go.mod h1:heF41EC8kN0qCLMokLBVkB8NXiLwx3t8R8810MTNI5o=
github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
github.com/anacrolix/log v0.10.0/go.mod h1:s5yBP/j046fm9odtUTbHOfDUq/zh1W8OkPpJtnX0oQI=
github.com/anacrolix/log v0.10.1-0.20220123034749-3920702c17f8/go.mod h1:GmnE2c0nvz8pOIPUSC9Rawgefy1sDXqposC2wgtBZE4=
github.com/anacrolix/log v0.13.1 h1:BmVwTdxHd5VcNrLylgKwph4P4wf+5VvPgOK4yi91fTY=
github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68=
github.com/anacrolix/log v0.14.0 h1:mYhTSemILe/Z8tIxbGdTIWWpPspI8W/fhZHpoFbDaL0=
github.com/anacrolix/log v0.14.0/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62 h1:P04VG6Td13FHMgS5ZBcJX23NPC/fiC4cp9bXwYujdYM=
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM=
github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s=
@@ -53,16 +56,16 @@ github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5ur
github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY=
github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA=
github.com/anacrolix/missinggo/v2 v2.5.2/go.mod h1:yNvsLrtZYRYCOI+KRH/JM8TodHjtIE/bjOGhQaLOWIE=
github.com/anacrolix/missinggo/v2 v2.7.0 h1:4fzOAAn/VCvfWGviLmh64MPMttrlYew81JdPO7nSHvI=
github.com/anacrolix/missinggo/v2 v2.7.0/go.mod h1:2IZIvmRTizALNYFYXsPR7ofXPzJgyBpKZ4kMqMEICkI=
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9 h1:W/oGeHhYwxueeiDjQfmK9G+X9M2xJgfTtow62v0TWAs=
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9/go.mod h1:mIEtp9pgaXqt8VQ3NQxFOod/eQ1H0D1XsZzKUQfwtac=
github.com/anacrolix/mmsg v0.0.0-20180515031531-a4a3ba1fc8bb/go.mod h1:x2/ErsYUmT77kezS63+wzZp8E3byYB0gzirM/WMBLfw=
github.com/anacrolix/mmsg v1.0.0 h1:btC7YLjOn29aTUAExJiVUhQOuf/8rhm+/nWCMAnL3Hg=
github.com/anacrolix/mmsg v1.0.0/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc=
github.com/anacrolix/multiless v0.2.1-0.20211218050420-533661eef5dc h1:K047jUtd0Xv4SEpv/5DoBgDvj4ZNpT1SOVtMlFpRrh0=
github.com/anacrolix/multiless v0.2.1-0.20211218050420-533661eef5dc/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4=
github.com/anacrolix/multiless v0.3.0 h1:5Bu0DZncjE4e06b9r1Ap2tUY4Au0NToBP5RpuEngSis=
github.com/anacrolix/multiless v0.3.0/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4=
github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg=
github.com/anacrolix/stm v0.3.0 h1:peQncJSNJtk1YBrFbW0DLKYqll+sa0kOk8EvXRcO+wA=
github.com/anacrolix/stm v0.3.0/go.mod h1:spImf/rXwiAUoYYJK1YCZeWkpaHZ3kzjGFjwK5OStfU=
github.com/anacrolix/stm v0.4.0 h1:tOGvuFwaBjeu1u9X1eIh9TX8OEedEiEQ1se1FjhFnXY=
github.com/anacrolix/stm v0.4.0/go.mod h1:GCkwqWoAsP7RfLW+jw+Z0ovrt2OO7wRzcTtFYMYY5t8=
github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk=
github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
github.com/anacrolix/sync v0.4.0 h1:T+MdO/u87ir/ijWsTFsPYw5jVm0SMm4kVpg8t4KF38o=
@@ -70,8 +73,8 @@ github.com/anacrolix/sync v0.4.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DC
github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
github.com/anacrolix/torrent v1.46.0 h1:m5SGlW4p0dJkqrIh4bCqzcKbKFFmfJorf9LSpSM5dEc=
github.com/anacrolix/torrent v1.46.0/go.mod h1:3DE+VA4AgyfKDPjZcIo70D3VFZRo3bfdEBn70CGjca4=
github.com/anacrolix/torrent v1.52.5 h1:jWowdx+EU6zFVfBwmnL0d3H4J6vTFEGOrHI35YdfIT8=
github.com/anacrolix/torrent v1.52.5/go.mod h1:CcM8oPMYye5J42cSqJrmUpqwRFgSsJQ1jCEHwygqnqQ=
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 h1:QAVZ3pN/J4/UziniAhJR2OZ9Ox5kOY2053tBbbqUPYA=
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96/go.mod h1:Wa6n8cYIdaG35x15aH3Zy6d03f7P728QfdcDeD/IEOs=
github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4=
@@ -85,8 +88,9 @@ github.com/benbjohnson/immutable v0.3.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylH
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA=
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bits-and-blooms/bitset v1.2.2 h1:J5gbX05GpMdBjCvQ9MteIg2KKDExr7DrgK+Yc15FvIk=
github.com/bits-and-blooms/bitset v1.2.2/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
@@ -109,7 +113,7 @@ github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8E
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
@@ -122,6 +126,11 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -145,14 +154,14 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -166,9 +175,10 @@ github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
@@ -186,14 +196,14 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lispad/go-generics-tools v1.0.0 h1:pPtt9QERE5sGQfwvLakKe2sjhcNKKsDSnr3939ofj1E=
github.com/lispad/go-generics-tools v1.0.0/go.mod h1:stn7X24ZIyFvaSyttafq3VlJzGJJJkUtLYdbgi/gopM=
github.com/lispad/go-generics-tools v1.1.0 h1:mbSgcxdFVmpoyso1X/MJHXbSbSL3dD+qhRryyxk+/XY=
github.com/lispad/go-generics-tools v1.1.0/go.mod h1:2csd1EJljo/gy5qG4khXol7ivCPptNjG5Uv2X8MgK84=
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0=
github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=
@@ -222,44 +232,47 @@ github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
github.com/pion/dtls/v2 v2.1.1/go.mod h1:qG3gA7ZPZemBqpEFqRKyURYdKEwFZQCGb7gv9T3ON3Y=
github.com/pion/dtls/v2 v2.1.2 h1:22Q1Jk9L++Yo7BIf9130MonNPfPVb+YgdYLeyQotuAA=
github.com/pion/dtls/v2 v2.1.2/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
github.com/pion/ice/v2 v2.1.20 h1:xpxXyX5b4WjCh/D905gzBeW/hbJxMEPx2ptVfrhVE6M=
github.com/pion/ice/v2 v2.1.20/go.mod h1:hEAldRzBhTtAfvlU1V/2/nLCMvveQWFKPNCop+63/Iw=
github.com/pion/interceptor v0.1.7 h1:HThW0tIIKT9RRoDWGURe8rlZVOx0fJHxBHpA0ej0+bo=
github.com/pion/interceptor v0.1.7/go.mod h1:Lh3JSl/cbJ2wP8I3ccrjh1K/deRGRn3UlSPuOTiHb6U=
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
github.com/pion/dtls/v2 v2.2.4 h1:YSfYwDQgrxMYXLBc/m7PFY5BVtWlNm/DN4qoU2CbcWg=
github.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw=
github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/rtp v1.7.4 h1:4dMbjb1SuynU5OpA3kz1zHK+u+eOCQjW3MAeVHf1ODA=
github.com/pion/rtp v1.7.4/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8=
github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
github.com/pion/srtp/v2 v2.0.5 h1:ks3wcTvIUE/GHndO3FAvROQ9opy0uLELpwHJaQ1yqhQ=
github.com/pion/srtp/v2 v2.0.5/go.mod h1:8k6AJlal740mrZ6WYxc4Dg6qDqqhxoRG2GSjlUhDF0A=
github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/srtp/v2 v2.0.9 h1:JJq3jClmDFBPX/F5roEb0U19jSU7eUhyDqR/NZ34EKQ=
github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
github.com/pion/transport v0.13.0 h1:KWTA5ZrQogizzYwPEciGtHPLwpAjE91FgXnyu+Hv2uY=
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
github.com/pion/turn/v2 v2.0.6 h1:AsXjSPR6Im15DMTB39NlfdTY9BQfieANPBjdg/aVNwY=
github.com/pion/turn/v2 v2.0.6/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
github.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4=
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
github.com/pion/webrtc/v3 v3.1.24-0.20220208053747-94262c1b2b38 h1:+IEql+S+YAj3S5e7Ftl/u4xPcZGG0WwLFsyFj6NRTz4=
github.com/pion/webrtc/v3 v3.1.24-0.20220208053747-94262c1b2b38/go.mod h1:L5S/oAhL0Fzt/rnftVQRrP80/j5jygY7XRZzWwFx6P4=
github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8=
github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us=
github.com/pion/webrtc/v3 v3.1.42 h1:wJEQFIXVanptnQcHOLTuIo4AtGB2+mG2x4OhIhnITOA=
github.com/pion/webrtc/v3 v3.1.42/go.mod h1:ffD9DulDrPxyWvDPUIPAOSAWx9GUlOExiJPf7cCcMLA=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -285,10 +298,10 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/dnscache v0.0.0-20210201191234-295bba877686 h1:IJ6Df0uxPDtNoByV0KkzVKNseWvZFCNM/S9UoyOMCSI=
github.com/rs/dnscache v0.0.0-20210201191234-295bba877686/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs=
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
@@ -300,6 +313,8 @@ github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:X
github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -307,48 +322,49 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/tidwall/btree v1.3.1 h1:636+tdVDs8Hjcf35Di260W2xCW4KuoXOKyk9QWOvCpA=
github.com/tidwall/btree v1.3.1/go.mod h1:LGm8L/DZjPLmeWGjv5kFrY8dL4uVhMmzmmLYmsObdKE=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg=
github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459 h1:x9pIfbdIjnw+Ylb2vE27Gtqb7BDmfR+nLcJwvbJh98U=
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459/go.mod h1:nFvuG3SWu3VWqobG3cX8nt57wXU0OOFapeCs/8axIuM=
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v1.8.0 h1:zcvBFizPbpa1q7FehvFiHbQwGzmPILebO0tyqIR5Djg=
go.opentelemetry.io/otel v1.8.0/go.mod h1:2pkj+iMj0o03Y+cW6/m8Y4WkRdYN3AvCXCnzRMp9yvM=
go.opentelemetry.io/otel/trace v1.8.0 h1:cSy0DF9eGI5WIfNwZ1q2iUyGj00tGzP24dE1lOlHrfY=
go.opentelemetry.io/otel/trace v1.8.0/go.mod h1:0Bt3PXY8w+3pheS3hQUt+wow8b1ojPaTBoTCh2zIFI4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171 h1:TfdoLivD44QwvssI9Sv1xwa5DcL5XQr4au4sZ2F2NV4=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0=
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20220928052126-fa6bcb076835 h1:v6yt1xSu+4xV0fPEmB8s2BHdOaoU0SSZrVQzj6pRvqk=
golang.org/x/mobile v0.0.0-20220928052126-fa6bcb076835/go.mod h1:M32cGdzp91A8Ex9qQtyZinr19EYxzkFqDjW2oyHzTDQ=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4=
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/net v0.0.0-20180524181706-dfa909b99c79/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -364,13 +380,19 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -380,7 +402,7 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -405,34 +427,42 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023 h1:0c3L82FDQ5rt1bjTBlchS8t6RQ6299/+5bWMnRLh+uI=
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -464,7 +494,8 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+30 -10
View File
@@ -1,29 +1,48 @@
package fetcher
import (
"github.com/monkeyWie/gopeed/internal/controller"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/pkg/base"
"path"
)
// Fetcher defines the interface for a download protocol.
// One fetcher for each download task
type Fetcher interface {
// Name return the name of the protocol.
Name() string
Setup(ctl *controller.Controller) error
// Resolve resource info from request
Resolve(req *base.Request) (res *base.Resource, err error)
Resolve(req *base.Request) error
// Create ready to download, but not started
Create(res *base.Resource, opts *base.Options) (err error)
Start() (err error)
Pause() (err error)
Continue() (err error)
Close() (err error)
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
// 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.
@@ -36,11 +55,12 @@ type FetcherBuilder interface {
// Store fetcher
Store(fetcher Fetcher) (any, error)
// Restore fetcher
Restore() (v any, f func(res *base.Resource, opts *base.Options, v any) Fetcher)
Restore() (v any, f func(meta *FetcherMeta, v any) Fetcher)
}
type DefaultFetcher struct {
Ctl *controller.Controller
Meta *FetcherMeta
DoneCh chan error
}
+190 -127
View File
@@ -1,32 +1,37 @@
package bt
import (
"bytes"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/storage"
"github.com/monkeyWie/gopeed/internal/controller"
"github.com/monkeyWie/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/util"
"path"
"path/filepath"
"sync"
"sync/atomic"
"time"
)
var client *torrent.Client
var (
client *torrent.Client
lock sync.Mutex
torrentDirMap = make(map[string]string)
ftMap = make(map[string]*fileTorrentImpl)
)
type Fetcher struct {
Ctl *controller.Controller
ctl *controller.Controller
torrent *torrent.Torrent
res *base.Resource
opts *base.Options
meta *fetcher.FetcherMeta
ready atomic.Bool
progress fetcher.Progress
torrentPaths map[string]string
torrentReady atomic.Bool
create atomic.Bool
progress fetcher.Progress
}
func (f *Fetcher) Name() string {
@@ -34,43 +39,140 @@ func (f *Fetcher) Name() string {
}
func (f *Fetcher) Setup(ctl *controller.Controller) (err error) {
f.Ctl = ctl
var once sync.Once
once.Do(func() {
cfg := torrent.NewDefaultClientConfig()
cfg.ListenPort = 0
f.ctl = ctl
if f.meta == nil {
f.meta = &fetcher.FetcherMeta{}
}
return
}
// Support custom download path for each torrent
pieceCompletion, err := storage.NewDefaultPieceCompletionForDir(cfg.DataDir)
if err != nil {
pieceCompletion = storage.NewMapPieceCompletion()
func (f *Fetcher) initClient() (err error) {
lock.Lock()
defer lock.Unlock()
if client != nil {
return
}
cfg := torrent.NewDefaultClientConfig()
cfg.ListenPort = 0
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
ClientBaseDir: cfg.DataDir,
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
if dir, ok := torrentDirMap[infoHash.String()]; ok {
ft.setTorrentDir(dir)
}
ftMap[infoHash.String()] = ft
},
})
client, err = torrent.NewClient(cfg)
return
}
func (f *Fetcher) Resolve(req *base.Request) error {
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
return err
}
if err := f.addTorrent(req); err != nil {
return err
}
go func() {
// recycle unused torrent resource
time.Sleep(time.Minute * 3)
if !f.create.Load() {
f.torrentReady.Store(false)
f.safeDrop()
}
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 {
}()
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.create.Store(true)
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 != "" {
torrentDirMap[f.meta.Res.Hash] = path.Join(f.meta.Opts.Path, f.meta.Res.RootDir)
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
// reuse resolve fetcher
ft.setTorrentDir(torrentDirMap[f.meta.Res.Hash])
}
}
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
return nil
}
func (f *Fetcher) Start() (err error) {
if !f.torrentReady.Load() {
if err = f.addTorrent(f.meta.Req); err != nil {
return
}
f.torrentPaths = make(map[string]string)
})
}
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.torrentReady.Store(false)
f.safeDrop()
return
}
func (f *Fetcher) Continue() (err error) {
return f.Start()
}
func (f *Fetcher) Close() (err error) {
f.safeDrop()
return nil
}
func (f *Fetcher) safeDrop() {
defer func() {
// ignore panic
_ = recover()
}()
f.torrent.Drop()
}
func (f *Fetcher) Wait() (err error) {
for {
if f.ready.Load() {
if f.torrentReady.Load() {
done := true
for _, selectIndex := range f.opts.SelectFiles {
for _, selectIndex := range f.meta.Opts.SelectFiles {
file := f.torrent.Files()[selectIndex]
if file.BytesCompleted() < file.Length() {
done = false
@@ -81,14 +183,14 @@ func (f *Fetcher) Wait() (err error) {
// remove unselected files
for i, file := range f.torrent.Files() {
selected := false
for _, selectIndex := range f.opts.SelectFiles {
for _, selectIndex := range f.meta.Opts.SelectFiles {
if i == selectIndex {
selected = true
break
}
}
if !selected {
util.SafeRemove(filepath.Join(f.opts.Path, file.Path()))
util.SafeRemove(filepath.Join(f.meta.Opts.Path, file.Path()))
}
}
break
@@ -99,124 +201,81 @@ func (f *Fetcher) Wait() (err error) {
return nil
}
func (f *Fetcher) Resolve(req *base.Request) (res *base.Resource, err error) {
if err = f.addTorrent(req.URL, true); err != nil {
return
}
defer f.torrent.Drop()
res = &base.Resource{
Req: req,
Name: f.torrent.Name(),
Range: true,
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()
}
return
}
func (f *Fetcher) Create(res *base.Resource, opts *base.Options) (err error) {
f.res = res
f.opts = opts
if len(f.opts.SelectFiles) == 0 {
f.opts.SelectFiles = make([]int, 0)
for i := range f.torrent.Files() {
f.opts.SelectFiles = append(f.opts.SelectFiles, i)
}
}
if f.opts.Path != "" {
f.torrentPaths[res.Hash] = f.opts.Path
}
f.progress = make(fetcher.Progress, len(f.opts.SelectFiles))
f.ready.Store(false)
return nil
}
func (f *Fetcher) Start() (err error) {
if err = f.addTorrent(f.res.Req.URL, false); err != nil {
return
}
files := f.torrent.Files()
if len(f.opts.SelectFiles) == len(files) {
f.torrent.DownloadAll()
} else {
for _, selectIndex := range f.opts.SelectFiles {
file := files[selectIndex]
file.Download()
}
}
return
}
func (f *Fetcher) Pause() (err error) {
f.ready.Store(false)
f.torrent.Drop()
return
}
func (f *Fetcher) Continue() (err error) {
return f.Start()
}
func (f *Fetcher) Close() (err error) {
f.torrent.Drop()
return nil
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
return f.meta
}
func (f *Fetcher) Progress() fetcher.Progress {
if !f.ready.Load() {
if !f.torrentReady.Load() {
return f.progress
}
for i := range f.progress {
selectIndex := f.opts.SelectFiles[i]
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, resolve bool) (err error) {
schema := util.ParseSchema(url)
func (f *Fetcher) addTorrent(req *base.Request) (err error) {
if err = f.initClient(); err != nil {
return
}
schema := util.ParseSchema(req.URL)
if schema == "MAGNET" {
f.torrent, err = client.AddMagnet(url)
f.torrent, err = client.AddMagnet(req.URL)
} else if schema == "APPLICATION/X-BITTORRENT" {
_, data := util.ParseDataUri(req.URL)
buf := bytes.NewBuffer(data)
var metaInfo *metainfo.MetaInfo
metaInfo, err = metainfo.Load(buf)
if err != nil {
return err
}
f.torrent, err = client.AddTorrent(metaInfo)
} else {
f.torrent, err = client.AddTorrentFromFile(url)
f.torrent, err = client.AddTorrentFromFile(req.URL)
}
if err != nil {
return
}
var cfg config
exist, err := f.Ctl.GetConfig(&cfg)
exist, err := f.ctl.GetConfig(&cfg)
if err != nil {
return
}
// use map to deduplicate
trackers := make(map[string]bool)
if req.Extra != nil {
extra := req.Extra.(*bt.ReqExtra)
if len(extra.Trackers) > 0 {
for _, tracker := range extra.Trackers {
trackers[tracker] = true
}
}
}
if exist && len(cfg.Trackers) > 0 {
announceList := make([][]string, 0)
for _, tracker := range cfg.Trackers {
trackers[tracker] = true
}
}
if len(trackers) > 0 {
announceList := make([][]string, 0)
for tracker := range trackers {
announceList = append(announceList, []string{tracker})
}
f.torrent.AddTrackers(announceList)
}
<-f.torrent.GotInfo()
if resolve {
return
}
f.ready.Store(true)
f.torrentReady.Store(true)
return
}
type FetcherBuilder struct {
}
var schemes = []string{"FILE", "MAGNET"}
var schemes = []string{"FILE", "MAGNET", "APPLICATION/X-BITTORRENT"}
func (fb *FetcherBuilder) Schemes() []string {
return schemes
@@ -230,6 +289,10 @@ func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
return nil, nil
}
func (fb *FetcherBuilder) Restore() (v any, f func(res *base.Resource, opts *base.Options, v any) fetcher.Fetcher) {
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,
}
}
}
+59 -17
View File
@@ -1,11 +1,14 @@
package bt
import (
"encoding/base64"
"encoding/json"
"github.com/monkeyWie/gopeed/internal/controller"
"github.com/monkeyWie/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed/internal/test"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
"os"
"reflect"
"testing"
)
@@ -14,23 +17,26 @@ 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) {
res, err := fetcher.Resolve(&base.Request{
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
fetcher := buildFetcher()
buf, err := os.ReadFile("./testdata/ubuntu-22.04-live-server-amd64.iso.torrent")
if err != nil {
t.Fatal(err)
}
// convert to data uri
dataUri := "data:application/x-bittorrent;base64," + base64.StdEncoding.EncodeToString(buf)
err = fetcher.Resolve(&base.Request{
URL: dataUri,
})
if err != nil {
panic(err)
}
want := &base.Resource{
Req: res.Req,
Name: "ubuntu-22.04-live-server-amd64.iso",
Size: 1466714112,
Range: true,
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",
@@ -39,8 +45,44 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
},
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
}
if !reflect.DeepEqual(want, res) {
t.Errorf("Resolve() got = %v, want %v", res, want)
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
}
}
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",
Extra: bt.ReqExtra{
Trackers: []string{
"udp://tracker.birkenwald.de:6969/announce",
"udp://tracker.bitsearch.to:1337/announce",
},
},
})
if err != nil {
panic(err)
}
want := &base.Resource{
Name: "ubuntu-22.04-live-server-amd64.iso",
Size: 1466714112,
Range: true,
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)
}
}
+214
View File
@@ -0,0 +1,214 @@
// modify from github.com/anacrolix/torrent/storage/file.go
package bt
import (
"fmt"
"github.com/anacrolix/torrent/storage"
"io"
"os"
"path/filepath"
"github.com/anacrolix/missinggo/v2"
"github.com/anacrolix/torrent/common"
"github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/segments"
)
// File-based storage for torrents, that isn't yet bound to a particular torrent.
type fileClientImpl struct {
opts newFileClientOpts
}
type newFileClientOpts struct {
// The base directory for all downloads.
ClientBaseDir string
HandleFileTorrent func(infoHash metainfo.Hash, ft *fileTorrentImpl)
FilePathMaker storage.FilePathMaker
PieceCompletion storage.PieceCompletion
}
func newFileOpts(opts newFileClientOpts) storage.ClientImplCloser {
if opts.FilePathMaker == nil {
opts.FilePathMaker = func(opts storage.FilePathMakerOpts) string {
var parts []string
if opts.Info.Name != metainfo.NoName {
parts = append(parts, opts.Info.Name)
}
return filepath.Join(append(parts, opts.File.Path...)...)
}
}
if opts.PieceCompletion == nil {
ret, err := storage.NewDefaultPieceCompletionForDir(opts.ClientBaseDir)
if err != nil {
ret = storage.NewMapPieceCompletion()
}
opts.PieceCompletion = ret
}
return fileClientImpl{opts}
}
func (fs fileClientImpl) Close() error {
return fs.opts.PieceCompletion.Close()
}
func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
upvertedFiles := info.UpvertedFiles()
files := make([]file, 0, len(upvertedFiles))
for _, fileInfo := range upvertedFiles {
filePath := filepath.Join("", fs.opts.FilePathMaker(storage.FilePathMakerOpts{
Info: info,
File: &fileInfo,
}))
f := file{
rawPath: filePath,
path: filePath,
length: fileInfo.Length,
}
if f.length == 0 {
err = CreateNativeZeroLengthFile(f.path)
if err != nil {
err = fmt.Errorf("creating zero length file: %w", err)
return
}
}
files = append(files, f)
}
t := &fileTorrentImpl{
files,
segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
infoHash,
fs.opts.PieceCompletion,
}
fs.opts.HandleFileTorrent(infoHash, t)
return storage.TorrentImpl{
Piece: t.Piece,
Close: t.Close,
}, nil
}
type file struct {
// The safe, OS-local file path.
rawPath string
path string
length int64
}
type fileTorrentImpl struct {
files []file
segmentLocater segments.Index
infoHash metainfo.Hash
completion storage.PieceCompletion
}
func (fts *fileTorrentImpl) Piece(p metainfo.Piece) storage.PieceImpl {
// Create a view onto the file-based torrent storage.
_io := fileTorrentImplIO{fts}
// Return the appropriate segments of this.
return &filePieceImpl{
fts,
p,
missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
io.NewSectionReader(_io, p.Offset(), p.Length()),
}
}
func (fts *fileTorrentImpl) Close() error {
return nil
}
func (fts *fileTorrentImpl) setTorrentDir(dir string) {
for i, f := range fts.files {
fts.files[i].path = filepath.Join(dir, f.rawPath)
}
}
// A helper to create zero-length files which won't appear for file-orientated storage since no
// writes will ever occur to them (no torrent data is associated with a zero-length file). The
// caller should make sure the file name provided is safe/sanitized.
func CreateNativeZeroLengthFile(name string) error {
os.MkdirAll(filepath.Dir(name), 0o777)
var f io.Closer
f, err := os.Create(name)
if err != nil {
return err
}
return f.Close()
}
// Exposes file-based storage of a torrent, as one big ReadWriterAt.
type fileTorrentImplIO struct {
fts *fileTorrentImpl
}
// Returns EOF on short or missing file.
func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
f, err := os.Open(file.path)
if os.IsNotExist(err) {
// File missing is treated the same as a short file.
err = io.EOF
return
}
if err != nil {
return
}
defer f.Close()
// Limit the read to within the expected bounds of this file.
if int64(len(b)) > file.length-off {
b = b[:file.length-off]
}
for off < file.length && len(b) != 0 {
n1, err1 := f.ReadAt(b, off)
b = b[n1:]
n += n1
off += int64(n1)
if n1 == 0 {
err = err1
break
}
}
return
}
// Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
n += n1
b = b[n1:]
err = err1
return err == nil // && int64(n1) == e.Length
})
if len(b) != 0 && err == nil {
err = io.EOF
}
return
}
func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
// log.Printf("write at %v: %v bytes", off, len(p))
fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
name := fst.fts.files[i].path
os.MkdirAll(filepath.Dir(name), 0o777)
var f *os.File
f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
if err != nil {
return false
}
var n1 int
n1, err = f.WriteAt(p[:e.Length], e.Start)
// log.Printf("%v %v wrote %v: %v", i, e, n1, err)
closeErr := f.Close()
n += n1
p = p[n1:]
if err == nil {
err = closeErr
}
if err == nil && int64(n1) != e.Length {
err = io.ErrShortWrite
}
return err == nil
})
return
}
+36
View File
@@ -0,0 +1,36 @@
// modify from github.com/anacrolix/torrent/storage/file_misc.go
package bt
import "github.com/anacrolix/torrent/metainfo"
type requiredLength struct {
fileIndex int
length int64
}
func extentCompleteRequiredLengths(info *metainfo.Info, off, n int64) (ret []requiredLength) {
if n == 0 {
return
}
for i, fi := range info.UpvertedFiles() {
if off >= fi.Length {
off -= fi.Length
continue
}
n1 := n
if off+n1 > fi.Length {
n1 = fi.Length - off
}
ret = append(ret, requiredLength{
fileIndex: i,
length: off + n1,
})
n -= n1
if n == 0 {
return
}
off = 0
}
return
}
+61
View File
@@ -0,0 +1,61 @@
// modify from github.com/anacrolix/torrent/storage/file_piece.go
package bt
import (
"github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/storage"
"io"
"log"
"os"
)
type filePieceImpl struct {
*fileTorrentImpl
p metainfo.Piece
io.WriterAt
io.ReaderAt
}
var _ storage.PieceImpl = (*filePieceImpl)(nil)
func (fs *filePieceImpl) pieceKey() metainfo.PieceKey {
return metainfo.PieceKey{InfoHash: fs.infoHash, Index: fs.p.Index()}
}
func (fs *filePieceImpl) Completion() storage.Completion {
c, err := fs.completion.Get(fs.pieceKey())
if err != nil {
log.Printf("error getting piece completion: %s", err)
c.Ok = false
return c
}
verified := true
if c.Complete {
// If it's allegedly complete, check that its constituent files have the necessary length.
for _, fi := range extentCompleteRequiredLengths(fs.p.Info, fs.p.Offset(), fs.p.Length()) {
s, err := os.Stat(fs.files[fi.fileIndex].path)
if err != nil || s.Size() < fi.length {
verified = false
break
}
}
}
if !verified {
// The completion was wrong, fix it.
c.Complete = false
fs.completion.Set(fs.pieceKey(), false)
}
return c
}
func (fs *filePieceImpl) MarkComplete() error {
return fs.completion.Set(fs.pieceKey(), true)
}
func (fs *filePieceImpl) MarkNotComplete() error {
return fs.completion.Set(fs.pieceKey(), false)
}
+68 -54
View File
@@ -4,10 +4,10 @@ import (
"bytes"
"context"
"fmt"
"github.com/monkeyWie/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed/pkg/base"
fhttp "github.com/monkeyWie/gopeed/pkg/protocol/http"
"github.com/monkeyWie/gopeed/pkg/util"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
"golang.org/x/sync/errgroup"
"io"
"mime"
@@ -16,10 +16,7 @@ import (
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
)
type RequestError struct {
@@ -51,10 +48,10 @@ func newChunk(begin int64, end int64) *chunk {
}
type Fetcher struct {
*fetcher.DefaultFetcher
ctl *controller.Controller
doneCh chan error
res *base.Resource
opts *base.Options
meta *fetcher.FetcherMeta
status base.Status
chunks []*chunk
@@ -68,25 +65,35 @@ func (f *Fetcher) Name() string {
return "http"
}
func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
func (f *Fetcher) Setup(ctl *controller.Controller) (err error) {
f.ctl = ctl
f.doneCh = make(chan error, 1)
f.pauseCh = make(chan interface{}, 1)
if f.meta == nil {
f.meta = &fetcher.FetcherMeta{}
}
return
}
func (f *Fetcher) Resolve(req *base.Request) error {
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
return nil, err
return err
}
httpReq, err := buildRequest(nil, req)
if err != nil {
return nil, err
return err
}
client := buildClient()
// 只访问一个字节,测试资源是否支持Range请求
// send Range request to check whether the server supports breakpoint continuation
// just test one byte, Range: bytes=0-0
httpReq.Header.Set(base.HttpHeaderRange, fmt.Sprintf(base.HttpHeaderRangeFormat, 0, 0))
httpResp, err := client.Do(httpReq)
if err != nil {
return nil, err
return err
}
// 拿到响应头就关闭,不用加defer
// close response body immediately
httpResp.Body.Close()
res := &base.Resource{
Req: req,
Range: false,
Files: []*base.FileInfo{},
}
@@ -98,7 +105,7 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
if contentTotal != "" {
parse, err := strconv.ParseInt(contentTotal, 10, 64)
if err != nil {
return nil, err
return err
}
res.Size = parse
}
@@ -108,12 +115,12 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
if contentLength != "" {
parse, err := strconv.ParseInt(contentLength, 10, 64)
if err != nil {
return nil, err
return err
}
res.Size = parse
}
} else {
return nil, NewRequestError(httpResp.StatusCode, httpResp.Status)
return NewRequestError(httpResp.StatusCode, httpResp.Status)
}
file := &base.FileInfo{
Size: res.Size,
@@ -121,33 +128,34 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition)
if contentDisposition != "" {
_, params, _ := mime.ParseMediaType(contentDisposition)
filename := params["filePath"]
filename := params["filename"]
if filename != "" {
file.Name = filename
}
}
// Get file filePath by URL
if file.Name == "" && strings.Count(req.URL, "/") > 2 {
file.Name = filepath.Base(req.URL)
if file.Name == "" {
file.Name = path.Base(httpReq.URL.Path)
}
// unknown file filePath
if file.Name == "" {
if file.Name == "" || file.Name == "/" || file.Name == "." {
file.Name = "unknown"
}
res.Files = append(res.Files, file)
res.Name = file.Name
return res, nil
f.meta.Req = req
f.meta.Res = res
return nil
}
func (f *Fetcher) Create(res *base.Resource, opts *base.Options) error {
f.res = res
f.opts = opts
func (f *Fetcher) Create(opts *base.Options) error {
f.meta.Opts = opts
f.status = base.DownloadStatusReady
if err := base.ParseReqExtra[fhttp.ReqExtra](res.Req); err != nil {
if err := base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil {
return err
}
if err := base.ParseOptsExtra[fhttp.OptsExtra](opts); err != nil {
if err := base.ParseOptsExtra[fhttp.OptsExtra](f.meta.Opts); err != nil {
return err
}
if opts.Extra == nil {
@@ -156,7 +164,7 @@ func (f *Fetcher) Create(res *base.Resource, opts *base.Options) error {
extra := opts.Extra.(*fhttp.OptsExtra)
if extra.Connections == 0 {
var cfg config
exist, err := f.Ctl.GetConfig(&cfg)
exist, err := f.ctl.GetConfig(&cfg)
if err != nil {
return err
}
@@ -172,7 +180,7 @@ func (f *Fetcher) Create(res *base.Resource, opts *base.Options) error {
func (f *Fetcher) Start() (err error) {
// 创建文件
name := f.filepath()
f.file, err = f.Ctl.Touch(name, f.res.Size)
f.file, err = f.ctl.Touch(name, f.meta.Res.Size)
if err != nil {
return err
}
@@ -215,6 +223,10 @@ func (f *Fetcher) Close() (err error) {
return
}
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
return f.meta
}
func (f *Fetcher) Progress() fetcher.Progress {
p := make(fetcher.Progress, 0)
if len(f.chunks) > 0 {
@@ -227,14 +239,18 @@ func (f *Fetcher) Progress() fetcher.Progress {
return p
}
func (f *Fetcher) Wait() (err error) {
return <-f.doneCh
}
func (f *Fetcher) filepath() string {
return util.Filepath(f.opts.Path, f.res.Files[0].Name, f.opts.Name)
return f.meta.Filepath(f.meta.Res.Files[0])
}
func (f *Fetcher) fetch() {
f.ctx, f.cancel = context.WithCancel(context.Background())
eg, _ := errgroup.WithContext(f.ctx)
for i := 0; i < f.opts.Extra.(*fhttp.OptsExtra).Connections; i++ {
for i := 0; i < len(f.chunks); i++ {
i := i
eg.Go(func() error {
return f.fetchChunk(i)
@@ -253,7 +269,7 @@ func (f *Fetcher) fetch() {
} else {
f.status = base.DownloadStatusDone
}
f.DoneCh <- err
f.doneCh <- err
}
}()
}
@@ -261,7 +277,7 @@ func (f *Fetcher) fetch() {
func (f *Fetcher) fetchChunk(index int) (err error) {
chunk := f.chunks[index]
httpReq, err := buildRequest(f.ctx, f.res.Req)
httpReq, err := buildRequest(f.ctx, f.meta.Req)
if err != nil {
return err
}
@@ -283,7 +299,7 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
resp *http.Response
retry bool
)
if f.res.Range {
if f.meta.Res.Range {
httpReq.Header.Set(base.HttpHeaderRange,
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
} else {
@@ -337,7 +353,7 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
return
}
isComplete := f.res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1
isComplete := f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1
if f.status == base.DownloadStatusPause && !isComplete {
chunk.Status = base.DownloadStatusPause
return
@@ -348,10 +364,10 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
}
func (f *Fetcher) splitChunk() (chunks []*chunk) {
if f.res.Range {
connections := f.opts.Extra.(*fhttp.OptsExtra).Connections
if f.meta.Res.Range {
connections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
// 每个连接平均需要下载的分块大小
chunkSize := f.res.Size / int64(connections)
chunkSize := f.meta.Res.Size / int64(connections)
chunks = make([]*chunk, connections)
for i := 0; i < connections; i++ {
var (
@@ -360,7 +376,7 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
)
if i == connections-1 {
// 最后一个分块需要保证把文件下载完
end = f.res.Size - 1
end = f.meta.Res.Size - 1
} else {
end = begin + chunkSize - 1
}
@@ -379,8 +395,7 @@ func buildClient() *http.Client {
// Cookie handle
jar, _ := cookiejar.New(nil)
return &http.Client{
Jar: jar,
Timeout: time.Second * 60,
Jar: jar,
}
}
@@ -413,6 +428,9 @@ func buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request
body = bytes.NewBufferString(extra.Body)
}
}
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
headers[base.HttpHeaderUserAgent] = []string{base.AgentName}
}
if ctx != nil {
httpReq, err = http.NewRequestWithContext(ctx, method, url.String(), body)
@@ -440,10 +458,7 @@ func (fb *FetcherBuilder) Schemes() []string {
}
func (fb *FetcherBuilder) Build() fetcher.Fetcher {
return &Fetcher{
DefaultFetcher: new(fetcher.DefaultFetcher),
pauseCh: make(chan interface{}),
}
return &Fetcher{}
}
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
@@ -453,16 +468,15 @@ func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
}, nil
}
func (fb *FetcherBuilder) Restore() (v any, f func(res *base.Resource, opts *base.Options, v any) fetcher.Fetcher) {
return &fetcherData{}, func(res *base.Resource, opts *base.Options, v any) fetcher.Fetcher {
func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
fd := v.(*fetcherData)
fb := &FetcherBuilder{}
fetcher := fb.Build().(*Fetcher)
fetcher.res = res
fetcher.opts = opts
fetcher.meta = meta
fetcher.status = base.DownloadStatusPause
base.ParseReqExtra[fhttp.ReqExtra](res.Req)
base.ParseOptsExtra[fhttp.OptsExtra](opts)
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
if len(fd.Chunks) == 0 {
fetcher.chunks = fetcher.splitChunk()
} else {
+39 -29
View File
@@ -3,11 +3,11 @@ package http
import (
"encoding/json"
"fmt"
"github.com/monkeyWie/gopeed/internal/controller"
"github.com/monkeyWie/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed/internal/test"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"net"
"reflect"
"testing"
@@ -15,7 +15,7 @@ import (
)
func TestFetcher_Resolve(t *testing.T) {
testResolve(test.StartTestFileServer, &base.Resource{
testResolve(test.StartTestFileServer, test.BuildName, &base.Resource{
Name: test.BuildName,
Size: test.BuildSize,
Range: true,
@@ -26,7 +26,18 @@ func TestFetcher_Resolve(t *testing.T) {
},
},
}, t)
testResolve(test.StartTestChunkedServer, &base.Resource{
testResolve(test.StartTestCustomServer, "disposition", &base.Resource{
Name: test.BuildName,
Size: test.BuildSize,
Range: false,
Files: []*base.FileInfo{
{
Name: test.BuildName,
Size: test.BuildSize,
},
},
}, t)
testResolve(test.StartTestCustomServer, test.BuildName, &base.Resource{
Name: test.BuildName,
Size: 0,
Range: false,
@@ -39,19 +50,18 @@ func TestFetcher_Resolve(t *testing.T) {
}, t)
}
func testResolve(startTestServer func() net.Listener, want *base.Resource, t *testing.T) {
func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) {
listener := startTestServer()
defer listener.Close()
fetcher := buildFetcher()
res, err := fetcher.Resolve(&base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
err := fetcher.Resolve(&base.Request{
URL: "http://" + listener.Addr().String() + "/" + path,
})
if err != nil {
t.Fatal(err)
}
res.Req = nil
if !reflect.DeepEqual(want, res) {
t.Errorf("Resolve() got = %v, want %v", res, want)
if !reflect.DeepEqual(want, fetcher.meta.Res) {
t.Errorf("Resolve() got = %v, want %v", fetcher.meta.Res, want)
}
}
@@ -76,10 +86,11 @@ func TestFetcher_DownloadContinue(t *testing.T) {
}
func TestFetcher_DownloadChunked(t *testing.T) {
listener := test.StartTestChunkedServer()
listener := test.StartTestCustomServer()
defer listener.Close()
// chunked编码下载
downloadNormal(listener, 1, t)
downloadNormal(listener, 2, t)
}
func TestFetcher_DownloadPost(t *testing.T) {
@@ -114,7 +125,7 @@ func TestFetcher_DownloadResume(t *testing.T) {
}
func TestFetcher_Config(t *testing.T) {
fetcher, _, _ := doDownloadReady(buildConfigFetcher(), test.StartTestFileServer(), 0, t)
fetcher := doDownloadReady(buildConfigFetcher(), test.StartTestFileServer(), 0, t)
err := fetcher.Start()
if err != nil {
t.Fatal(err)
@@ -130,13 +141,12 @@ func TestFetcher_Config(t *testing.T) {
}
}
func downloadReady(listener net.Listener, connections int, t *testing.T) (fetcher fetcher.Fetcher, res *base.Resource, opts *base.Options) {
func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
return doDownloadReady(buildFetcher(), listener, connections, t)
}
func doDownloadReady(f fetcher.Fetcher, listener net.Listener, connections int, t *testing.T) (fetcher fetcher.Fetcher, res *base.Resource, opts *base.Options) {
fetcher = f
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,
})
if err != nil {
@@ -148,20 +158,20 @@ func doDownloadReady(f fetcher.Fetcher, listener net.Listener, connections int,
Connections: connections,
}
}
opts = &base.Options{
opts := &base.Options{
Name: test.DownloadName,
Path: test.Dir,
Extra: extra,
}
err = fetcher.Create(res, opts)
err = f.Create(opts)
if err != nil {
t.Fatal(err)
}
return
return f
}
func downloadNormal(listener net.Listener, connections int, t *testing.T) {
fetcher, _, _ := downloadReady(listener, connections, t)
fetcher := downloadReady(listener, connections, t)
err := fetcher.Start()
if err != nil {
t.Fatal(err)
@@ -178,8 +188,8 @@ func downloadNormal(listener net.Listener, connections int, t *testing.T) {
}
func downloadPost(listener net.Listener, connections int, t *testing.T) {
fetcher, _, _ := downloadReady(listener, connections, t)
fetcher.(*Fetcher).res.Req.Extra = &http.ReqExtra{
fetcher := downloadReady(listener, connections, t)
fetcher.Meta().Req.Extra = &http.ReqExtra{
Method: "POST",
Header: map[string]string{
"Authorization": "Bearer 123456",
@@ -202,7 +212,7 @@ func downloadPost(listener net.Listener, connections int, t *testing.T) {
}
func downloadContinue(listener net.Listener, connections int, t *testing.T) {
fetcher, _, _ := downloadReady(listener, connections, t)
fetcher := downloadReady(listener, connections, t)
err := fetcher.Start()
if err != nil {
t.Fatal(err)
@@ -227,7 +237,7 @@ func downloadContinue(listener net.Listener, connections int, t *testing.T) {
}
func downloadError(listener net.Listener, connections int, t *testing.T) {
fetcher, _, _ := downloadReady(listener, connections, t)
fetcher := downloadReady(listener, connections, t)
err := fetcher.Start()
if err != nil {
t.Fatal(err)
@@ -239,7 +249,7 @@ func downloadError(listener net.Listener, connections int, t *testing.T) {
}
func downloadResume(listener net.Listener, connections int, t *testing.T) {
fetcher, res, opts := downloadReady(listener, connections, t)
fetcher := downloadReady(listener, connections, t)
err := fetcher.Start()
if err != nil {
t.Fatal(err)
@@ -255,7 +265,7 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
fetcher.Pause()
_, f := fb.Restore()
f(res, opts, data)
f(fetcher.Meta(), data)
if err != nil {
t.Fatal(err)
}
+12 -1
View File
@@ -46,7 +46,7 @@ func StartTestSlowFileServer(delay time.Duration) net.Listener {
})
}
func StartTestChunkedServer() net.Listener {
func StartTestCustomServer() net.Listener {
return startTestServer(func() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
@@ -57,6 +57,17 @@ func StartTestChunkedServer() net.Listener {
defer file.Close()
io.Copy(writer, file)
})
mux.HandleFunc("/disposition", func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Disposition", "attachment; filename=\""+BuildName+"\"")
writer.Header().Set("Content-Type", "application/octet-stream")
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
file, err := os.Open(BuildFile)
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(writer, file)
})
return mux
})
}
+7 -1
View File
@@ -3,9 +3,10 @@ package base
type Status string
const (
DownloadStatusReady Status = "ready"
DownloadStatusReady Status = "ready" // task create but not start
DownloadStatusRunning Status = "running"
DownloadStatusPause Status = "pause"
DownloadStatusWait Status = "wait" // task is wait for running
DownloadStatusError Status = "error"
DownloadStatusDone Status = "done"
)
@@ -18,6 +19,11 @@ const (
HttpHeaderContentLength = "Content-Length"
HttpHeaderContentRange = "Content-Range"
HttpHeaderContentDisposition = "Content-Disposition"
HttpHeaderUserAgent = "User-Agent"
HttpHeaderRangeFormat = "bytes=%d-%d"
)
const (
AgentName = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
)
+18 -21
View File
@@ -1,28 +1,25 @@
package base
import "github.com/monkeyWie/gopeed/pkg/util"
import (
"github.com/GopeedLab/gopeed/pkg/util"
)
// 下载请求
// Request download request
type Request struct {
// 下载链接
URL string `json:"url"`
// 附加信息
Extra any `json:"extra"`
URL string `json:"url"`
Extra any `json:"extra"`
}
// 资源信息
// Resource download resource
type Resource struct {
Req *Request `json:"req"`
// 资源名称
Name string `json:"name"`
// 资源总大小
Size int64 `json:"size"`
// 是否支持断点下载
Range bool `json:"range"`
// 资源所包含的文件列表
Size int64 `json:"size"`
// is support range download
Range bool `json:"range"`
RootDir string `json:"rootDir"`
// file list
Files []*FileInfo `json:"files"`
// 资源hash值
Hash string `json:"hash"`
Hash string `json:"hash"`
}
type FileInfo struct {
@@ -31,15 +28,15 @@ type FileInfo struct {
Size int64 `json:"size"`
}
// 下载选项
// Options for download
type Options struct {
// 保存文件名
// Download file name
Name string `json:"name"`
// 保存目录
// Download file path
Path string `json:"path"`
// 选择下载的文件下标列表
// Select file indexes to download
SelectFiles []int `json:"selectFiles"`
// 附加信息
// Extra info for specific fetcher
Extra any `json:"extra"`
}
+320 -109
View File
@@ -2,10 +2,17 @@ package download
import (
"errors"
"github.com/monkeyWie/gopeed/internal/controller"
"github.com/monkeyWie/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/util"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/virtuald/go-paniclog"
"math"
"os"
"path"
"path/filepath"
"sort"
"strings"
"sync"
"sync/atomic"
@@ -13,8 +20,11 @@ import (
)
const (
bucketTask = "task"
bucketSave = "save"
// task info bucket
bucketTask = "task"
// task download data bucket
bucketSave = "save"
// downloader config bucket
bucketConfig = "config"
)
@@ -30,14 +40,16 @@ type Progress struct {
}
type Downloader struct {
cfg *DownloaderConfig
fetchBuilders map[string]fetcher.FetcherBuilder
fetcherMap map[string]fetcher.Fetcher
storage Storage
tasks []*Task
listener Listener
refreshInterval int
lock *sync.Mutex
closed atomic.Bool
lock *sync.Mutex
fetcherMapLock *sync.RWMutex
closed atomic.Bool
}
func NewDownloader(cfg *DownloaderConfig) *Downloader {
@@ -47,16 +59,22 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
cfg.Init()
d := &Downloader{
fetchBuilders: make(map[string]fetcher.FetcherBuilder),
refreshInterval: cfg.RefreshInterval,
storage: cfg.Storage,
lock: &sync.Mutex{},
cfg: cfg,
fetchBuilders: make(map[string]fetcher.FetcherBuilder),
fetcherMap: make(map[string]fetcher.Fetcher),
storage: cfg.Storage,
lock: &sync.Mutex{},
fetcherMapLock: &sync.RWMutex{},
}
for _, f := range cfg.FetchBuilders {
for _, p := range f.Schemes() {
d.fetchBuilders[strings.ToUpper(p)] = f
}
}
logPanic(filepath.Join(cfg.StorageDir, "logs"))
return d
}
@@ -65,6 +83,21 @@ func (d *Downloader) Setup() error {
if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketConfig}); err != nil {
return err
}
// load config from storage
var cfg DownloaderStoreConfig
exist, err := d.storage.Get(bucketConfig, "config", &cfg)
if err != nil {
return err
}
if exist {
d.cfg.DownloaderStoreConfig = &cfg
} else {
d.cfg.DownloaderStoreConfig = &DownloaderStoreConfig{
FirstLoad: true,
}
}
// init default config
d.cfg.DownloaderStoreConfig.Init()
// load tasks from storage
var tasks []*Task
if err := d.storage.List(bucketTask, &tasks); err != nil {
@@ -73,26 +106,35 @@ func (d *Downloader) Setup() error {
if tasks == nil {
tasks = make([]*Task, 0)
} else {
for _, task := range tasks {
for i := len(tasks) - 1; i >= 0; i-- {
task := tasks[i]
// Remove broken tasks
if task.Meta == nil {
tasks = append(tasks[:i], tasks[i+1:]...)
continue
}
initTask(task)
if task.Status != base.DownloadStatusDone && task.Status != base.DownloadStatusError {
task.Status = base.DownloadStatusPause
}
}
// sort by create time
sort.Slice(tasks, func(i, j int) bool {
return tasks[i].CreatedAt.Before(tasks[j].CreatedAt)
})
}
d.tasks = tasks
// 每秒统计一次下载速度
// 每个tick统计一次下载速度
go func() {
for !d.closed.Load() {
if len(d.tasks) > 0 {
for _, task := range d.tasks {
func() {
task.lock.Lock()
defer task.lock.Unlock()
if task.Status == base.DownloadStatusDone ||
task.Status == base.DownloadStatusError ||
task.Status == base.DownloadStatusPause {
if task.Status != base.DownloadStatusRunning {
return
}
// check if task is deleted
@@ -102,9 +144,7 @@ func (d *Downloader) Setup() error {
current := task.fetcher.Progress().TotalDownloaded()
task.Progress.Used = task.timer.Used()
currUsedTime := float64(d.refreshInterval) / 1000
currSpeed := int64(float64(current-task.Progress.Downloaded) / currUsedTime)
task.Progress.Speed = (task.Progress.Speed + currSpeed) / 2
task.Progress.Speed = task.calcSpeed(current-task.Progress.Downloaded, float64(d.cfg.RefreshInterval)/1000)
task.Progress.Downloaded = current
d.emit(EventKeyProgress, task)
@@ -122,24 +162,19 @@ func (d *Downloader) Setup() error {
}()
}
}
time.Sleep(time.Millisecond * time.Duration(d.refreshInterval))
time.Sleep(time.Millisecond * time.Duration(d.cfg.RefreshInterval))
}
}()
return nil
}
func (d *Downloader) buildFetcher(url string) (fetcher.FetcherBuilder, fetcher.Fetcher, error) {
func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) {
schema := util.ParseSchema(url)
fetchBuilder, ok := d.fetchBuilders[schema]
if !ok {
fetchBuilder, ok = d.fetchBuilders[util.FileSchema]
}
if ok {
fetcher := fetchBuilder.Build()
d.setupFetcher(fetcher)
return fetchBuilder, fetcher, nil
return fetchBuilder, nil
}
return nil, nil, errors.New("unsupported protocol")
return nil, errors.New("unsupported protocol")
}
func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
@@ -150,23 +185,58 @@ func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
fetcher.Setup(ctl)
}
func (d *Downloader) Resolve(req *base.Request) (*base.Resource, error) {
_, fetcher, err := d.buildFetcher(req.URL)
func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
fb, err := d.parseFb(req.URL)
if err != nil {
return nil, err
return
}
res, err := fetcher.Resolve(req)
fetcher := fb.Build()
d.setupFetcher(fetcher)
err = fetcher.Resolve(req)
if err != nil {
return nil, err
return
}
return res, nil
rrId, err := gonanoid.New()
if err != nil {
return
}
d.fetcherMapLock.Lock()
d.fetcherMap[rrId] = fetcher
d.fetcherMapLock.Unlock()
rr = &ResolveResult{
ID: rrId,
Res: fetcher.Meta().Res,
}
return
}
func (d *Downloader) Create(res *base.Resource, opts *base.Options) (taskId string, err error) {
func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId string, err error) {
rr, err := d.Resolve(req)
if err != nil {
return
}
return d.Create(rr.ID, opts)
}
func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err error) {
if opts == nil {
opts = &base.Options{}
}
d.fetcherMapLock.RLock()
fetcher, ok := d.fetcherMap[rrId]
d.fetcherMapLock.RUnlock()
if !ok {
return "", errors.New("invalid resource id")
}
defer func() {
d.fetcherMapLock.Lock()
delete(d.fetcherMap, rrId)
d.fetcherMapLock.Unlock()
}()
meta := fetcher.Meta()
meta.Opts = opts
res := meta.Res
if len(opts.SelectFiles) == 0 {
opts.SelectFiles = make([]int, 0)
for i := range res.Files {
@@ -174,60 +244,67 @@ func (d *Downloader) Create(res *base.Resource, opts *base.Options) (taskId stri
}
}
if opts.Path == "" {
exist, storeConfig, err := d.GetConfig()
storeConfig, err := d.GetConfig()
if err != nil {
return "", err
}
if exist {
opts.Path = storeConfig.DownloadDir
}
opts.Path = storeConfig.DownloadDir
}
fetcherBuilder, fetcher, err := d.buildFetcher(res.Req.URL)
fb, err := d.parseFb(fetcher.Meta().Req.URL)
if err != nil {
return
}
err = fetcher.Create(res, opts)
// check if the download file is duplicated and rename it automatically.
files := res.Files
if res.RootDir != "" {
fullDirPath := path.Join(opts.Path, res.RootDir)
newName, err := util.CheckDuplicateAndRename(fullDirPath)
if err != nil {
return "", err
}
res.RootDir = newName
} else if len(files) == 1 {
fullPath := meta.Filepath(files[0])
newName, err := util.CheckDuplicateAndRename(fullPath)
if err != nil {
return "", err
}
opts.Name = newName
}
err = fetcher.Create(opts)
if err != nil {
return
}
task := NewTask()
task.fetcherBuilder = fetcherBuilder
task.fetcherBuilder = fb
task.fetcher = fetcher
task.Res = res
task.Opts = opts
task.Meta = fetcher.Meta()
task.Progress = &Progress{}
task.Status = base.DownloadStatusRunning
task.Status = base.DownloadStatusReady
// calculate select files size
for _, selectIndex := range opts.SelectFiles {
task.Size += res.Files[selectIndex].Size
}
initTask(task)
task.timer.Start()
err = func() error {
d.lock.Lock()
defer d.lock.Unlock()
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
return err
}
d.tasks = append(d.tasks, task)
return nil
return d.storage.Put(bucketTask, task.ID, task.clone())
}()
if err != nil {
return
}
d.emit(EventKeyStart, task)
taskId = task.ID
go func() {
err := fetcher.Start()
if err != nil {
d.emit(EventKeyError, task, err)
return
d.tryRunning(func(remain int) {
if remain > 0 {
err = d.start(task)
}
d.watch(task)
}()
})
return
}
@@ -236,16 +313,10 @@ func (d *Downloader) Pause(id string) (err error) {
if task == nil {
return
}
task.lock.Lock()
defer task.lock.Unlock()
if task.Status == base.DownloadStatusRunning {
task.Status = base.DownloadStatusPause
task.timer.Pause()
task.fetcher.Pause()
d.storage.Put(bucketTask, task.ID, task.clone())
d.emit(EventKeyPause, task)
if err = d.doPause(task, base.DownloadStatusPause); err != nil {
return err
}
return
return d.notifyRunning()
}
func (d *Downloader) Continue(id string) (err error) {
@@ -253,21 +324,57 @@ func (d *Downloader) Continue(id string) (err error) {
if task == nil {
return
}
task.lock.Lock()
defer task.lock.Unlock()
if task.Status == base.DownloadStatusPause || task.Status == base.DownloadStatusError {
task.Status = base.DownloadStatusRunning
err = d.restoreFetcher(task)
d.tryRunning(func(remain int) {
if remain == 0 {
// no more task can be running, need to doPause a running task
for _, t := range d.tasks {
if t.Status == base.DownloadStatusRunning {
err = d.doPause(t, base.DownloadStatusWait)
break
}
}
}
if err != nil {
return
}
task.timer.Start()
err = task.fetcher.Continue()
if err != nil {
})
if err != nil {
return
}
return d.doContinue(task)
}
func (d *Downloader) PauseAll() (err error) {
d.lock.Lock()
defer d.lock.Unlock()
for _, task := range d.tasks {
if task.Status == base.DownloadStatusRunning {
if err = d.doPause(task, base.DownloadStatusPause); err != nil {
return
}
}
}
return
}
func (d *Downloader) ContinueAll() (err error) {
d.lock.Lock()
defer d.lock.Unlock()
runningCount := 0
continueTasks := make([]*Task, 0)
for _, task := range d.tasks {
if task.Status == base.DownloadStatusRunning {
runningCount++
} else if task.Status != base.DownloadStatusDone {
continueTasks = append(continueTasks, task)
}
}
// calculate how many tasks can be continued, can't exceed maxRunning
continueCount := int(math.Min(float64(d.cfg.MaxRunning-runningCount), float64(len(continueTasks))))
for i := 0; i < continueCount; i++ {
if err = d.doContinue(continueTasks[i]); err != nil {
return
}
d.storage.Put(bucketTask, task.ID, task.clone())
d.emit(EventKeyContinue, task)
}
return
}
@@ -302,13 +409,24 @@ func (d *Downloader) Delete(id string, force bool) (err error) {
return
}
if force {
names := make([]string, 0)
for _, file := range task.Res.Files {
names = append(names, util.Filepath(file.Path, file.Name, task.Opts.Name))
if task.Meta.Res.RootDir != "" {
if err = os.RemoveAll(path.Join(task.Meta.Opts.Path, task.Meta.Res.RootDir)); err != nil {
return
}
} else {
for _, file := range task.Meta.Res.Files {
if err = util.SafeRemove(task.Meta.Filepath(file)); err != nil {
return err
}
}
}
util.SafeRemoveAll(task.Opts.Path, names)
}
d.emit(EventKeyDelete, task)
if task.Status == base.DownloadStatusRunning {
if err = d.doNotifyRunning(); err != nil {
return err
}
}
task = nil
return
}
@@ -318,7 +436,7 @@ func (d *Downloader) Close() error {
defer d.lock.Unlock()
d.closed.Store(true)
for _, task := range d.tasks {
d.Pause(task.ID)
d.doPause(task, base.DownloadStatusPause)
}
return d.storage.Close()
}
@@ -364,27 +482,20 @@ func (d *Downloader) GetTasks() []*Task {
return d.tasks
}
func (d *Downloader) GetConfig() (bool, *DownloaderStoreConfig, error) {
var cfg DownloaderStoreConfig
exist, err := d.storage.Get(bucketConfig, "config", &cfg)
if err != nil {
return false, nil, err
}
return exist, &cfg, nil
func (d *Downloader) GetConfig() (*DownloaderStoreConfig, error) {
return d.cfg.DownloaderStoreConfig, nil
}
func (d *Downloader) PutConfig(v *DownloaderStoreConfig) error {
d.cfg.DownloaderStoreConfig = v
return d.storage.Put(bucketConfig, "config", v)
}
func (d *Downloader) getProtocolConfig(name string, v any) (bool, error) {
exist, cfg, err := d.GetConfig()
cfg, err := d.GetConfig()
if err != nil {
return false, err
}
if !exist {
return false, nil
}
if cfg.ProtocolConfig == nil || cfg.ProtocolConfig[name] == nil {
return false, nil
}
@@ -394,6 +505,7 @@ func (d *Downloader) getProtocolConfig(name string, v any) (bool, error) {
return true, nil
}
// wait task done
func (d *Downloader) watch(task *Task) {
err := task.fetcher.Wait()
if err != nil {
@@ -403,7 +515,7 @@ func (d *Downloader) watch(task *Task) {
task.Progress.Used = task.timer.Used()
if task.Size == 0 {
task.Size = task.fetcher.Progress().TotalDownloaded()
task.Res.Size = task.Size
task.Meta.Res.Size = task.Size
}
used := task.Progress.Used / int64(time.Second)
if used == 0 {
@@ -416,42 +528,141 @@ func (d *Downloader) watch(task *Task) {
d.emit(EventKeyDone, task)
}
d.emit(EventKeyFinally, task, err)
if task.Status == EventKeyDone {
d.notifyRunning()
}
}
func (d *Downloader) restoreFetcher(task *Task) error {
if task.fetcher == nil {
fetcherBuilder, _, err := d.buildFetcher(task.Res.Req.URL)
fb, err := d.parseFb(task.Meta.Req.URL)
if err != nil {
return err
}
task.fetcherBuilder = fetcherBuilder
task.fetcherBuilder = fb
err = func() error {
v, f := fetcherBuilder.Restore()
v, f := fb.Restore()
if v != nil {
err := d.storage.Pop(bucketSave, task.ID, v)
if err != nil {
return err
}
task.fetcher = f(task.Res, task.Opts, v)
}
task.fetcher = f(task.Meta, v)
return nil
}()
if err != nil {
// TODO log
}
if task.fetcher == nil {
task.fetcher = fetcherBuilder.Build()
task.fetcher = fb.Build()
}
d.setupFetcher(task.fetcher)
task.fetcher.Create(task.Res, task.Opts)
go d.watch(task)
task.fetcher.Create(task.Meta.Opts)
}
return nil
}
func (d *Downloader) tryRunning(cb func(remain int)) {
d.lock.Lock()
defer d.lock.Unlock()
runningCount := 0
for _, task := range d.tasks {
if task.Status == base.DownloadStatusRunning {
runningCount++
}
}
cb(d.cfg.MaxRunning - runningCount)
}
func (d *Downloader) notifyRunning() error {
d.lock.Lock()
defer d.lock.Unlock()
return d.doNotifyRunning()
}
func (d *Downloader) doNotifyRunning() error {
for _, task := range d.tasks {
if task.Status == base.DownloadStatusReady || task.Status == base.DownloadStatusWait {
return d.doContinue(task)
}
}
return nil
}
func (d *Downloader) start(task *Task) error {
var event EventKey
if task.Status == base.DownloadStatusReady {
event = EventKeyStart
} else {
event = EventKeyContinue
}
task.Status = base.DownloadStatusRunning
task.Progress.Speed = 0
task.timer.Start()
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
return err
}
var err error
if event == EventKeyStart {
err = task.fetcher.Start()
}
if event == EventKeyContinue {
err = task.fetcher.Continue()
}
if err != nil {
return err
}
go d.watch(task)
d.emit(event, task)
return nil
}
func (d *Downloader) doPause(task *Task, status base.Status) (err error) {
task.lock.Lock()
defer task.lock.Unlock()
if task.Status == base.DownloadStatusRunning {
task.Status = status
task.timer.Pause()
task.fetcher.Pause()
d.storage.Put(bucketTask, task.ID, task.clone())
d.emit(EventKeyPause, task)
}
return
}
func (d *Downloader) doContinue(task *Task) (err error) {
task.lock.Lock()
defer task.lock.Unlock()
if task.Status != base.DownloadStatusRunning && task.Status != base.DownloadStatusDone {
err = d.restoreFetcher(task)
if err != nil {
return
}
err = d.start(task)
}
return
}
// redirect stderr to log file, when panic happened log it
func logPanic(logDir string) {
if err := util.CreateDirIfNotExist(logDir); err != nil {
return
}
f, err := os.Create(filepath.Join(logDir, "crash.log"))
if err != nil {
return
}
paniclog.RedirectStderr(f)
}
func initTask(task *Task) {
task.timer = &util.Timer{}
task.timer = util.NewTimer(task.Progress.Used)
task.lock = &sync.Mutex{}
task.speedArr = make([]int64, 0)
}
var defaultDownloader = NewDownloader(nil)
@@ -472,7 +683,7 @@ func (b *boot) Extra(extra interface{}) *boot {
return b
}
func (b *boot) Resolve() (*base.Resource, error) {
func (b *boot) Resolve() (*ResolveResult, error) {
return defaultDownloader.Resolve(&base.Request{
URL: b.url,
Extra: b.extra,
@@ -485,12 +696,12 @@ func (b *boot) Listener(listener Listener) *boot {
}
func (b *boot) Create(opts *base.Options) (string, error) {
res, err := b.Resolve()
rr, err := b.Resolve()
if err != nil {
return "", err
}
defaultDownloader.Listener(b.listener)
return defaultDownloader.Create(res, opts)
return defaultDownloader.Create(rr.ID, opts)
}
func Boot() *boot {
+13 -18
View File
@@ -1,9 +1,9 @@
package download
import (
"github.com/monkeyWie/gopeed/internal/test"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"reflect"
"sync"
"testing"
@@ -22,13 +22,12 @@ func TestDownloader_Resolve(t *testing.T) {
req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
}
res, err := downloader.Resolve(req)
rr, err := downloader.Resolve(req)
if err != nil {
t.Fatal(err)
}
want := &base.Resource{
Name: test.BuildName,
Req: req,
Size: test.BuildSize,
Range: true,
Files: []*base.FileInfo{
@@ -39,8 +38,8 @@ func TestDownloader_Resolve(t *testing.T) {
},
},
}
if !reflect.DeepEqual(want, res) {
t.Errorf("Resolve() got = %v, want %v", res, want)
if !reflect.DeepEqual(want, rr.Res) {
t.Errorf("Resolve() got = %v, want %v", rr.Res, want)
}
}
@@ -56,7 +55,7 @@ func TestDownloader_Create(t *testing.T) {
req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
}
res, err := downloader.Resolve(req)
rr, err := downloader.Resolve(req)
if err != nil {
t.Fatal(err)
}
@@ -68,7 +67,7 @@ func TestDownloader_Create(t *testing.T) {
wg.Done()
}
})
_, err = downloader.Create(res, &base.Options{
_, err = downloader.Create(rr.ID, &base.Options{
Path: test.Dir,
Name: test.DownloadName,
Extra: http.OptsExtra{
@@ -100,12 +99,12 @@ func TestDownloader_StoreAndRestore(t *testing.T) {
req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
}
res, err := downloader.Resolve(req)
rr, err := downloader.Resolve(req)
if err != nil {
t.Fatal(err)
}
id, err := downloader.Create(res, &base.Options{
id, err := downloader.Create(rr.ID, &base.Options{
Path: test.Dir,
Name: test.DownloadName,
Extra: http.OptsExtra{
@@ -171,8 +170,7 @@ func TestDownloader_Protocol_Config(t *testing.T) {
}
storeCfg := &DownloaderStoreConfig{
RefreshInterval: 500,
DownloadDir: "./downloads",
DownloadDir: "./downloads",
ProtocolConfig: map[string]any{
"http": map[string]any{
"connections": 4,
@@ -192,17 +190,14 @@ func TestDownloader_Protocol_Config(t *testing.T) {
},
}
if err := downloader.PutConfig(storeCfg); err != nil {
if err = downloader.PutConfig(storeCfg); err != nil {
t.Fatal(err)
}
exits, newStoreCfg, err := downloader.GetConfig()
newStoreCfg, err := downloader.GetConfig()
if err != nil {
t.Fatal(err)
}
if !exits {
t.Errorf("GetConfig() got = %v, want %v", exits, true)
}
if !test.JsonEqual(storeCfg, newStoreCfg) {
t.Errorf("GetConfig() got = %v, want %v", test.ToJson(storeCfg), test.ToJson(newStoreCfg))
+84 -17
View File
@@ -1,16 +1,79 @@
package download
import (
"github.com/monkeyWie/gopeed/internal/controller"
"github.com/monkeyWie/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed/internal/protocol/bt"
"github.com/monkeyWie/gopeed/internal/protocol/http"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/protocol/bt"
"github.com/GopeedLab/gopeed/internal/protocol/http"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2"
"sync"
"time"
)
type ResolveResult struct {
ID string `json:"id"`
Res *base.Resource `json:"res"`
}
type Task struct {
ID string `json:"id"`
Meta *fetcher.FetcherMeta `json:"meta"`
Status base.Status `json:"status"`
Progress *Progress `json:"progress"`
Size int64 `json:"size"`
CreatedAt time.Time `json:"createdAt"`
fetcherBuilder fetcher.FetcherBuilder
fetcher fetcher.Fetcher
timer *util.Timer
lock *sync.Mutex
speedArr []int64
}
func NewTask() *Task {
id, err := gonanoid.New()
if err != nil {
panic(err)
}
return &Task{
ID: id,
Status: base.DownloadStatusReady,
CreatedAt: time.Now(),
}
}
func (t *Task) clone() *Task {
return &Task{
ID: t.ID,
Meta: t.Meta,
Status: t.Status,
Progress: t.Progress,
Size: t.Size,
CreatedAt: t.CreatedAt,
}
}
func (t *Task) calcSpeed(downloaded int64, usedTime float64) int64 {
t.speedArr = append(t.speedArr, downloaded)
if len(t.speedArr) > 6 {
t.speedArr = t.speedArr[1:]
}
var total int64
for _, v := range t.speedArr {
total += v
}
return int64(float64(total) / float64(len(t.speedArr)) / usedTime)
}
type DownloaderConfig struct {
Controller *controller.Controller
FetchBuilders []fetcher.FetcherBuilder
Storage Storage
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
Storage Storage
StorageDir string
*DownloaderStoreConfig
}
@@ -25,24 +88,28 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig {
new(bt.FetcherBuilder),
}
}
if cfg.Storage == nil {
cfg.Storage = NewMemStorage()
}
if cfg.DownloaderStoreConfig == nil {
cfg.DownloaderStoreConfig = &DownloaderStoreConfig{}
}
if cfg.RefreshInterval == 0 {
cfg.RefreshInterval = 350
}
if cfg.Storage == nil {
cfg.Storage = NewMemStorage()
}
return cfg
}
// DownloaderStoreConfig is the config that can restore the downloader.
type DownloaderStoreConfig struct {
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
DownloadDir string `json:"downloadDir"` // DownloadDir is the directory to save the downloaded files
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
Extra map[string]any `json:"extra"` // Extra is the extra config
FirstLoad bool `json:"-"` // fromNoStore is the flag that the config is first time init and not from store
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
Extra map[string]any `json:"extra"` // Extra is the extra config
}
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
if cfg.MaxRunning == 0 {
cfg.MaxRunning = 3
}
return cfg
}
-49
View File
@@ -1,49 +0,0 @@
package download
import (
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/monkeyWie/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/util"
"sync"
"time"
)
type Task struct {
ID string `json:"id"`
Res *base.Resource `json:"res"`
Opts *base.Options `json:"opts"`
Status base.Status `json:"status"`
Progress *Progress `json:"progress"`
Size int64 `json:"size"`
CreatedAt time.Time `json:"createdAt"`
fetcherBuilder fetcher.FetcherBuilder
fetcher fetcher.Fetcher
timer *util.Timer
lock *sync.Mutex
}
func NewTask() *Task {
id, err := gonanoid.New()
if err != nil {
panic(err)
}
return &Task{
ID: id,
Status: base.DownloadStatusReady,
CreatedAt: time.Now(),
}
}
func (t *Task) clone() *Task {
return &Task{
ID: t.ID,
Res: t.Res,
Opts: t.Opts,
Status: t.Status,
Progress: t.Progress,
Size: t.Size,
CreatedAt: t.CreatedAt,
}
}
+5
View File
@@ -0,0 +1,5 @@
package bt
type ReqExtra struct {
Trackers []string `json:"trackers"`
}
+107 -50
View File
@@ -1,12 +1,10 @@
package rest
import (
"compress/gzip"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/gorilla/mux"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/download"
"github.com/monkeyWie/gopeed/pkg/rest/model"
"github.com/monkeyWie/gopeed/pkg/rest/util"
"io"
"net/http"
"net/url"
@@ -15,75 +13,123 @@ import (
func Resolve(w http.ResponseWriter, r *http.Request) {
var req base.Request
if util.ReadJson(w, r, &req) {
resource, err := Downloader.Resolve(&req)
if ReadJson(r, w, &req) {
rr, err := Downloader.Resolve(&req)
if err != nil {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error()))
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
util.WriteJsonOk(w, model.NewResultWithData(resource))
WriteJson(w, model.NewOkResult(rr))
}
}
func CreateTask(w http.ResponseWriter, r *http.Request) {
var req model.CreateTask
if util.ReadJson(w, r, &req) {
taskId, err := Downloader.Create(req.Res, req.Opts)
if err != nil {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error()))
if ReadJson(r, w, &req) {
var (
taskId string
err error
)
if req.Rid != "" {
taskId, err = Downloader.Create(req.Rid, req.Opts)
} else if req.Req != nil {
taskId, err = Downloader.CreateDirect(req.Req, req.Opts)
} else {
WriteJson(w, model.NewErrorResult("param invalid: rid or req", model.CodeInvalidParam))
return
}
util.WriteJsonOk(w, model.NewResultWithData(taskId))
if err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewOkResult(taskId))
}
}
func PauseTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
if err := Downloader.Pause(taskId); err != nil {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error()))
if taskId == "" {
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
return
}
util.WriteJsonOk(w, nil)
if err := Downloader.Pause(taskId); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func ContinueTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
if err := Downloader.Continue(taskId); err != nil {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error()))
if taskId == "" {
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
return
}
util.WriteJsonOk(w, nil)
if err := Downloader.Continue(taskId); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func PauseAllTask(w http.ResponseWriter, r *http.Request) {
if err := Downloader.PauseAll(); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func ContinueAllTask(w http.ResponseWriter, r *http.Request) {
if err := Downloader.ContinueAll(); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func DeleteTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
force := r.FormValue("force")
if err := Downloader.Delete(taskId, force == "true"); err != nil {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error()))
if taskId == "" {
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
return
}
util.WriteJsonOk(w, nil)
if err := Downloader.Delete(taskId, force == "true"); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func GetTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
task := Downloader.GetTask(taskId)
if task == nil {
util.WriteJson(w, http.StatusNotFound, model.NewResultWithMsg("task not found"))
if taskId == "" {
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
return
}
util.WriteJsonOk(w, model.NewResultWithData(task))
task := Downloader.GetTask(taskId)
if task == nil {
WriteJson(w, model.NewErrorResult("task not found", model.CodeTaskNotFound))
return
}
WriteJson(w, model.NewOkResult(task))
}
func GetTasks(w http.ResponseWriter, r *http.Request) {
status := r.FormValue("status")
if status == "" {
util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg("param is required: status"))
return
status = strings.Join([]string{
string(base.DownloadStatusReady),
string(base.DownloadStatusRunning),
string(base.DownloadStatusPause),
string(base.DownloadStatusError),
string(base.DownloadStatusDone),
}, ",")
}
statusArr := strings.Split(status, ",")
tasks := Downloader.GetTasks()
@@ -95,33 +141,33 @@ func GetTasks(w http.ResponseWriter, r *http.Request) {
}
}
}
util.WriteJsonOk(w, model.NewResultWithData(result))
WriteJson(w, model.NewOkResult(result))
}
func GetConfig(w http.ResponseWriter, r *http.Request) {
util.WriteJsonOk(w, model.NewResultWithData(getServerConfig()))
WriteJson(w, model.NewOkResult(getServerConfig()))
}
func PutConfig(w http.ResponseWriter, r *http.Request) {
var cfg download.DownloaderStoreConfig
if util.ReadJson(w, r, &cfg) {
if ReadJson(r, w, &cfg) {
if err := Downloader.PutConfig(&cfg); err != nil {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error()))
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
}
util.WriteJsonOk(w, nil)
WriteJson(w, model.NewNilResult())
}
func DoProxy(w http.ResponseWriter, r *http.Request) {
target := r.Header.Get("X-Target-Uri")
if target == "" {
util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg("header is required: X-Target-Uri"))
writeError(w, "param invalid: X-Target-Uri")
return
}
targetUrl, err := url.Parse(target)
if err != nil {
util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg(err.Error()))
writeError(w, err.Error())
return
}
r.RequestURI = ""
@@ -129,31 +175,42 @@ func DoProxy(w http.ResponseWriter, r *http.Request) {
r.Host = targetUrl.Host
resp, err := http.DefaultClient.Do(r)
if err != nil {
util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg(err.Error()))
writeError(w, err.Error())
return
}
defer resp.Body.Close()
w.WriteHeader(resp.StatusCode)
for k, vv := range resp.Header {
for _, v := range vv {
w.Header().Add(k, v)
w.Header().Set(k, v)
}
}
var reader io.ReadCloser
switch resp.Header.Get("Content-Encoding") {
case "gzip":
reader, err = gzip.NewReader(resp.Body)
defer reader.Close()
default:
reader = resp.Body
}
if _, err := io.Copy(w, reader); err != nil {
util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg(err.Error()))
w.WriteHeader(resp.StatusCode)
buf, err := io.ReadAll(resp.Body)
if err != nil {
writeError(w, err.Error())
return
}
w.Write(buf)
//var reader io.ReadCloser
//switch resp.Header.Get("Content-Encoding") {
//case "gzip":
// reader, err = gzip.NewReader(resp.Body)
// defer reader.Close()
//default:
// reader = resp.Body
//}
//if _, err := io.Copy(w, resp.Body); err != nil {
// writeError(w, err.Error())
// return
//}
}
func writeError(w http.ResponseWriter, msg string) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(msg))
}
func getServerConfig() *download.DownloaderStoreConfig {
_, cfg, _ := Downloader.GetConfig()
cfg, _ := Downloader.GetConfig()
return cfg
}
+34 -8
View File
@@ -1,21 +1,47 @@
package model
type RespCode int
const (
CodeOk RespCode = 0
// CodeError is the common error code
CodeError RespCode = 1000
// CodeUnauthorized is the error code for unauthorized
CodeUnauthorized RespCode = 1001
// CodeInvalidParam is the error code for invalid parameter
CodeInvalidParam RespCode = 1002
// CodeTaskNotFound is the error code for task not found
CodeTaskNotFound RespCode = 2001
)
type Result[T any] struct {
Msg string `json:"msg"`
Data T `json:"data"`
Code RespCode `json:"code"`
Msg string `json:"msg"`
Data T `json:"data"`
}
func NewResult[T any](msg string, data T) *Result[T] {
func NewOkResult[T any](data T) *Result[T] {
return &Result[T]{
Msg: msg,
Code: CodeOk,
Data: data,
}
}
func NewResultWithMsg(msg string) *Result[any] {
return NewResult[any](msg, nil)
func NewNilResult() *Result[any] {
return &Result[any]{
Code: CodeOk,
}
}
func NewResultWithData[T any](data T) *Result[T] {
return NewResult[T]("", data)
func NewErrorResult(msg string, code ...RespCode) *Result[any] {
// if code is not provided, the default code is CodeError
c := CodeError
if len(code) > 0 {
c = code[0]
}
return &Result[any]{
Code: c,
Msg: msg,
}
}
+22 -4
View File
@@ -1,6 +1,9 @@
package model
import "io/fs"
import (
"encoding/base64"
"io/fs"
)
type Storage string
@@ -12,13 +15,14 @@ const (
type StartConfig struct {
Network string `json:"network"`
Address string `json:"address"`
RefreshInterval int `json:"refreshInterval"`
Storage Storage `json:"storage"`
StorageDir string `json:"storageDir"`
RefreshInterval int `json:"refreshInterval"`
ApiToken string `json:"apiToken"`
WebEnable bool
WebFS fs.FS
WebEnable bool
WebFS fs.FS
WebBasicAuth *WebBasicAuth
}
func (cfg *StartConfig) Init() *StartConfig {
@@ -28,6 +32,9 @@ func (cfg *StartConfig) Init() *StartConfig {
if cfg.Address == "" {
cfg.Address = "127.0.0.1:0"
}
if cfg.RefreshInterval == 0 {
cfg.RefreshInterval = 350
}
if cfg.Storage == "" {
cfg.Storage = StorageBolt
}
@@ -36,3 +43,14 @@ func (cfg *StartConfig) Init() *StartConfig {
}
return cfg
}
type WebBasicAuth struct {
Username string
Password string
}
// Authorization returns the value of the Authorization header to be used in HTTP requests.
func (cfg *WebBasicAuth) Authorization() string {
userId := cfg.Username + ":" + cfg.Password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(userId))
}
+4 -3
View File
@@ -1,8 +1,9 @@
package model
import "github.com/monkeyWie/gopeed/pkg/base"
import "github.com/GopeedLab/gopeed/pkg/base"
type CreateTask struct {
Res *base.Resource `json:"res"`
Opts *base.Options `json:"opts"`
Rid string `json:"rid"`
Req *base.Request `json:"req"`
Opts *base.Options `json:"opts"`
}
+39 -10
View File
@@ -2,12 +2,12 @@ package rest
import (
"context"
"encoding/json"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/monkeyWie/gopeed/pkg/download"
"github.com/monkeyWie/gopeed/pkg/rest/model"
restUtil "github.com/monkeyWie/gopeed/pkg/rest/util"
"github.com/monkeyWie/gopeed/pkg/util"
"net"
"net/http"
)
@@ -57,14 +57,16 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
}
startCfg.Init()
downloadCfg := &download.DownloaderConfig{}
downloadCfg := &download.DownloaderConfig{
RefreshInterval: startCfg.RefreshInterval,
}
if startCfg.Storage == model.StorageBolt {
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
} else {
downloadCfg.Storage = download.NewMemStorage()
}
downloadCfg.StorageDir = startCfg.StorageDir
downloadCfg.Init()
downloadCfg.RefreshInterval = startCfg.RefreshInterval
Downloader = download.NewDownloader(downloadCfg)
if err := Downloader.Setup(); err != nil {
return nil, nil, err
@@ -84,6 +86,8 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
r.Methods(http.MethodPost).Path("/api/v1/tasks").HandlerFunc(CreateTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseAllTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/continue").HandlerFunc(ContinueAllTask)
r.Methods(http.MethodDelete).Path("/api/v1/tasks/{id}").HandlerFunc(DeleteTask)
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask)
r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks)
@@ -94,14 +98,21 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
r.PathPrefix("/").Handler(http.FileServer(http.FS(startCfg.WebFS)))
}
if startCfg.ApiToken != "" {
if startCfg.ApiToken != "" || (startCfg.WebEnable && startCfg.WebBasicAuth != nil) {
r.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Api-Token") != startCfg.ApiToken {
restUtil.WriteJson(w, http.StatusUnauthorized, model.NewResultWithMsg("invalid token"))
if startCfg.ApiToken != "" && r.Header.Get("X-Api-Token") == startCfg.ApiToken {
h.ServeHTTP(w, r)
return
}
h.ServeHTTP(w, r)
if startCfg.WebEnable && startCfg.WebBasicAuth != nil {
if r.Header.Get("Authorization") == startCfg.WebBasicAuth.Authorization() {
h.ServeHTTP(w, r)
return
}
w.Header().Set("WWW-Authenticate", "Basic realm=\"gopeed web\"")
}
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
})
})
}
@@ -113,3 +124,21 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
)(r)}
return srv, listener, nil
}
func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool {
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return false
}
return true
}
func WriteJson(w http.ResponseWriter, v any) {
WriteStatusJson(w, http.StatusOK, v)
}
func WriteStatusJson(w http.ResponseWriter, statusCode int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(v)
}
+121 -25
View File
@@ -6,10 +6,10 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/monkeyWie/gopeed/internal/test"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/download"
"github.com/monkeyWie/gopeed/pkg/rest/model"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"io"
"net"
"net/http"
@@ -34,7 +34,6 @@ var (
}
taskRes = &base.Resource{
Name: test.BuildName,
Req: taskReq,
Size: test.BuildSize,
Range: true,
Files: []*base.FileInfo{
@@ -46,7 +45,7 @@ var (
},
}
createReq = &model.CreateTask{
Res: taskRes,
Req: taskReq,
Opts: &base.Options{
Path: test.Dir,
Name: test.DownloadName,
@@ -59,9 +58,9 @@ var (
func TestResolve(t *testing.T) {
doTest(func() {
resp := httpRequestCheckOk[*base.Resource](http.MethodPost, "/api/v1/resolve", taskReq)
if !test.JsonEqual(taskRes, resp) {
t.Errorf("Resolve() got = %v, want %v", test.ToJson(resp), test.ToJson(taskRes))
resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq)
if !test.JsonEqual(taskRes, resp.Res) {
t.Errorf("Resolve() got = %v, want %v", test.ToJson(resp.Res), test.ToJson(taskRes))
}
})
}
@@ -145,22 +144,56 @@ func TestPauseAndContinueTask(t *testing.T) {
})
}
func TestPauseAllAndContinueALLTasks(t *testing.T) {
doTest(func() {
cfg, err := Downloader.GetConfig()
if err != nil {
t.Fatal(err)
}
createAndPause := func() {
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
httpRequestCheckOk[*download.Task](http.MethodPut, "/api/v1/tasks/"+taskId+"/pause", nil)
}
total := cfg.MaxRunning + 2
for i := 0; i < total; i++ {
createAndPause()
}
// continue all
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil)
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusRunning), nil)
if len(tasks) != cfg.MaxRunning {
t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning)
}
// pause all
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/pause", nil)
tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s,%s", base.DownloadStatusReady, base.DownloadStatusPause), nil)
if len(tasks) != total {
t.Errorf("PauseAllTasks() got = %v, want %v", len(tasks), total)
}
})
}
func TestDeleteTask(t *testing.T) {
doTest(func() {
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
time.Sleep(time.Millisecond * 200)
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks/"+taskId, nil)
code, _ := httpRequest[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
checkCode(code, http.StatusNotFound)
checkCode(code, model.CodeTaskNotFound)
})
}
func TestDeleteTaskForce(t *testing.T) {
doTest(func() {
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
time.Sleep(time.Millisecond * 500)
time.Sleep(time.Millisecond * 200)
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks/"+taskId+"?force=true", nil)
code, _ := httpRequest[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
checkCode(code, http.StatusNotFound)
checkCode(code, model.CodeTaskNotFound)
if _, err := os.Stat(test.DownloadFile); !errors.Is(err, os.ErrNotExist) {
t.Errorf("DeleteTaskForce() got = %v, want %v", err, os.ErrNotExist)
}
@@ -179,8 +212,7 @@ func TestGetTasks(t *testing.T) {
httpRequestCheckOk[string](http.MethodPost, fmt.Sprintf("/api/v1/tasks?status=%s,%s",
base.DownloadStatusReady, base.DownloadStatusRunning), createReq)
code, _ := httpRequest[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil)
checkCode(code, http.StatusBadRequest)
httpRequestCheckOk[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil)
wg.Wait()
r := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s",
@@ -219,7 +251,7 @@ func TestGetAndPutConfig(t *testing.T) {
func TestDoProxy(t *testing.T) {
doTest(func() {
code, respBody := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
"X-Target-Uri": "https://github.com/monkeyWie/gopeed/raw/695da7ea87d2b455552b709d3cb4d7879484d4d1/README.md",
"X-Target-Uri": "https://github.com/GopeedLab/gopeed/raw/695da7ea87d2b455552b709d3cb4d7879484d4d1/README.md",
}, nil)
if code != http.StatusOK {
t.Errorf("DoProxy() got = %v, want %v", code, http.StatusOK)
@@ -230,9 +262,18 @@ func TestDoProxy(t *testing.T) {
t.Errorf("DoProxy() got = %v, want %v", got, want)
}
})
doTest(func() {
code, _ := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
"X-Target-Uri": "https://github.com/GopeedLab/gopeed/raw/695da7ea87d2b455552b709d3cb4d7879484d4d1/NOT_FOUND",
}, nil)
if code != http.StatusNotFound {
t.Errorf("DoProxy() got = %v, want %v", code, http.StatusNotFound)
}
})
}
func TestAuthorization(t *testing.T) {
func TestApiToken(t *testing.T) {
var cfg = &model.StartConfig{}
cfg.Init()
cfg.ApiToken = "123456"
@@ -244,16 +285,57 @@ func TestAuthorization(t *testing.T) {
Stop()
}()
code, _ := doHttpRequest[any](http.MethodGet, "/api/v1/config", nil, nil)
checkCode(code, http.StatusUnauthorized)
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestApiToken() got = %v, want %v", status, http.StatusUnauthorized)
}
code, _ = doHttpRequest[any](http.MethodGet, "/api/v1/config", map[string]string{
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"X-Api-Token": cfg.ApiToken,
}, nil)
checkCode(code, http.StatusOK)
if status != http.StatusOK {
t.Errorf("TestApiToken() got = %v, want %v", status, http.StatusOK)
}
}
func TestAuthorization(t *testing.T) {
var cfg = &model.StartConfig{}
cfg.Init()
cfg.ApiToken = "123456"
cfg.WebEnable = true
cfg.WebBasicAuth = &model.WebBasicAuth{
Username: "admin",
Password: "123456",
}
fileListener := doStart(cfg)
defer func() {
if err := fileListener.Close(); err != nil {
panic(err)
}
Stop()
}()
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Authorization": cfg.WebBasicAuth.Authorization(),
}, nil)
if status != http.StatusOK {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"X-Api-Token": cfg.ApiToken,
}, nil)
if status != http.StatusOK {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
}
}
func doTest(handler func()) {
testFunc := func(storage model.Storage) {
var cfg = &model.StartConfig{}
@@ -267,6 +349,16 @@ func doTest(handler func()) {
Stop()
}()
defer Downloader.Clear()
defer func() {
tasks := Downloader.GetTasks()
taskIds := make([]string, len(tasks))
for i, task := range tasks {
taskIds[i] = task.ID
}
for _, id := range taskIds {
Downloader.Delete(id, true)
}
}()
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
handler()
}
@@ -308,17 +400,21 @@ func doHttpRequest0(method string, path string, headers map[string]string, body
if err != nil {
panic(err)
}
return response.StatusCode, respBody
}
func doHttpRequest[T any](method string, path string, headers map[string]string, body any) (int, *model.Result[T]) {
code, respBody := doHttpRequest0(method, path, headers, body)
statusCode, respBody := doHttpRequest0(method, path, headers, body)
if statusCode != http.StatusOK {
panic(fmt.Sprintf("http request failed, status code: %d", statusCode))
}
var r model.Result[T]
if err := json.Unmarshal(respBody, &r); err != nil {
panic(err)
}
return code, &r
return int(r.Code), &r
}
func httpRequest[T any](method string, path string, body any) (int, *model.Result[T]) {
@@ -332,11 +428,11 @@ func httpRequestCheckOk[T any](method string, path string, body any) T {
}
func checkOk(code int) {
checkCode(code, 200)
checkCode(code, model.CodeOk)
}
func checkCode(code int, exceptCode int) {
if code != exceptCode {
func checkCode(code int, exceptCode model.RespCode) {
if code != int(exceptCode) {
panic(fmt.Sprintf("code got = %d, want %d", code, exceptCode))
}
}
-25
View File
@@ -1,25 +0,0 @@
package util
import (
"encoding/json"
"github.com/monkeyWie/gopeed/pkg/rest/model"
"net/http"
)
func ReadJson(w http.ResponseWriter, r *http.Request, v any) bool {
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error()))
return false
}
return true
}
func WriteJsonOk(w http.ResponseWriter, v any) {
WriteJson(w, http.StatusOK, v)
}
func WriteJson(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
json.NewEncoder(w).Encode(v)
}
+64 -45
View File
@@ -2,13 +2,14 @@ package util
import (
"errors"
"io"
"fmt"
"os"
"path/filepath"
syspath "path"
"strings"
)
func Dir(path string) string {
dir := filepath.Dir(path)
dir := syspath.Dir(path)
if dir == "." {
return ""
}
@@ -19,7 +20,7 @@ func Filepath(path string, originName string, customName string) string {
if customName == "" {
customName = originName
}
return filepath.Join(path, customName)
return syspath.Join(path, customName)
}
// SafeRemove remove file safely, ignoring errors if the path does not exist.
@@ -30,52 +31,70 @@ func SafeRemove(name string) error {
return nil
}
// SafeRemoveAll remove file and parent directories safely
func SafeRemoveAll(path string, names []string) error {
for _, name := range names {
err := SafeRemove(filepath.Join(path, name))
if err != nil {
return err
// CheckDuplicateAndRename rename duplicate file, add suffix (1) (2) ...
// if file name is a.txt, rename to a (1).txt
// if directory name is a, rename to a (1)
// return new name
func CheckDuplicateAndRename(path string) (string, error) {
dir := syspath.Dir(path)
name := syspath.Base(path)
// if file not exists, return directly
_, err := os.Stat(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return name, nil
}
if err := safeRemoveParent(path, name); err != nil {
return err
return "", err
}
index := strings.LastIndex(name, ".")
var nameTpl string
if index == -1 {
nameTpl = name + " (%d)"
} else {
nameTpl = name[:index] + " (%d)" + name[index:]
}
for i := 1; ; i++ {
newName := fmt.Sprintf(nameTpl, i)
newPath := syspath.Join(dir, newName)
if _, err := os.Stat(newPath); os.IsNotExist(err) {
return newName, nil
}
}
}
// GetSingleDir get the top level single folder name,if not exist, return empty string
func GetSingleDir(paths []string) string {
if len(paths) == 0 {
return ""
}
split := strings.Split(paths[0], "/")
if len(split) == 0 || split[0] == "" {
return ""
}
dir := split[0]
for i := 1; i < len(paths); i++ {
if !strings.HasPrefix(paths[i], dir) {
return ""
}
}
return dir
}
func CreateDirIfNotExist(dir string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
return os.MkdirAll(dir, 0o777)
}
return nil
}
func safeRemoveParent(path string, subPath string) error {
currPath := filepath.Dir(subPath)
if currPath == "." {
return nil
// IsExistsFile check file exists and is a file
func IsExistsFile(path string) bool {
info, err := os.Stat(path)
// 判断路径是否存在,并且是一个文件
if err == nil && !info.IsDir() {
return true
}
// if directory is empty, remove it
dir := filepath.Join(path, filepath.Dir(subPath))
empty, err := isEmpty(dir)
if err != nil {
return err
}
if empty {
err = SafeRemove(dir)
if err != nil {
return err
}
return safeRemoveParent(path, currPath)
}
return nil
}
// check directory is empty
func isEmpty(name string) (bool, error) {
f, err := os.Open(name)
if err != nil {
return false, err
}
defer f.Close()
_, err = f.Readdirnames(1) // Or f.Readdir(1)
if err == io.EOF {
return true, nil
}
return false, err // Either not empty or error, suits both cases
return false
}
+129 -86
View File
@@ -2,7 +2,6 @@ package util
import (
"os"
"path/filepath"
"testing"
)
@@ -87,10 +86,13 @@ func TestFilepath(t *testing.T) {
func TestSafeRemove(t *testing.T) {
name := "test_safe_remove.data"
_, err := os.Create(name)
file, err := os.Create(name)
if err != nil {
t.Fatal(err)
}
if err := file.Close(); err != nil {
t.Fatal(err)
}
if err := SafeRemove(name); err != nil {
t.Fatal(err)
}
@@ -103,96 +105,137 @@ func TestSafeRemove(t *testing.T) {
}
}
func TestSafeRemoveAll(t *testing.T) {
testDir := "test_dir"
err := os.Mkdir(testDir, 0755)
func TestGetSingleDir(t *testing.T) {
type args struct {
paths []string
}
tests := []struct {
name string
args args
want string
}{
{
name: "empty",
args: args{
paths: []string{},
},
want: "",
},
{
name: "blank",
args: args{
paths: []string{""},
},
want: "",
},
{
name: "blank multi",
args: args{
paths: []string{"", "a"},
},
want: "",
},
{
name: "multi dir not single",
args: args{
paths: []string{"a", "b"},
},
want: "",
},
{
name: "single dir",
args: args{
paths: []string{"a"},
},
want: "a",
},
{
name: "multi dir single",
args: args{
paths: []string{"a/b", "a/c"},
},
want: "a",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetSingleDir(tt.args.paths); got != tt.want {
t.Errorf("IsSingleDir() = %v, want %v", got, tt.want)
}
})
}
}
func TestCheckDuplicateAndRename(t *testing.T) {
doCheckDuplicateAndRename(t, []string{}, "a.txt", "a.txt")
doCheckDuplicateAndRename(t, []string{"a.txt"}, "a.txt", "a (1).txt")
doCheckDuplicateAndRename(t, []string{"a.txt", "a (1).txt"}, "a.txt", "a (2).txt")
doCheckDuplicateAndRename(t, []string{}, "a", "a")
doCheckDuplicateAndRename(t, []string{"a"}, "a", "a (1)")
doCheckDuplicateAndRename(t, []string{"a", "a (1)"}, "a", "a (2)")
}
func doCheckDuplicateAndRename(t *testing.T, exitsPaths []string, path string, except string) {
for _, path := range exitsPaths {
if err := os.MkdirAll(path, 0755); err != nil {
t.Fatal(err)
}
}
defer func() {
for _, path := range exitsPaths {
if err := os.RemoveAll(path); err != nil {
t.Fatal(err)
}
}
}()
got, err := CheckDuplicateAndRename(path)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(testDir)
doSafeRemoveAll(t, testDir, []string{
"1.txt",
}, []string{})
doSafeRemoveAll(t, testDir, []string{
"1.txt",
}, []string{
"2.txt",
}, "2.txt")
doSafeRemoveAll(t, testDir, []string{
"1.txt",
"a/b/c/1.txt",
"a/b/1.txt",
"a/1.txt",
}, []string{})
doSafeRemoveAll(t, testDir, []string{
"1.txt",
"a/b/c/1.txt",
"a/b/1.txt",
"a/1.txt",
}, []string{
"a/b/2.txt",
}, "a/b/2.txt")
doSafeRemoveAll(t, testDir, []string{
"1.txt",
"a/b/c/1.txt",
"a/b/1.txt",
"a/1.txt",
}, []string{
"a/2.txt",
}, "a/2.txt")
if got != except {
t.Errorf("CheckDuplicateAndRename() = %v, want %v", got, except)
}
}
func doSafeRemoveAll(t *testing.T, path string, downloadNames []string, otherNames []string, exist ...string) {
preCreate(t, path, downloadNames)
preCreate(t, path, otherNames)
if err := SafeRemoveAll(path, downloadNames); err != nil {
t.Fatal(err)
func TestIsExistsFile(t *testing.T) {
type args struct {
path string
}
if len(exist) == 0 {
for _, name := range downloadNames {
filePath := filepath.Join(path, name)
if isExist(filePath) {
t.Fatalf("file %s should not exist", filePath)
tests := []struct {
name string
args args
want bool
}{
{
name: "exist",
args: args{
path: "./path.go",
},
want: true,
},
{
name: "not exist",
args: args{
path: "./path_not_exist.go",
},
want: false,
},
{
name: "is dir",
args: args{
path: "../util",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsExistsFile(tt.args.path); got != tt.want {
t.Errorf("IsExistsFile() = %v, want %v", got, tt.want)
}
subDirPath := filepath.Dir(name)
if subDirPath != "." {
dirPath := filepath.Join(path, subDirPath)
if isExist(dirPath) {
t.Fatalf("dir %s should not exist", dirPath)
}
}
}
return
}
for _, name := range exist {
if !isExist(filepath.Join(path, name)) {
t.Fatalf("file %s should exist", name)
}
})
}
}
func preCreate(t *testing.T, path string, names []string) {
for _, name := range names {
fullPath := filepath.Join(path, name)
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
t.Fatal(err)
}
_, err := os.Create(fullPath)
if err != nil {
t.Fatal(err)
}
}
}
func isExist(path string) bool {
_, err := os.Stat(path)
return err == nil || os.IsExist(err)
}
+6 -1
View File
@@ -2,12 +2,17 @@ package util
import "time"
// 计时器
type Timer struct {
t int64
used int64
}
func NewTimer(used int64) *Timer {
return &Timer{
used: used,
}
}
func (t *Timer) Start() {
t.t = time.Now().UnixNano()
}
+29 -3
View File
@@ -1,13 +1,39 @@
package util
import "strings"
import (
"encoding/base64"
"regexp"
"strings"
)
const FileSchema = "FILE"
func ParseSchema(url string) string {
index := strings.Index(url, ":")
if index == -1 {
// if no schema or windows path like C:\a.txt, return FILE
if index == -1 || index == 1 {
return FileSchema
}
return strings.ToUpper(url[:index])
schema := url[:index]
if schema == "data" {
schema, _ = ParseDataUri(url)
}
return strings.ToUpper(schema)
}
// ParseDataUri parses a data URI and returns the MIME type and decode data.
func ParseDataUri(uri string) (string, []byte) {
re := regexp.MustCompile(`^data:(.*);base64,(.*)$`)
matches := re.FindStringSubmatch(uri)
if len(matches) != 3 {
return "", nil
}
mime := matches[1]
base64Data := matches[2]
// 解码Base64数据
data, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return "", nil
}
return mime, data
}
+82 -2
View File
@@ -1,6 +1,10 @@
package util
import "testing"
import (
"encoding/base64"
"reflect"
"testing"
)
func TestParseSchema(t *testing.T) {
type args struct {
@@ -35,7 +39,21 @@ func TestParseSchema(t *testing.T) {
{
name: "file-no-scheme",
args: args{
url: "/home/bt.torrent",
url: "./url.go",
},
want: "FILE",
},
{
name: "data-uri",
args: args{
url: "data:application/x-bittorrent;base64,test",
},
want: "APPLICATION/X-BITTORRENT",
},
{
name: "windows-path",
args: args{
url: "D:\\bt.torrent",
},
want: "FILE",
},
@@ -48,3 +66,65 @@ func TestParseSchema(t *testing.T) {
})
}
}
func TestParseDataUri(t *testing.T) {
type args struct {
uri string
}
type result struct {
mime string
data []byte
}
testData := []byte("test")
testData64 := base64.StdEncoding.EncodeToString(testData)
tests := []struct {
name string
args args
want result
}{
{
name: "success",
args: args{
uri: "data:application/x-bittorrent;base64," + testData64,
},
want: result{
mime: "application/x-bittorrent",
data: testData,
},
},
{
name: "fail-dirty-data",
args: args{
uri: "data::application/x-bittorrent;base64,!@$",
},
want: result{
mime: "",
data: nil,
},
},
{
name: "fail-miss-data",
args: args{
uri: ":application/x-bittorrent;base64," + testData64,
},
want: result{
mime: "",
data: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mime, data := ParseDataUri(tt.args.uri)
got := result{
mime: mime,
data: data,
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseDataUri() = %v, want %v", got, tt.want)
}
})
}
}
+2 -1
View File
@@ -29,7 +29,8 @@
.packages
.pub-cache/
.pub/
/build/
build/
.fvm/
# Web related
+5 -20
View File
@@ -4,7 +4,7 @@
# This file should be version controlled.
version:
revision: f1875d570e39de09040c8f79aa13cc56baab8db1
revision: 7048ed95a5ad3e43d697e0c397464193991fc230
channel: stable
project_type: app
@@ -13,26 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: android
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: ios
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: linux
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: macos
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: web
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
- platform: windows
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
# User provided section
-16
View File
@@ -1,16 +0,0 @@
# gopeed
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
Whitespace-only changes.
@@ -12,9 +12,54 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:launchMode="singleInstance"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="file"
android:host="*"
android:pathPattern=".*torrent" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="file"
android:host="*"
android:mimeType="application/x-bittorrent" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="content"
android:host="*"
android:pathPattern=".*torrent" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="content"
android:host="*"
android:mimeType="application/x-bittorrent" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="magnet" />
</intent-filter>
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
@@ -31,12 +31,8 @@ class MainActivity : FlutterActivity() {
}
}
"stop" -> {
try {
Libgopeed.stop()
result.success(null)
} catch (e: Exception) {
result.error("ERROR", e.message, null)
}
Libgopeed.stop()
result.success(null)
}
else -> {
result.notImplemented()
+2 -2
View File
@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.6.10'
ext.kotlin_version = '1.8.22'
repositories {
google()
mavenCentral()
@@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
Binary file not shown.
+64
View File
@@ -0,0 +1,64 @@
{
"label": "English",
"error": "Error",
"tip": "Tip",
"confirm": "Confirm",
"cancel": "Cancel",
"selectAll": "Select All",
"task": "Task",
"downloading": "downloading",
"downloaded": "downloaded",
"setting": "Setting",
"donate": "Donate",
"exit": "Exit",
"create": "Create Task",
"advancedOptions": "Advanced Options",
"downloadLink": "Download Link",
"downloadLinkValid": "Please enter the download link",
"downloadLinkHit": "Please enter the download link, HTTP/HTTPS/MAGNET supported@append",
"downloadLinkHitDesktop": ", or drag the torrent file here directly",
"download": "Download",
"noFileSelected": "Please select at least one file to continue.",
"noStoragePermission": "Storage permission required",
"selectFile": "Select File",
"rename": "Rename",
"basic": "Basic",
"advanced": "Advanced",
"general": "General",
"downloadDir": "Download Directory",
"downloadDirValid": "Please select the download directory",
"connections": "Connections",
"maxRunning": "Max Running Tasks",
"items": "@count items",
"subscribeTracker": "Subscribe Tracker",
"subscribeFail": "Subscribe failed, please check network or try again later",
"update": "Update",
"updateDaily": "Update daily",
"lastUpdate": "Last update: @time",
"addTracker": "Add Tracker",
"addTrackerHit": "Please enter the tracker server url, one per line",
"ui": "UI",
"theme": "Theme",
"themeSystem": "System",
"themeLight": "Light",
"themeDark": "Dark",
"locale": "Language",
"about": "About",
"homepage": "Homepage",
"version": "Version",
"protocol": "Protocol",
"port": "Port",
"apiToken": "API Token",
"notSet": "NS",
"set": "SET",
"effectAfterRestart": "Effect after restart",
"startAll": "Start All",
"pauseAll": "Pause All",
"deleteTask": "Delete Task",
"deleteTaskTip": "Keep downloaded files",
"delete": "Delete",
"newVersionTitle": "Discover new version @version",
"newVersionUpdate": "Download Now",
"newVersionLater": "Later"
}
+58
View File
@@ -0,0 +1,58 @@
{
"label": "فارسی",
"error": "اررور",
"tip": "Tip",
"confirm": "تایید",
"cancel": "انصراف",
"selectAll": "انتخاب همه",
"task": "کار",
"setting": "تنظیمات",
"donate": "کمک مالی",
"exit": "خروج",
"create": "ایجاد کار",
"downloading": "دانلود کردن",
"downloaded": "دانلود شدن",
"downloadLink": "لینک دانلود",
"downloadLinkValid": "لطفا لینک دانلود را وارد کنید",
"download": "دانلود",
"noFileSelected": "لطفا حداقل یک فایل را جهت دانلود انتخاب کنید.",
"noStoragePermission": "مجوز ذخیره سازی مورد نیاز است",
"selectFile": "انتخاب فایل",
"basic": "پایه",
"advanced": "پیشرفته",
"general": "عمومی",
"downloadDir": "دایرکتوری دانلود",
"downloadDirValid": "لطفا دایرکتوری دانلود را انتخاب کنید",
"connections": "اتصالات",
"items": "@count آیتم ها",
"subscribeTracker": "Subscribe Tracker",
"subscribeFail": "Subscribe failed, please check network or try again later",
"update": "بروزرسانی",
"updateDaily": "بروزرسانی روزانه",
"lastUpdate": "Last update: @time",
"addTracker": "Add Tracker",
"addTrackerHit": "Please enter the tracker server url, one per line",
"ui": "رابط کاربری",
"theme": "پوسته",
"themeSystem": "سیستم",
"themeLight": "روشن",
"themeDark": "تاریک",
"locale": "Language",
"about": "درباره ی",
"homepage": "صفحه اصلی",
"version": "نسخه",
"protocol": "پروتکل",
"port": "پورت",
"apiToken": "API توکن",
"notSet": "NS",
"set": "SET",
"effectAfterRestart": "Effect after restart",
"startAll": "شروع همه",
"pauseAll": "توقف همه",
"deleteTask": "پاک کردن کار",
"deleteTaskTip": "فایل های دانلود شده را نگه دارد",
"delete": "پاک کردن",
"newVersionTitle": "@version عنوان: کشف نسخه جدید",
"newVersionUpdate": "دریافت اکنون",
"newVersionLater": "بعداً"
}
+61
View File
@@ -0,0 +1,61 @@
{
"label": "日本語",
"error": "エラー",
"tip": "ヒント",
"confirm": "確認",
"cancel": "キャンセル",
"selectAll": "すべてを選択",
"task": "タスク",
"downloading": "ダウンロード中",
"downloaded": "ダウンロード済",
"setting": "セッティング",
"donate": "寄付する",
"exit": "終了",
"create": "タスクを作成",
"downloadLink": "ダウンロードリンク",
"downloadLinkValid": "ダウンロードリンクを入力してください",
"downloadLinkHit": "ダウンロード リンクを入力してください、HTTP/HTTPS/MAGNET support@append",
"downloadLinkHitDesktop": "または、torrent ファイルを直接ここにドラッグします",
"download": "ダウンロード",
"noFileSelected": "続行するには少なくとも 1 つのファイルを選択してください。",
"noStoragePermission": "ストレージのパーミッションが必要",
"selectFile": "ファイルを選択",
"basic": "ベーシック",
"advanced": "アドバンスド",
"general": "ジェネラル",
"downloadDir": "ディレクトリのダウンロード",
"downloadDirValid": "ダウンロードディレクトリを選択してください",
"connections": "接続",
"maxRunning": "最大実行タスク",
"items": "@count アイテム",
"subscribeTracker": "トラッカーを購読",
"subscribeFail": "登録に失敗しました。ネットワークを確認するか、後でもう一度お試しください",
"update": "アップデート",
"updateDaily": "毎日アップデート",
"lastUpdate": "最終アップデート: @time",
"addTracker": "トラッカーを追加",
"addTrackerHit": "トラッカーサーバーの URL を 1 行に 1 つずつ入力してください",
"ui": "UI",
"theme": "テーマ",
"themeSystem": "システム",
"themeLight": "ライト",
"themeDark": "ダーク",
"locale": "言語",
"about": "概要",
"homepage": "ホームページ",
"version": "バージョン",
"protocol": "プロトコル",
"port": "ポート",
"apiToken": "API トークン",
"notSet": "未設置",
"set": "設置",
"effectAfterRestart": "再起動後の効果",
"startAll": "すべてを開始",
"pauseAll": "すべてを一時停止",
"deleteTask": "タスクを削除",
"deleteTaskTip": "ダウンロードしたファイルを保持",
"delete": "削除",
"newVersionTitle": "新しいバージョン @version を発見する",
"newVersionUpdate": "今すぐダウンロード",
"newVersionLater": "後で"
}
+61
View File
@@ -0,0 +1,61 @@
{
"label": "Русский(Россия)",
"error": "Ошибка",
"tip": "Подсказка",
"confirm": "Подтвердить",
"cancel": "Отмена",
"selectAll": "Выбрать все",
"task": "Задачи",
"downloading": "Загрузка",
"downloaded": "Загружено",
"setting": "Настройки",
"donate": "Пожертвовать",
"exit": "Выход",
"create": "Создать задачу",
"downloadLink": "Ссылка для скачивание",
"downloadLinkValid": "Пожалуйста, введите ссылку для скачивания",
"downloadLinkHit": "Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET supported@append",
"downloadLinkHitDesktop": ", или перетащите сюда торрент-файл",
"download": "Скачать",
"noFileSelected": "Файл не выбран",
"noStoragePermission": "Требуется доступ к хранилищу",
"selectFile": "Выберать файл",
"basic": "Основные параметры",
"advanced": "Расширенные",
"general": "Общие",
"downloadDir": "Папка загрузки",
"downloadDirValid": "Пожалуйста, выберите папку для загрузки",
"connections": "Соединений",
"maxRunning": "Максимальное количество активных задач",
"items": "@подсчет элементов",
"subscribeTracker": "Подпись трекера",
"subscribeFail": "Подписка не удалась, пожалуйста, проверьте сеть или повторите попытку позже",
"update": "Обновить",
"updateDaily": "Обновлять ежедневно",
"lastUpdate": "Последнее обновление: @время",
"addTracker": "Добавить трекер",
"addTrackerHit": "Пожалуйста, введите url трекера, по одному в строке",
"ui": "UI",
"theme": "Тема",
"themeSystem": "Системная",
"themeLight": "Светлая",
"themeDark": "Темная",
"locale": "Язык",
"about": "О приложении",
"homepage": "Домашняя страница",
"version": "Версия",
"protocol": "Протокол",
"port": "Порт",
"apiToken": "API Токен",
"notSet": "Не установлено",
"set": "Установлено",
"effectAfterRestart": "Эффект после перезагрузки",
"startAll": "Запустить все",
"pauseAll": "Приостановить все",
"deleteTask": "Удалить задачу",
"deleteTaskTip": "Сохранить загруженные файлы",
"delete": "Удалить",
"newVersionTitle": "Обнаружена новая версия @version",
"newVersionUpdate": "Скачать сейчас",
"newVersionLater": "позже"
}
+63
View File
@@ -0,0 +1,63 @@
{
"label": "中文(简体)",
"error": "错误",
"tip": "提示",
"confirm": "确认",
"cancel": "取消",
"selectAll": "全选",
"task": "任务",
"downloading": "下载中",
"downloaded": "已完成",
"setting": "设置",
"donate": "打赏",
"exit": "退出",
"create": "创建任务",
"advancedOptions": "高级选项",
"downloadLink": "下载链接",
"downloadLinkValid": "请输入下载链接",
"downloadLinkHit": "请输入下载链接,支持 HTTP/HTTPS/MAGNET@append",
"downloadLinkHitDesktop": ",也可以直接拖拽种子文件到此处",
"download": "下载",
"noFileSelected": "请至少选择一个文件下载",
"noStoragePermission": "需要开启存储权限",
"selectFile": "选择文件",
"rename": "重命名",
"basic": "基础",
"advanced": "高级",
"general": "通用",
"downloadDir": "下载目录",
"downloadDirValid": "请选择下载目录",
"connections": "连接数",
"maxRunning": "最大下载数",
"items": "@count 项",
"subscribeTracker": "订阅 Tracker",
"subscribeFail": "订阅失败,请检查网络或稍后重试",
"update": "更新",
"updateDaily": "每天自动更新",
"lastUpdate": "上次更新:@time",
"addTracker": "添加 Tracker",
"addTrackerHit": "请输入 tracker 服务器地址,每行一条",
"ui": "界面",
"theme": "主题",
"themeSystem": "跟随系统",
"themeLight": "明亮主题",
"themeDark": "暗黑主题",
"locale": "语言",
"about": "关于",
"homepage": "主页",
"version": "版本",
"protocol": "通讯协议",
"port": "端口",
"apiToken": "接口令牌",
"notSet": "未设置",
"set": "已设置",
"effectAfterRestart": "此配置项将在重启应用后生效",
"startAll": "全部开始",
"pauseAll": "全部暂停",
"deleteTask": "删除任务",
"deleteTaskTip": "保留已下载的文件",
"delete": "删除",
"newVersionTitle": "发现新版本 @version",
"newVersionUpdate": "立即下载",
"newVersionLater": "稍后再说"
}
+63
View File
@@ -0,0 +1,63 @@
{
"label": "中文(正體)",
"error": "錯誤",
"tip": "提示",
"confirm": "確定",
"cancel": "取消",
"selectAll": "全選",
"task": "任務",
"downloading": "下載中",
"downloaded": "已完成",
"setting": "設定",
"donate": "斗內",
"exit": "離開",
"create": "新建任務",
"advancedOptions": "進階選項",
"downloadLink": "下載鏈接",
"downloadLinkValid": "請輸入下載鏈接",
"downloadLinkHit": "請輸入下載鏈接,支援 HTTP/HTTPS/MAGNET@append",
"downloadLinkHitDesktop": ",也可以通過拖拽Torrent文件至此以新增任務",
"download": "下載",
"noFileSelected": "沒有選取的文件",
"noStoragePermission": "沒有存儲權限",
"selectFile": "選擇文件",
"rename": "重命名",
"basic": "基礎",
"advanced": "進階",
"general": "一般",
"downloadDir": "下載目録",
"downloadDirValid": "請選擇下載目録",
"connections": "同時連接數量",
"maxRunning": "最大同時下載數",
"items": "@count 項",
"subscribeTracker": "訂閲 Tracker",
"subscribeFail": "訂閲失敗",
"update": "更新",
"updateDaily": "每日自動更新",
"lastUpdate": "上次更新:@time",
"addTracker": "新增 Tracker",
"addTrackerHit": "請輸入 Tracker 服務器地址,每行一條",
"ui": "UI",
"theme": "佈景主題",
"themeSystem": "跟隨系統",
"themeLight": "亮色主題",
"themeDark": "暗色主題",
"locale": "語言",
"about": "關於",
"homepage": "首頁",
"version": "版本",
"protocol": "通訊協定",
"port": "端口",
"apiToken": "接口 Token",
"notSet": "尚未設定",
"set": "已設定",
"effectAfterRestart": "該選項將在重新啓動軟體後生效",
"startAll": "全部開始",
"pauseAll": "全部暫停",
"deleteTask": "刪除任務",
"deleteTaskTip": "保留已經下載的文件",
"delete": "刪除",
"newVersionTitle": "發現新版本 @version",
"newVersionUpdate": "立刻更新",
"newVersionLater": "稍後更新"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
/* Code generated by cmd/cgo; DO NOT EDIT. */
/* package github.com/monkeyWie/gopeed/bind/desktop */
/* package github.com/GopeedLab/gopeed/bind/desktop */
#line 1 "cgo-builtin-export-prolog"
@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
<string>11.0</string>
</dict>
</plist>
+1 -1
View File
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
# platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+52
View File
@@ -0,0 +1,52 @@
PODS:
- file_selector_ios (0.0.1):
- Flutter
- Flutter (1.0.0)
- package_info_plus (0.4.5):
- Flutter
- path_provider_ios (0.0.1):
- Flutter
- permission_handler_apple (9.0.4):
- Flutter
- shared_preferences_ios (0.0.1):
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`)
- Flutter (from `Flutter`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
EXTERNAL SOURCES:
file_selector_ios:
:path: ".symlinks/plugins/file_selector_ios/ios"
Flutter:
:path: Flutter
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
shared_preferences_ios:
:path: ".symlinks/plugins/shared_preferences_ios/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
file_selector_ios: 6e9b6d7300556c730f224905726f0a2d5d874e51
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
COCOAPODS: 1.11.3
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -13,28 +13,36 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
D081C25C294826C0006EB10B /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D081C25B294826C0006EB10B /* libc++.tbd */; };
D0E623A929482D160001185E /* Libgopeed.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0E623A729482D030001185E /* Libgopeed.xcframework */; };
D0E623AA29482D160001185E /* Libgopeed.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0E623A729482D030001185E /* Libgopeed.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
DCA5B0FA95A8007ADEDACA1B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70BC8D37FAC641C7F20125E8 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
buildActionMask = 8;
dstPath = "";
dstSubfolderSpec = 10;
files = (
D0E623AA29482D160001185E /* Libgopeed.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
runOnlyForDeploymentPostprocessing = 1;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
25B61E78A388C87913468FD6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
70BC8D37FAC641C7F20125E8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
97232923D357E56CAA39281D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -42,6 +50,9 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
BC90D28B9068C663815415CC /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
D081C25B294826C0006EB10B /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; };
D0E623A729482D030001185E /* Libgopeed.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Libgopeed.xcframework; path = Frameworks/Libgopeed.xcframework; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -49,6 +60,9 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D0E623A929482D160001185E /* Libgopeed.xcframework in Frameworks */,
D081C25C294826C0006EB10B /* libc++.tbd in Frameworks */,
DCA5B0FA95A8007ADEDACA1B /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -72,6 +86,8 @@
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
ADF7F433B4BC23D6C18AB78B /* Pods */,
B4A1CBAC5AA50208793CFF34 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -98,6 +114,26 @@
path = Runner;
sourceTree = "<group>";
};
ADF7F433B4BC23D6C18AB78B /* Pods */ = {
isa = PBXGroup;
children = (
97232923D357E56CAA39281D /* Pods-Runner.debug.xcconfig */,
BC90D28B9068C663815415CC /* Pods-Runner.release.xcconfig */,
25B61E78A388C87913468FD6 /* Pods-Runner.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
B4A1CBAC5AA50208793CFF34 /* Frameworks */ = {
isa = PBXGroup;
children = (
D0E623A729482D030001185E /* Libgopeed.xcframework */,
D081C25B294826C0006EB10B /* libc++.tbd */,
70BC8D37FAC641C7F20125E8 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -105,12 +141,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
F3093A8F123671F8F587626F /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
DDA801E9D83E2E88398514D3 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -127,7 +165,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@@ -171,10 +209,12 @@
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
@@ -185,6 +225,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -197,6 +238,45 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
DDA801E9D83E2E88398514D3 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
F3093A8F123671F8F587626F /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -272,7 +352,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -288,6 +368,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -349,7 +430,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -398,7 +479,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -416,6 +497,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -438,6 +520,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
+32 -7
View File
@@ -1,13 +1,38 @@
import UIKit
import Flutter
import Libgopeed
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(name: "gopeed.com/libgopeed",
binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "start":
let args = call.arguments as? Dictionary<String, Any>
let cfg = args?["cfg"] as? String
let portPrt = UnsafeMutablePointer<Int>.allocate(capacity: MemoryLayout<Int>.stride)
var error: NSError?
if LibgopeedStart(cfg, portPrt, &error){
result(portPrt.pointee)
}else{
result(FlutterError(code: "ERROR", message: error.debugDescription, details: nil))
}
case "stop":
LibgopeedStop()
result(nil)
default:
result(FlutterMethodNotImplemented)
}
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Loaded 100 of 207 files, more files were not shown because too many files have changed in this diff. Show more