Compare commits

...
202 Commits
Author SHA1 Message Date
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
Li Wei 21ec1e4539 fix: follow android 11 storage permissions (#66) 2022-12-16 21:43:06 +08:00
levi dda44cc770 fix: docker tag 2022-12-16 15:24:41 +08:00
levi 78911b2aa9 ci: fix docker build 2022-12-16 15:10:34 +08:00
Li Wei 4935dcd0e6 ci: fix 2022-12-16 15:02:33 +08:00
Li Wei e8fe6ad550 ci: docker supports more platforms (#64) 2022-12-16 14:56:56 +08:00
Li Wei db4003ad4f fix: speed calculate (#63) 2022-12-16 14:53:07 +08:00
Li Wei 0a4be165a5 fix: http download fail (#61) 2022-12-14 12:12:39 +08:00
levi c62867e3ef chore: fix docker-compose command 2022-12-13 15:00:32 +08:00
levi 6df4959845 ci: fix 2022-12-13 14:49:53 +08:00
levi e0b18013be chore: release v1.2.0 2022-12-13 14:48:01 +08:00
levi d205e3b372 feat: enable ru_RU 2022-12-13 14:47:08 +08:00
azatzin30 719daf054b feat: Translation to Russian language (#60) 2022-12-13 14:42:08 +08:00
levi a5f72a22c3 fix: i18n 2022-12-13 14:39:51 +08:00
levi bf5c7655f2 test: remove fail code 2022-12-12 16:06:00 +08:00
Li Wei 691c2f496e feat: support custom tracker url (#59) 2022-12-12 15:58:11 +08:00
gesneriana 9ef3405e43 fix: missing single quotes (#57) 2022-11-16 13:34:15 +08:00
azatzin30 215faa0bf0 feat: Added Russian language (#55) 2022-11-11 22:17:38 +08:00
cgcel 695da7ea87 feat: support docker compose (#54) 2022-11-10 14:13:34 +08:00
liwei 7c6c361d30 fix: docker dist copy 2022-11-08 22:55:56 +08:00
liwei 872ba0ba87 doc: fix docker run command 2022-11-08 22:36:47 +08:00
monkeyWie 93c14dcee5 fix: ip bind (#50) 2022-11-08 22:29:33 +08:00
liwei 1af0b2175c ci: path include Dockerfile 2022-11-08 21:50:45 +08:00
monkeyWie ee81ebffef fix: docker executable (#49) 2022-11-08 21:49:02 +08:00
levi dc00fd2543 doc: fix 2022-11-08 15:02:58 +08:00
levi 11015c7be1 doc: mac repair tip 2022-11-08 14:35:51 +08:00
monkeyWie 57befd2c03 fix: web download dir setting (#46) 2022-11-08 14:31:49 +08:00
monkeyWie 35156ed968 ci: remove windows zip root folder 2022-11-07 20:29:10 +08:00
levi ee6c36174f ci: fix version 2022-11-07 17:25:23 +08:00
levi fdfd95555d ci: fix docker build 2022-11-07 17:20:31 +08:00
levi 7ec824edda ci: fix 2022-11-07 17:13:00 +08:00
levi 1f8b1be669 ci: fix action version 2022-11-07 16:49:58 +08:00
levi b0bcfeae34 ci: fix action version 2022-11-07 16:48:02 +08:00
levi 10b346a326 ci: fix 2022-11-07 16:45:44 +08:00
levi a85af39b35 ci: fix build 2022-11-07 16:43:55 +08:00
levi 576f0d2e24 ci: fix docker tag 2022-11-07 16:41:52 +08:00
monkeyWie 95eb3a427c feat: docker support (#43) 2022-11-07 16:32:52 +08:00
monkeyWie e9442edd94 feat: support drag torrent file (#42) 2022-11-07 14:12:12 +08:00
monkeyWie 77e59f7376 feat: i18n support (#40) 2022-11-05 17:04:22 +08:00
monkeyWie 135fba1b54 doc: fix md 2022-11-05 13:16:57 +08:00
liwei ff813e3bc9 ci: build on published 2022-11-05 11:27:56 +08:00
Trim21 babfe79023 Update flags.go 2022-11-05 10:43:07 +08:00
liwei 89ac52bebd ci: path ignore 2022-11-04 21:48:22 +08:00
liwei 15e1c2f2d5 chore: git ignore generated_plugin 2022-11-04 21:43:31 +08:00
levi 203f0ccc82 chore: fix go version 2022-11-04 17:13:32 +08:00
levi 2422bd2c66 chore: update md 2022-11-04 16:38:07 +08:00
levi 0306a1ed6b chore: en link 2022-11-04 16:26:19 +08:00
levi 8a94650370 chore: en readme 2022-11-04 16:25:57 +08:00
levi 837a193e96 chore: add discord link 2022-11-04 15:40:31 +08:00
levi fcf0746f9a ci: fix version 2022-11-04 15:12:38 +08:00
levi 9eb466ccd3 ci: path ignore 2022-11-04 15:11:06 +08:00
levi c54f22613e chore: issue tpl 2022-11-04 15:06:54 +08:00
liwei 3e1cfa0a27 ci: release v1.0.0 again 2022-11-03 23:31:48 +08:00
liwei 9d22b49e41 ci: fix windows build 2022-11-03 23:23:23 +08:00
liwei 1c6df55975 ci: debug 2022-11-03 23:17:40 +08:00
liwei 237616ded2 ci: debug 2022-11-03 23:15:35 +08:00
liwei cebdda564e ci: debug windows 2022-11-03 22:18:15 +08:00
liwei 08ffb51946 ci: build windows 2022-11-03 21:21:39 +08:00
levi 8378f51f8e ci: fix windows 2022-11-03 20:11:28 +08:00
levi 978d786f99 ci: fix windows 2022-11-03 19:56:17 +08:00
levi 8b997fa7f2 ci: fix windows build 2022-11-03 19:38:00 +08:00
levi 745afb600c ci: debug windows 2022-11-03 19:21:59 +08:00
levi b45b840afb ci: fix windows build 2022-11-03 19:03:12 +08:00
levi 2a7a136203 ci: debug 2022-11-03 18:13:09 +08:00
levi dfb8958999 ci: debug 2022-11-03 18:05:24 +08:00
levi 1568f4d6ff ci: debug windows 2022-11-03 17:56:55 +08:00
levi 12ebceecb0 ci: debug windows 2022-11-03 17:34:23 +08:00
levi 8cfe46cb71 ci: debug windows mingw dir 2022-11-03 17:01:49 +08:00
liwei 2f1b929bcd ci: fix windows build 2022-11-02 22:55:11 +08:00
liwei 2144cd8c3a ci: release v1.0.0 2022-11-02 22:40:35 +08:00
liwei 080b34d070 ci: fix windows 2022-11-02 22:14:06 +08:00
liwei de73d2a29f ci: fix windows build 2022-11-02 22:04:45 +08:00
liwei 9de2b765fb chore: add windows build step 2022-11-02 21:58:49 +08:00
liwei be75b50521 ci: build windows 2022-11-02 21:58:34 +08:00
levi 39aec19471 chore: update md 2022-11-02 17:55:00 +08:00
monkeyWie 9884001d54 chore: update md 2022-11-02 17:35:08 +08:00
levi 29c834a070 chore: add linux build step 2022-11-02 17:26:03 +08:00
levi b16f348676 chore: update md 2022-11-02 17:23:10 +08:00
monkeyWie 7b46c2a2f6 chore: create LICENSE 2022-11-02 17:17:54 +08:00
levi 1716d8b33e chore: update md 2022-11-02 17:02:45 +08:00
levi 855f0b4002 chore: update md 2022-11-02 17:01:58 +08:00
monkeyWie ee0d081b83 ci: fix linux desktop icon 2022-11-02 16:41:31 +00:00
monkeyWie d2f91a49ad ci: fix android name 2022-11-02 16:05:51 +00:00
monkeyWie fb6f889ddb ci: fix linux name 2022-11-02 15:53:16 +00:00
monkeyWie 0e6f1013cb ci: fix linux 2022-11-02 15:48:54 +00:00
monkeyWie fd10b18707 ci: fix 2022-11-02 15:42:27 +00:00
monkeyWie f423cf5b6d ci: fix linux version 2022-11-02 15:37:39 +00:00
monkeyWie 37b54b0c0f ci: fix linux build 2022-11-02 15:27:04 +00:00
monkeyWie cd74c895d3 ci: fix config 2022-11-02 15:22:38 +00:00
monkeyWie 450027a8b0 ci: fix linux build env 2022-11-02 15:21:04 +00:00
monkeyWie 48821a519e ci: build linux 2022-11-02 15:16:24 +00:00
levi 423ee5c3d1 chore: remove test code 2022-11-02 12:12:31 +08:00
levi 96c8945ac8 chore: update md 2022-11-02 12:05:22 +08:00
levi b2b714ae88 chore: change banner 2022-11-02 12:04:01 +08:00
levi 2793ff696e chore: refactor md 2022-11-02 12:02:58 +08:00
levi 2f81c5ef04 ci: fix web build 2022-11-02 12:02:19 +08:00
levi 2eed470d5c feat: icon 2022-11-02 10:26:22 +08:00
levi 34f8df1999 ci: fix web zip 2022-11-01 16:39:57 +08:00
levi e901e08534 fix: android lib 2022-11-01 16:30:15 +08:00
levi 56561ba764 ci: fix web build name 2022-11-01 16:05:57 +08:00
levi cf60e5a60a ci: fix web embed 2022-11-01 15:30:45 +08:00
levi bf8e24a6af ci: build web 2022-11-01 15:13:59 +08:00
levi ff4e6f448c ci: improve name 2022-11-01 13:38:37 +08:00
levi dccf080fbb fix: mac build 2022-11-01 10:33:17 +08:00
levi 9d7094133d chore: update md 2022-11-01 10:17:27 +08:00
levi ea6277805c ci: move to main branch 2022-11-01 10:15:09 +08:00
levi 3ad3060d71 ci: fix macos build 2022-11-01 10:07:23 +08:00
levi c1545adeb2 fix: andriod unix socket 2022-11-01 10:05:04 +08:00
levi 9659176752 ci: fix gomobile init 2022-10-31 15:36:58 +08:00
levi 4371345c7d ci: apk build 2022-10-31 15:33:54 +08:00
levi 655f9d99c7 chore: git ignore 2022-10-31 15:25:21 +08:00
levi a9c6705bd3 ci: add xgo cache 2022-10-28 14:55:25 +08:00
levi 46e0c6258f ci: upgrade go version to 1.19 2022-10-28 14:46:16 +08:00
levi 7c596c6b91 ci: fix 2022-10-28 14:40:40 +08:00
levi 5e92379aa2 ci: fix macos arm 2022-10-28 14:39:41 +08:00
levi 8a47b6cfb5 ci: macos arm 2022-10-28 14:26:31 +08:00
levi 97eca4ec9c ci: fix overwrite 2022-10-28 11:11:28 +08:00
levi b0a7a5569a feat: add logger 2022-10-28 10:57:39 +08:00
levi 5073ae6166 ci: fix macos asset path 2022-10-27 16:26:49 +08:00
levi 81c6fcf15d fix: ci node install cache 2022-10-27 16:16:19 +08:00
levi 2c6f821c4b chore: update md 2022-10-27 16:11:30 +08:00
levi 236423fe3c ci: fix 2022-10-27 16:07:59 +08:00
levi a8136afc31 ci: flutter build 2022-10-27 15:57:56 +08:00
levi 4d4d3b446a feat: support flutter web 2022-10-27 13:58:43 +08:00
levi c4a30dc4c2 feat: flutter ui integration 2022-10-27 13:58:43 +08:00
levi 0fc133e46d feat: bt improve 2022-10-26 17:18:13 +08:00
monkeyWie c6aa83194f feat: support RESTful api (#33) 2022-09-09 18:31:57 +08:00
monkeyWie e292926697 fix: http download resume task (#31) 2022-08-12 13:06:58 +08:00
271 changed files with 18283 additions and 604 deletions

No files matched your search

+27
View File
@@ -0,0 +1,27 @@
## Donate
Thank you for your support! 🙏
<div style="text-align: center;">
<table>
<tr>
<th>微信</th>
<th>支付宝</th>
<th>PayPal</th>
<th>Ko-fi</th>
</tr>
<tr>
<td><img src="/.donate/qr/wechatpay.jpg" alt="wechatpay" width="300" /></td>
<td><img src="/.donate/qr/alipay.jpg" alt="alipay" width="300" /></td>
<td><a href="https://paypal.me/monkeyWie" target="_blank">Click Me</a></td>
<td><a href="https://ko-fi.com/gopeed" target="_blank">Click Me</a></td>
</table>
</div>
### Crypto
- **BTC**: 35jYkPN8bgkh7AWjt3B1VgMQHqtGYcvF6p
- **ETH:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0
- **Matic:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0
- **BNB:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0
- **USDT(ERC20)**: 0x928a7fd2398c941Ac6713704e606Fa414595e3A0
Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

+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
+8
View File
@@ -0,0 +1,8 @@
<!--
Please use the following template to create an issue.
-->
### Description(required)
### App Version(required)
### OS Version(required)
### Snapshots
### Log
+370
View File
@@ -0,0 +1,370 @@
name: build
on:
workflow_dispatch:
env:
GO_VERSION: 1.19
FLUTTER_VERSION: 3.7.12
permissions:
contents: write
jobs:
get-release:
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.get-release.outputs.tag_name }}
upload_url: ${{ steps.get-release.outputs.upload_url }}
steps:
- 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@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: |
Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/main/Files/Languages/Unofficial/ChineseSimplified.isl" -OutFile "ChineseSimplified.isl"
mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\"
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build windows
$mingw = "C:\ProgramData\Chocolatey\lib\mingw\tools\install\mingw64\bin"
$system = "C:\Windows\System32"
$release = "build\windows\runner\Release\"
cp $mingw\libstdc++-6.dll $release
cp $mingw\libgcc_s_seh-1.dll $release
cp $mingw\libwinpthread-1.dll $release
cp $system\msvcp140.dll $release
cp $system\vcruntime140.dll $release
cp $system\vcruntime140_1.dll $release
New-Item -Path build\windows\Output -ItemType Directory
Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$env:VERSION-windows-amd64-portable.zip"
cd build/windows
echo @"
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "Gopeed"
#define MyAppVersion "$($env:VERSION.substring(1))"
#define MyAppPublisher "monkeyWie"
#define MyAppURL "https://gopeed.com"
#define MyAppExeName "gopeed.exe"
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{5960F34D-1E42-402C-8C85-DE2FF24CBAE4}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\gopeed
DisableProgramGroupPage=yes
LicenseFile=..\..\..\..\LICENSE
; Remove the following line to run in administrative install mode (install for all users.)
PrivilegesRequired=lowest
OutputBaseFilename=gopeed
SetupIconFile=..\..\assets\icon\icon.ico
Compression=lzma
SolidCompression=yes
WizardStyle=modern
LanguageDetectionMethod=uilanguage
ShowLanguageDialog=yes
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: ".\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[UninstallDelete]
Type: filesandordirs; Name: "{app}"
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
"@ > setup.iss
iscc.exe setup.iss
mv "Output\gopeed.exe" "Output\Gopeed-$env:VERSION-windows-amd64.exe"
Compress-Archive -Path "Output\Gopeed-$env:VERSION-windows-amd64.exe" -DestinationPath "Output\Gopeed-$env:VERSION-windows-amd64.zip"
Remove-Item -Path "Output\Gopeed-$env:VERSION-windows-amd64.exe"
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/build/windows/Output/*
overwrite: true
build-macos-arm64-lib:
runs-on: ubuntu-latest
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- name: Build
run: |
docker pull techknowlogick/xgo:go-1.19.x
go install src.techknowlogick.com/xgo@latest
xgo --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
mv libgopeed-*.dylib libgopeed.dylib
- uses: actions/upload-artifact@v3
with:
name: macos-arm64-lib
path: libgopeed.dylib
build-macos:
runs-on: macos-latest
needs: [get-release, build-macos-arm64-lib]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/setup-node@v3
with:
node-version: 16
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install appdmg
run: npm install -g appdmg
- uses: actions/download-artifact@v3
with:
name: macos-arm64-lib
path: ui/flutter/lib-arm64
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
run: |
PROJECT_DIR=$(pwd)
# amd64 lib
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/lib-amd64/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
# universal binary
mkdir -p ui/flutter/macos/Frameworks
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/amd64-lib
cp ui/flutter/lib-arm64/libgopeed.dylib ui/flutter/macos/Frameworks/arm64-lib
cd ui/flutter/macos/Frameworks
lipo -create -output libgopeed.dylib amd64-lib arm64-lib
cd $PROJECT_DIR/ui/flutter
flutter build macos
cd build/macos/Build/Products/Release
cat>appdmg.json<<EOF
{
"title": "Gopeed",
"icon": "Gopeed.app/Contents/Resources/AppIcon.icns",
"contents": [
{ "x": 448, "y": 344, "type": "link", "path": "/Applications" },
{ "x": 192, "y": 344, "type": "file", "path": "Gopeed.app" }
]
}
EOF
mkdir dist
appdmg appdmg.json dist/Gopeed-$VERSION-macos.dmg
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/build/macos/Build/Products/Release/dist/*
overwrite: true
build-linux:
runs-on: ubuntu-latest
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build linux
mkdir -p debian/gui
cp assets/icon/icon_1024.png debian/gui/gopeed.png
cat>debian/debian.yaml<<EOF
flutter_app:
command: gopeed
arch: x64
parent: /usr/local/lib
control:
Package: gopeed
Version: ${VERSION:1}
Architecture: amd64
Essential: no
Priority: optional
Depends:
Maintainer: monkeyWie
Description: High speed downloader that supports all platforms.
EOF
cat>debian/gui/gopeed.desktop<<EOF
[Desktop Entry]
Version=${VERSION:1}
Name=Gopeed
GenericName=Gopeed
Comment=High speed downloader that supports all platforms.
Terminal=false
Type=Application
Categories=Utility
Keywords=Flutter;Downloader;
EOF
dart pub global activate flutter_to_debian
flutter_to_debian
mv debian/packages/gopeed*.deb debian/packages/Gopeed-$VERSION-linux-amd64.deb
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/debian/packages/*
overwrite: true
build-android:
runs-on: ubuntu-latest
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/setup-java@v3
with:
distribution: "zulu"
java-version: "11"
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
APK_KEYSTORE : ${{ secrets.APK_KEYSTORE }}
APK_KEY_PASSWORD : ${{ secrets.APK_KEY_PASSWORD }}
APK_STORE_PASSWORD : ${{ secrets.APK_STORE_PASSWORD }}
run: |
go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind
gomobile init
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
flutter build apk
mkdir dist
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-$VERSION-android.apk
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/dist/*
overwrite: true
build-web:
runs-on: ubuntu-latest
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
run: |
cd ui/flutter
flutter build web
cd ../../
cp -r ui/flutter/build/web cmd/web/dist
mkdir -p dist/zip
goos_arr=(windows darwin linux)
goarch_arr=(386 amd64 arm64)
export CGO_ENABLED=0
for goos in "${goos_arr[@]}"; do
for goarch in "${goarch_arr[@]}"; do
goos_name=$goos
if [ $goos = "darwin" ]; then
goos_name="macos"
fi
name=gopeed-web-$VERSION-$goos_name-$goarch
dir="dist/$name/"
(GOOS=$goos GOARCH=$goarch go build -tags nosqlite,web -ldflags="-s -w" -o $dir github.com/GopeedLab/gopeed/cmd/web \
&& cd $dir \
&& file=$(ls -AU | head -1) \
&& mkdir $name \
&& mv $file $name/$(echo $file | sed -e "s/web/gopeed/g") \
&& zip -r ../zip/$name.zip * \
&& cd ../..) \
|| true
done
done
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: dist/zip/*
overwrite: true
build-docker:
runs-on: ubuntu-latest
needs: [get-release]
steps:
- uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: liwei2633
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v2
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
with:
push: true
platforms: |
linux/386
linux/amd64
linux/arm64
tags: |
liwei2633/gopeed:latest
liwei2633/gopeed:${{ env.VERSION }}
+25 -30
View File
@@ -3,43 +3,38 @@ name: release
on: on:
push: push:
branches: branches:
- master - main
paths-ignore: paths:
- '_docs/**' - "bind/**"
- 'README.md' - "cmd/**"
- "internal/**"
- "pkg/**"
- "ui/**"
- ".github/workflows/release.yml"
- ".github/release-drafter.yml"
- "go.mod"
- "go.sum"
- "Dockerfile"
env:
GO_VERSION: 1.19
permissions:
contents: write
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.create_release.outputs.tag_name }}
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps: steps:
- uses: actions/setup-go@v3
with:
go-version: '^1.18'
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- uses: release-drafter/release-drafter@v5 - uses: release-drafter/release-drafter@v5
id: create_release id: create_release
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build
env:
VERSION: ${{ steps.create_release.outputs.tag_name }}
run: |
mkdir -p dist/zip
goos_arr=(windows darwin linux)
goarch_arr=(386 amd64 arm arm64)
export CGO_ENABLED=0
for goos in "${goos_arr[@]}"; do
for goarch in "${goarch_arr[@]}"; do
dir="dist/gopeed-$VERSION-$goos-$goarch/"
(GOOS=$goos GOARCH=$goarch go build -tags nosqlite -ldflags="-s -w" -o $dir github.com/monkeyWie/gopeed-core/cmd/gopeed \
&& cd $dir \
&& zip -r ../zip/gopeed-$VERSION-$goos-$goarch.zip * \
&& cd ../..) \
|| true
done
done
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: dist/zip/*
+59 -35
View File
@@ -3,54 +3,78 @@ name: test
on: on:
pull_request: pull_request:
branches: branches:
- master - main
paths-ignore: paths:
- '_docs/**' - "bind/**"
- 'README.md' - "cmd/**"
- "internal/**"
- "pkg/**"
- "ui/**"
- ".github/workflows/test.yml"
- "go.mod"
- "go.sum"
push: push:
branches: branches:
- master - main
paths-ignore: paths:
- '_docs/**' - "bind/**"
- 'README.md' - "cmd/**"
- "internal/**"
- "pkg/**"
- "ui/**"
- ".github/workflows/test.yml"
- "go.mod"
- "go.sum"
env:
GO_VERSION: 1.19
FLUTTER_VERSION: 3.7.12
jobs: jobs:
# lint: # lint:
# name: Lint # name: Lint
# runs-on: ubuntu-latest # runs-on: ubuntu-latest
# steps: # steps:
# - uses: actions/setup-go@v2 # - uses: actions/setup-go@v2
# with: # with:
# go-version: '^1.18' # go-version: '^1.19'
# - uses: actions/checkout@v2 # - uses: actions/checkout@v2
# - name: Lint Go Code # - name: Lint Go Code
# run: | # run: |
# export PATH=$PATH:$(go env GOPATH)/bin # temporary fix. See https://github.com/actions/setup-go/issues/14 # export PATH=$PATH:$(go env GOPATH)/bin # temporary fix. See https://github.com/actions/setup-go/issues/14
# go get -u golang.org/x/lint/golint # go get -u golang.org/x/lint/golint
# golint -set_exit_status ./... # golint -set_exit_status ./...
test: test:
name: Test name: Test check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-go@v2 - uses: actions/checkout@v3
- uses: actions/setup-go@v4
with: with:
go-version: '^1.18' go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v2
- name: Run Unit tests. - name: Run Unit tests.
run: go test ./... -v -covermode=count -coverprofile=coverage.txt run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt
- uses: codecov/codecov-action@v1 - uses: codecov/codecov-action@v3
with: with:
file: ./coverage.txt files: ./coverage.txt
fail_ci_if_error: true
build: build:
name: Build name: Build check
runs-on: ubuntu-latest runs-on: ubuntu-latest
# needs: [lint, test] # needs: [lint, test]
needs: [test] needs: [test]
steps: steps:
- uses: actions/setup-go@v2 - uses: actions/checkout@v3
- uses: actions/setup-go@v4
with: with:
go-version: '^1.18' go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v2 - uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
- name: Build - name: Build
run: go build github.com/monkeyWie/gopeed-core/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
+7 -2
View File
@@ -11,8 +11,6 @@
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
debug
.idea/ .idea/
bin/ bin/
@@ -20,3 +18,10 @@ bin/
.torrent.db .torrent.db
.torrent.db-shm .torrent.db-shm
.torrent.db-wal .torrent.db-wal
*.data
*.db
.DS_Store
dist/
+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
```
+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
```
+21
View File
@@ -0,0 +1,21 @@
FROM cirrusci/flutter:3.7.7 AS flutter
WORKDIR /app
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
RUN flutter pub get
COPY ./ui/flutter ./
RUN flutter build web
FROM golang:1.19.3 AS go
WORKDIR /app
COPY ./go.mod ./go.sum ./
RUN go mod download
COPY . .
COPY --from=flutter /app/build/web ./cmd/web/dist
RUN CGO_ENABLED=0 go build -tags nosqlite,web -ldflags="-s -w" -o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
FROM alpine:3.14.2
LABEL maintainer="monkeyWie"
WORKDIR /app
COPY --from=go /app/dist/gopeed ./
EXPOSE 9999
ENTRYPOINT ["./gopeed"]
+674
View File
File diff suppressed because it is too large. Load diff
+154 -31
View File
@@ -1,47 +1,170 @@
[![Test Status](https://github.com/monkeyWie/gopeed-core/workflows/test/badge.svg)](https://github.com/monkeyWie/gopeed-core/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/monkeyWie/gopeed-core/branch/master/graph/badge.svg)](https://codecov.io/gh/monkeyWie/gopeed-core) [![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/monkeyWie/gopeed-core.svg?style=flat-square)](https://github.com/monkeyWie/gopeed-core/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg?style=flat-square)](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)
## gopeed ![](_docs/img/banner.png)
支持多协议(HTTP、BitTorrent)下载的客户端,提供命令行、RESTful API、WebSocket、Go 类库方式来使用。 [English](/README.md) | [中文](/README_zh-CN.md)
## 安装 ## Introduction
```sh Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms.
go install github.com/monkeyWie/gopeed-core
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Develop Docs](https://docs.gopeed.com)
## Install
**Supported platforms**
- [x] windows
- [x] macos
- [x] linux
- [x] android
- [ ] ios
- [x] web
- [x] docker
[Download](https://github.com/GopeedLab/gopeed/releases/latest)
> Tips: If the macos open failed, please execute the `xattr -d com.apple.quarantine /Applications/Gopeed.app` command in the terminal
### Command tool
use `go install`:
```bash
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
``` ```
## 示例 ### Docker
可以在本仓库的[\_examples](_examples)目录下查看 #### Start directly
## 命令行工具 ```bash
docker run -d -p 9999:9999 -v /path/to/download:/download liwei2633/gopeed
```
### 预览 #### Using Docker Compose
![](_docs/img/cli-demo.gif) ```bash
docker-compose up -d
```
### 下载 #### Access Gopeed
前往[releases](https://github.com/monkeyWie/gopeed-core/releases)页面 When the docker container is running, you can access the web page through `http://localhost:9999`.
## TODO > Tip: Modify the download path to `/download` on the setting page to access the downloaded files on the host.
- [x] HTTP 下载实现 ## Donate
- [x] BitTorrent 下载实现
- [x] .torrent 文件解析
- [x] tracker 协议实现
- [x] peer wire protocol 协议实现
- [x] DHT 协议实现
- [x] 磁力链接支持
- [x] uTP 协议实现
- [x] 下载接口抽象(不关心具体协议)
- [ ] 支持自定义配置
- [ ] 限速功能实现
- [x] 命令行工具提供
- [ ] RESTful 服务提供
## 参与 If you like this project, please consider [donating](/.donate/index.md#donate) to support the development of this project, thank you!
由于项目目前还未定型,代码可能随时有大的调整,所以暂不接受 PR,当然如果有什么好的想法可以在 issue 区提出来。 ## Showcase
![](_docs/img/ui-demo.png)
## Development
This project is divided into two parts, the front end uses `flutter`, the back end uses `Golang`, and the two sides communicate through the `http` protocol. On the unix system, `unix socket` is used, and on the windows system, `tcp` protocol is used.
> The front code is located in the `ui/flutter` directory.
### Environment
1. Golang 1.19+
2. Flutter 3.7+
### Clone
```bash
git clone [email protected]:monkeyWie/gopeed.git
```
### Contributing
Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md)
### Build
#### Desktop
First, you need to configure the environment according to the [flutter desktop official website document](https://docs.flutter.dev/development/platform-integration/desktop), and then you need to prepare the `cgo` environment, which can be searched for yourself.
command:
- windows
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build windows
```
- macos
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build macos
```
- linux
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build linux
```
#### Mobile
Same as before, you also need to prepare the `cgo` environment, and then install `gomobile`:
```bash
go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind
gomobile init
```
command:
- android
```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build apk
```
#### Web
command:
```bash
cd ui/flutter
flutter build web
cd ../../
rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
go run cmd/web/main.go
```
## Thanks
### Contributors
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
</a>
### JetBrains
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## License
[GPLv3](LICENSE)
+169
View File
@@ -0,0 +1,169 @@
[![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?style=flat-square)](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)
## 介绍
Gopeed(全称 Go Speed),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [开发文档](https://docs.gopeed.com/zh/)
## 安装
**已支持平台**
- [x] windows
- [x] macos
- [x] linux
- [x] android
- [ ] ios
- [x] web
- [x] docker
[前往下载](https://github.com/GopeedLab/gopeed/releases/latest)
> 注:macos 版本运行如果提示损坏,请在终端执行 `xattr -d com.apple.quarantine /Applications/Gopeed.app` 命令
### 命令行工具
使用`go install`安装:
```bash
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
```
### Docker
#### 直接运行
```bash
docker run -d -p 9999:9999 -v /path/to/download:/download liwei2633/gopeed
```
#### 使用 Docker Compose
```bash
docker-compose up -d
```
#### 访问服务
当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。
> 提示:在设置页面把下载路径修改为 `/download` 以便在宿主机访问下载完的文件。
## 打赏
如果觉得项目对你有帮助,请考虑[打赏](/.donate/index.md#donate)以支持这个项目的发展,非常感谢!
## 界面展示
![](_docs/img/ui-demo.png)
## 开发
本项目分为前端和后端两个部分,前端使用`flutter`,后端使用`Golang`,两边通过`http`协议进行通讯,在 unix 系统下,使用的是`unix socket`,在 windows 系统下,使用的是`tcp`协议。
> 前端代码位于`ui/flutter`目录下。
### 环境要求
1. Golang 1.19+
2. Flutter 3.7+
### 克隆项目
```bash
git clone [email protected]:monkeyWie/gopeed.git
```
### 贡献代码
请参考[贡献指南](CONTRIBUTING_zh-CN.md)
### 编译
#### 桌面端
首先需要按照[flutter desktop 官网文档](https://docs.flutter.dev/development/platform-integration/desktop)进行环境配置,然后需要准备好`cgo`环境,具体可以自行搜索。
构建命令:
- windows
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build windows
```
- macos
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build macos
```
- linux
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build linux
```
#### 移动端
同样的也是需要准备好`cgo`环境,接着安装`gomobile`:
```bash
go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind
gomobile init
```
构建命令:
- android
```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build apk
```
#### Web 端
构建命令:
```bash
cd ui/flutter
flutter build web
cd ../../
rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
go run cmd/web/main.go
```
## 感谢
### 贡献者
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
</a>
### JetBrains
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## 开源许可
基于 [GPLv3](LICENSE) 协议开源。
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+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

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+6 -3
View File
@@ -2,13 +2,14 @@ package main
import ( import (
"fmt" "fmt"
"github.com/monkeyWie/gopeed-core/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/monkeyWie/gopeed-core/pkg/download" "github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
) )
func main() { func main() {
finallyCh := make(chan error) finallyCh := make(chan error)
err := download.Boot(). _, err := download.Boot().
URL("https://www.baidu.com/index.html"). URL("https://www.baidu.com/index.html").
Listener(func(event *download.Event) { Listener(func(event *download.Event) {
if event.Key == download.EventKeyFinally { if event.Key == download.EventKeyFinally {
@@ -16,7 +17,9 @@ func main() {
} }
}). }).
Create(&base.Options{ Create(&base.Options{
Extra: http.OptsExtra{
Connections: 8, Connections: 8,
},
}) })
if err != nil { if err != nil {
panic(err) panic(err)
+28
View File
@@ -0,0 +1,28 @@
package main
import "C"
import (
"encoding/json"
"github.com/GopeedLab/gopeed/pkg/rest"
"github.com/GopeedLab/gopeed/pkg/rest/model"
)
func main() {}
//export Start
func Start(cfg *C.char) (int, *C.char) {
var config model.StartConfig
if err := json.Unmarshal([]byte(C.GoString(cfg)), &config); err != nil {
return 0, C.CString(err.Error())
}
realPort, err := rest.Start(&config)
if err != nil {
return 0, C.CString(err.Error())
}
return realPort, nil
}
//export Stop
func Stop() {
rest.Stop()
}
+21
View File
@@ -0,0 +1,21 @@
package libgopeed
// #cgo LDFLAGS: -static-libstdc++
import "C"
import (
"encoding/json"
"github.com/GopeedLab/gopeed/pkg/rest"
"github.com/GopeedLab/gopeed/pkg/rest/model"
)
func Start(cfg string) (int, error) {
var config model.StartConfig
if err := json.Unmarshal([]byte(cfg), &config); err != nil {
return 0, err
}
return rest.Start(&config)
}
func Stop() {
rest.Stop()
}
+16
View File
@@ -0,0 +1,16 @@
package main
import (
_ "embed"
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
)
func main() {
cfg := &model.StartConfig{
Network: "tcp",
Address: "127.0.0.1:9999",
Storage: model.StorageBolt,
}
cmd.Start(cfg)
}
+7
View File
@@ -0,0 +1,7 @@
_______ ______ .______ _______ _______ _______
/ _____| / __ \ | _ \ | ____|| ____|| \
| | __ | | | | | |_) | | |__ | |__ | .--. |
| | |_ | | | | | | ___/ | __| | __| | | | |
| |__| | | `--' | | | | |____ | |____ | '--' |
\______| \______/ | _| |_______||_______||_______/
+2 -3
View File
@@ -4,7 +4,6 @@ import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"path/filepath"
) )
type args struct { type args struct {
@@ -14,7 +13,7 @@ type args struct {
} }
func parse() *args { func parse() *args {
dir, err := filepath.Abs(filepath.Dir(os.Args[0])) dir, err := os.Getwd()
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -26,7 +25,7 @@ func parse() *args {
if len(t) > 0 { if len(t) > 0 {
args.url = t[0] args.url = t[0]
} else { } else {
gPrintln("missing url parameter") gPrintln("missing url parameter, for example: gopeed https://www.google.com or gopeed bt.torrent or gopeed magnet:?xt=urn:btih:...")
gPrintln("try 'gopeed -h' for more information") gPrintln("try 'gopeed -h' for more information")
os.Exit(1) os.Exit(1)
} }
+17 -9
View File
@@ -2,9 +2,10 @@ package main
import ( import (
"fmt" "fmt"
"github.com/monkeyWie/gopeed-core/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/monkeyWie/gopeed-core/pkg/download" "github.com/GopeedLab/gopeed/pkg/download"
"github.com/monkeyWie/gopeed-core/pkg/util" "github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util"
"strings" "strings"
"sync" "sync"
) )
@@ -12,10 +13,18 @@ import (
const progressWidth = 20 const progressWidth = 20
func main() { func main() {
args := parse() //args := parse()
var dir = "./"
var connections = 8
args := &args{
url: "https://sel-kor-ping.vultr.com/vultr.com.100MB.bin",
dir: &dir,
connections: &connections,
}
var wg sync.WaitGroup var wg sync.WaitGroup
err := download.Boot(). wg.Add(1)
_, err := download.Boot().
URL(args.url). URL(args.url).
Listener(func(event *download.Event) { Listener(func(event *download.Event) {
if event.Key == download.EventKeyProgress { if event.Key == download.EventKeyProgress {
@@ -40,12 +49,11 @@ func main() {
}). }).
Create(&base.Options{ Create(&base.Options{
Path: *args.dir, Path: *args.dir,
Connections: *args.connections, Extra: http.OptsExtra{Connections: *args.connections},
}) })
if err != nil { if err != nil {
panic(err) panic(err)
} }
wg.Add(1)
wg.Wait() wg.Wait()
} }
@@ -55,10 +63,10 @@ var (
) )
func printProgress(task *download.Task, title string) { func printProgress(task *download.Task, title string) {
rate := float64(task.Progress.Downloaded) / float64(task.Res.Length) rate := float64(task.Progress.Downloaded) / float64(task.Meta.Res.Size)
completeWidth := int(progressWidth * rate) completeWidth := int(progressWidth * rate)
speed := util.ByteFmt(task.Progress.Speed) speed := util.ByteFmt(task.Progress.Speed)
totalSize := util.ByteFmt(task.Res.Length) totalSize := util.ByteFmt(task.Meta.Res.Size)
sb.WriteString(fmt.Sprintf("\r%s [", title)) sb.WriteString(fmt.Sprintf("\r%s [", title))
for i := 0; i < progressWidth; i++ { for i := 0; i < progressWidth; i++ {
if i < completeWidth { if i < completeWidth {
+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)
}
}
+29
View File
@@ -0,0 +1,29 @@
//go:build web
// +build web
package main
import (
"embed"
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"io/fs"
)
//go:embed dist/*
var dist embed.FS
func main() {
sub, err := fs.Sub(dist, "dist")
if err != nil {
panic(err)
}
cfg := &model.StartConfig{
Network: "tcp",
Address: "0.0.0.0:9999",
Storage: model.StorageBolt,
WebEnable: true,
WebFS: sub,
}
cmd.Start(cfg)
}
+11
View File
@@ -0,0 +1,11 @@
version: '3'
services:
gopeed:
container_name: gopeed
ports:
- 9999:9999 # HTTP port (host:container)
image: liwei2633/gopeed
volumes:
- ~/Downloads:/download # replace with your prefer path
restart: unless-stopped
+10 -6
View File
@@ -1,12 +1,14 @@
module github.com/monkeyWie/gopeed-core module github.com/GopeedLab/gopeed
go 1.18 go 1.19
require ( require (
github.com/anacrolix/torrent v1.46.0 github.com/anacrolix/torrent v1.46.0
github.com/google/uuid v1.3.0 github.com/gorilla/handlers v1.5.1
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd github.com/gorilla/mux v1.8.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c github.com/matoous/go-nanoid/v2 v2.0.0
go.etcd.io/bbolt v1.3.6
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
) )
require ( require (
@@ -35,7 +37,9 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/google/btree v1.0.1 // indirect github.com/google/btree v1.0.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect github.com/gorilla/websocket v1.4.2 // indirect
github.com/huandu/xstrings v1.3.2 // 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.0.0 // indirect
@@ -60,9 +64,9 @@ require (
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/rs/dnscache v0.0.0-20210201191234-295bba877686 // indirect github.com/rs/dnscache v0.0.0-20210201191234-295bba877686 // indirect
github.com/tidwall/btree v1.3.1 // indirect github.com/tidwall/btree v1.3.1 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect
golang.org/x/exp v0.0.0-20220428152302-39d4317da171 // indirect golang.org/x/exp v0.0.0-20220428152302-39d4317da171 // indirect
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
+30 -1
View File
@@ -7,6 +7,7 @@ crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508 h1:fILCBBFnjnrQ0whVJlGhf
crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= 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 v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
@@ -105,6 +106,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
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.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -158,7 +161,11 @@ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORR
github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 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 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -187,6 +194,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:pPtt9QERE5sGQfwvLakKe2sjhcNKKsDSnr3939ofj1E=
github.com/lispad/go-generics-tools v1.0.0/go.mod h1:stn7X24ZIyFvaSyttafq3VlJzGJJJkUtLYdbgi/gopM= github.com/lispad/go-generics-tools v1.0.0/go.mod h1:stn7X24ZIyFvaSyttafq3VlJzGJJJkUtLYdbgi/gopM=
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=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -306,6 +316,7 @@ github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDW
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= 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/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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= 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.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.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
@@ -313,19 +324,30 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 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-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-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-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-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-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 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 h1:TfdoLivD44QwvssI9Sv1xwa5DcL5XQr4au4sZ2F2NV4=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 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/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 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-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-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/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.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/net v0.0.0-20180524181706-dfa909b99c79/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -343,6 +365,7 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/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-20210119194325-5f4716e94777/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-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-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-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-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -357,8 +380,9 @@ 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-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-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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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-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= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -381,6 +405,7 @@ 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-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-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-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 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-20211216021012-1d35b9e2eb4e/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -397,9 +422,13 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/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-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-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-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-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.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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+27 -36
View File
@@ -1,61 +1,52 @@
package controller package controller
import ( import (
"golang.org/x/net/proxy" "errors"
"net"
"os" "os"
"time" "path/filepath"
) )
type Controller interface { type Controller struct {
GetConfig func(v any) (bool, error)
FileController
//ContextDialer() (proxy.Dialer, error)
}
type FileController interface {
Touch(name string, size int64) (file *os.File, err error) Touch(name string, size int64) (file *os.File, err error)
Open(name string) (file *os.File, err error)
Write(name string, offset int64, buf []byte) (int, error)
Close(name string) error
ContextDialer() (proxy.Dialer, error)
} }
type DefaultController struct { type DefaultFileController struct {
Files map[string]*os.File
} }
func NewController() *DefaultController { func NewController() *Controller {
return &DefaultController{Files: make(map[string]*os.File)} return &Controller{
FileController: &DefaultFileController{},
GetConfig: func(v any) (bool, error) {
return false, nil
},
}
} }
func (c *DefaultController) Touch(name string, size int64) (file *os.File, err error) { func (c *DefaultFileController) Touch(name string, size int64) (file *os.File, err error) {
dir := filepath.Dir(name)
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
return
}
file, err = os.Create(name) file, err = os.Create(name)
if err != nil && !errors.Is(err, os.ErrExist) {
return
}
if size > 0 { if size > 0 {
err = os.Truncate(name, size) err = os.Truncate(name, size)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
if err == nil {
c.Files[name] = file
}
return return
} }
func (c *DefaultController) Open(name string) (file *os.File, err error) { /*func (c *DefaultController) ContextDialer() (proxy.Dialer, error) {
file, err = os.OpenFile(name, os.O_RDWR, os.ModePerm)
if err == nil {
c.Files[name] = file
}
return
}
func (c *DefaultController) Write(name string, offset int64, buf []byte) (int, error) {
return c.Files[name].WriteAt(buf, offset)
}
func (c *DefaultController) Close(name string) error {
err := c.Files[name].Close()
delete(c.Files, name)
return err
}
func (c *DefaultController) ContextDialer() (proxy.Dialer, error) {
// return proxy.SOCKS5("tpc", "127.0.0.1:9999", nil, nil) // return proxy.SOCKS5("tpc", "127.0.0.1:9999", nil, nil)
var dialer proxy.Dialer var dialer proxy.Dialer
return &DialerWarp{dialer: dialer}, nil return &DialerWarp{dialer: dialer}, nil
@@ -107,4 +98,4 @@ func (d *DialerWarp) Dial(network, addr string) (c net.Conn, err error) {
return nil, err return nil, err
} }
return &ConnWarp{conn: conn}, nil return &ConnWarp{conn: conn}, nil
} }*/
+49 -24
View File
@@ -1,45 +1,70 @@
package fetcher package fetcher
import ( import (
"github.com/monkeyWie/gopeed-core/internal/controller" "github.com/GopeedLab/gopeed/internal/controller"
"github.com/monkeyWie/gopeed-core/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"path"
) )
// 对应协议的下载支持 // Fetcher defines the interface for a download protocol.
// One fetcher for each download task
type Fetcher interface { type Fetcher interface {
Setup(ctl controller.Controller) error // Name return the name of the protocol.
// 解析请求 Name() string
Resolve(req *base.Request) (res *base.Resource, err error)
// 创建任务
Create(res *base.Resource, opts *base.Options) (err error)
Start() (err error)
Pause() (err error)
Continue() (err error)
// 获取任务各个文件下载进度 Setup(ctl *controller.Controller) error
// Resolve resource info from request
Resolve(req *base.Request) error
// Create ready to download, but not started
Create(opts *base.Options) error
Start() error
Pause() error
Continue() error
Close() error
// Meta returns the meta information of the download.
Meta() *FetcherMeta
// Progress returns the progress of the download.
Progress() Progress Progress() Progress
// 该方法会一直阻塞,直到任务下载结束 // Wait for the download to complete, this method will block until the download is done.
Wait() (err error) Wait() error
} }
// FetcherMeta defines the meta information of a fetcher.
type FetcherMeta struct {
Req *base.Request `json:"req"`
Res *base.Resource `json:"res"`
Opts *base.Options `json:"opts"`
}
func (m *FetcherMeta) Filepath(file *base.FileInfo) string {
finalName := m.Opts.Name
if finalName == "" {
finalName = file.Name
}
return path.Join(m.Opts.Path, m.Res.RootDir, finalName)
}
// FetcherBuilder defines the interface for a fetcher builder.
type FetcherBuilder interface { type FetcherBuilder interface {
// Schemes returns the schemes supported by the fetcher.
Schemes() []string Schemes() []string
// Build returns a new fetcher.
Build() Fetcher Build() Fetcher
}
type FetcherSaver interface { // Store fetcher
// Store 存储任务 Store(fetcher Fetcher) (any, error)
Store(fetcher Fetcher) interface{} // Restore fetcher
// Resume 恢复任务 Restore() (v any, f func(meta *FetcherMeta, v any) Fetcher)
Resume(res *base.Resource, opts *base.Options, data interface{}) Fetcher
} }
type DefaultFetcher struct { type DefaultFetcher struct {
Ctl controller.Controller Ctl *controller.Controller
Meta *FetcherMeta
DoneCh chan error DoneCh chan error
} }
func (f *DefaultFetcher) Setup(ctl controller.Controller) (err error) { func (f *DefaultFetcher) Setup(ctl *controller.Controller) (err error) {
f.Ctl = ctl f.Ctl = ctl
f.DoneCh = make(chan error, 1) f.DoneCh = make(chan error, 1)
return return
@@ -49,10 +74,10 @@ func (f *DefaultFetcher) Wait() (err error) {
return <-f.DoneCh return <-f.DoneCh
} }
// 获取任务中各个文件的已下载字节数 // Progress is a map of the progress of each file in the torrent.
type Progress []int64 type Progress []int64
// TotalDownloaded 获取任务总下载字节数 // TotalDownloaded returns the total downloaded bytes.
func (p Progress) TotalDownloaded() int64 { func (p Progress) TotalDownloaded() int64 {
total := int64(0) total := int64(0)
for _, d := range p { for _, d := range p {
+5
View File
@@ -0,0 +1,5 @@
package bt
type config struct {
Trackers []string `json:"trackers"`
}
+177 -46
View File
@@ -1,104 +1,223 @@
package bt package bt
import ( import (
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/anacrolix/torrent" "github.com/anacrolix/torrent"
"github.com/monkeyWie/gopeed-core/internal/controller" "github.com/anacrolix/torrent/metainfo"
"github.com/monkeyWie/gopeed-core/internal/fetcher" "github.com/anacrolix/torrent/storage"
"github.com/monkeyWie/gopeed-core/pkg/base" "path"
"github.com/monkeyWie/gopeed-core/pkg/util"
"path/filepath" "path/filepath"
"sync" "sync"
"sync/atomic"
"time"
) )
var client *torrent.Client var client *torrent.Client
type Fetcher struct { type Fetcher struct {
Ctl controller.Controller ctl *controller.Controller
torrent *torrent.Torrent torrent *torrent.Torrent
opts *base.Options meta *fetcher.FetcherMeta
ready atomic.Bool
progress fetcher.Progress
torrentPaths map[string]string
} }
func (f *Fetcher) Setup(ctl controller.Controller) (err error) { func (f *Fetcher) Name() string {
f.Ctl = ctl return "bt"
}
func (f *Fetcher) Setup(ctl *controller.Controller) (err error) {
f.ctl = ctl
if f.meta == nil {
f.meta = &fetcher.FetcherMeta{}
}
var once sync.Once var once sync.Once
once.Do(func() { once.Do(func() {
client, err = torrent.NewClient(nil) cfg := torrent.NewDefaultClientConfig()
cfg.ListenPort = 0
// Support custom download path for each torrent
pieceCompletion, err := storage.NewDefaultPieceCompletionForDir(cfg.DataDir)
if err != nil {
pieceCompletion = storage.NewMapPieceCompletion()
}
clientImplCloser := storage.NewFileOpts(storage.NewFileClientOpts{
ClientBaseDir: cfg.DataDir,
TorrentDirMaker: func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
if dir, ok := f.torrentPaths[infoHash.String()]; ok {
return dir
}
return baseDir
},
PieceCompletion: pieceCompletion,
})
cfg.DefaultStorage = clientImplCloser
client, err = torrent.NewClient(cfg)
if err != nil {
return
}
f.torrentPaths = make(map[string]string)
}) })
return return
} }
func (f *Fetcher) Wait() (err error) { func (f *Fetcher) Resolve(req *base.Request) error {
client.WaitAll() if err := f.addTorrent(req.URL); err != nil {
return nil return err
}
func (f *Fetcher) Resolve(req *base.Request) (res *base.Resource, err error) {
schema := util.ParseSchema(req.URL)
if schema == "MAGNET" {
f.torrent, err = client.AddMagnet(req.URL)
} else {
f.torrent, err = client.AddTorrentFromFile(req.URL)
} }
if err != nil { defer f.torrent.Drop()
return res := &base.Resource{
} Name: f.torrent.Name(),
<-f.torrent.GotInfo()
res = &base.Resource{
Req: req,
Length: f.torrent.Length(),
Range: true, Range: true,
RootDir: f.torrent.Name(),
Files: make([]*base.FileInfo, len(f.torrent.Files())), Files: make([]*base.FileInfo, len(f.torrent.Files())),
Hash: f.torrent.InfoHash().String(),
} }
for i, file := range f.torrent.Files() { for i, file := range f.torrent.Files() {
res.Files[i] = &base.FileInfo{ res.Files[i] = &base.FileInfo{
Name: filepath.Base(file.DisplayPath()), Name: filepath.Base(file.DisplayPath()),
Path: filepath.Dir(file.Path()), Path: util.Dir(file.Path()),
Size: file.Length(), Size: file.Length(),
} }
res.Size += file.Length()
} }
return f.meta.Req = req
f.meta.Res = res
return nil
} }
func (f *Fetcher) Create(res *base.Resource, opts *base.Options) (err error) { func (f *Fetcher) Create(opts *base.Options) (err error) {
f.opts = opts f.meta.Opts = opts
if len(f.opts.SelectFiles) == 0 { if len(opts.SelectFiles) == 0 {
f.opts.SelectFiles = make([]int, len(res.Files)) opts.SelectFiles = make([]int, 0)
for i := range f.opts.SelectFiles { for i := range f.torrent.Files() {
f.opts.SelectFiles[i] = i opts.SelectFiles = append(opts.SelectFiles, i)
} }
} }
if opts.Path != "" {
f.torrentPaths[f.meta.Res.Hash] = path.Join(f.meta.Opts.Path, f.meta.Res.RootDir)
}
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
f.ready.Store(false)
return nil return nil
} }
func (f *Fetcher) Start() (err error) { func (f *Fetcher) Start() (err error) {
if len(f.opts.SelectFiles) == 0 { if !f.ready.Load() {
if err = f.addTorrent(f.meta.Req.URL); err != nil {
return
}
}
files := f.torrent.Files()
if len(f.meta.Opts.SelectFiles) == len(files) {
f.torrent.DownloadAll() f.torrent.DownloadAll()
} else { } else {
for _, index := range f.opts.SelectFiles { for _, selectIndex := range f.meta.Opts.SelectFiles {
f.torrent.Files()[index].Download() file := files[selectIndex]
file.Download()
} }
} }
return return
} }
func (f *Fetcher) Pause() (err error) { func (f *Fetcher) Pause() (err error) {
f.torrent.DisallowDataDownload() f.ready.Store(false)
f.torrent.Drop()
return return
} }
func (f *Fetcher) Continue() (err error) { func (f *Fetcher) Continue() (err error) {
f.torrent.AllowDataDownload() return f.Start()
return }
func (f *Fetcher) Close() (err error) {
f.torrent.Drop()
return nil
}
func (f *Fetcher) Wait() (err error) {
for {
if f.ready.Load() {
done := true
for _, selectIndex := range f.meta.Opts.SelectFiles {
file := f.torrent.Files()[selectIndex]
if file.BytesCompleted() < file.Length() {
done = false
break
}
}
if done {
// remove unselected files
for i, file := range f.torrent.Files() {
selected := false
for _, selectIndex := range f.meta.Opts.SelectFiles {
if i == selectIndex {
selected = true
break
}
}
if !selected {
util.SafeRemove(filepath.Join(f.meta.Opts.Path, file.Path()))
}
}
break
}
}
time.Sleep(time.Millisecond * 500)
}
return nil
}
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
return f.meta
} }
func (f *Fetcher) Progress() fetcher.Progress { func (f *Fetcher) Progress() fetcher.Progress {
p := make(fetcher.Progress, len(f.opts.SelectFiles)) if !f.ready.Load() {
for i := range p { return f.progress
file := f.torrent.Files()[i]
p[i] = file.BytesCompleted()
} }
return p for i := range f.progress {
selectIndex := f.meta.Opts.SelectFiles[i]
file := f.torrent.Files()[selectIndex]
f.progress[i] = file.BytesCompleted()
}
return f.progress
}
func (f *Fetcher) addTorrent(url string) (err error) {
schema := util.ParseSchema(url)
if schema == "MAGNET" {
f.torrent, err = client.AddMagnet(url)
} else {
f.torrent, err = client.AddTorrentFromFile(url)
}
if err != nil {
return
}
var cfg config
exist, err := f.ctl.GetConfig(&cfg)
if err != nil {
return
}
if exist && len(cfg.Trackers) > 0 {
announceList := make([][]string, 0)
for _, tracker := range cfg.Trackers {
announceList = append(announceList, []string{tracker})
}
f.torrent.AddTrackers(announceList)
}
<-f.torrent.GotInfo()
f.ready.Store(true)
return
} }
type FetcherBuilder struct { type FetcherBuilder struct {
@@ -113,3 +232,15 @@ func (fb *FetcherBuilder) Schemes() []string {
func (fb *FetcherBuilder) Build() fetcher.Fetcher { func (fb *FetcherBuilder) Build() fetcher.Fetcher {
return &Fetcher{} return &Fetcher{}
} }
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
return nil, nil
}
func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
return nil, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
return &Fetcher{
meta: meta,
}
}
}
+38 -10
View File
@@ -1,16 +1,25 @@
package bt package bt
import ( import (
"github.com/monkeyWie/gopeed-core/internal/controller" "encoding/json"
"github.com/monkeyWie/gopeed-core/internal/fetcher" "github.com/GopeedLab/gopeed/internal/controller"
"github.com/monkeyWie/gopeed-core/pkg/base" "github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"reflect" "reflect"
"testing" "testing"
) )
func TestFetcher_Resolve_Torrent(t *testing.T) { func TestFetcher_Resolve_Torrent(t *testing.T) {
fetcher := buildFetcher() doResolve(t, buildFetcher())
res, err := fetcher.Resolve(&base.Request{ }
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", URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
}) })
if err != nil { if err != nil {
@@ -18,19 +27,20 @@ func TestFetcher_Resolve_Torrent(t *testing.T) {
} }
want := &base.Resource{ want := &base.Resource{
Req: res.Req, Name: "ubuntu-22.04-live-server-amd64.iso",
Length: 1466714112, Size: 1466714112,
Range: true, Range: true,
RootDir: "ubuntu-22.04-live-server-amd64.iso",
Files: []*base.FileInfo{ Files: []*base.FileInfo{
{ {
Name: "ubuntu-22.04-live-server-amd64.iso", Name: "ubuntu-22.04-live-server-amd64.iso",
Path: ".",
Size: 1466714112, Size: 1466714112,
}, },
}, },
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
} }
if !reflect.DeepEqual(want, res) { if !reflect.DeepEqual(want, fetcher.Meta().Res) {
t.Errorf("Resolve error = %v, want %v", res, want) t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
} }
} }
@@ -39,3 +49,21 @@ func buildFetcher() fetcher.Fetcher {
fetcher.Setup(controller.NewController()) fetcher.Setup(controller.NewController())
return fetcher return fetcher
} }
func buildConfigFetcher() fetcher.Fetcher {
fetcher := new(FetcherBuilder).Build()
newController := controller.NewController()
mockCfg := config{
Trackers: []string{
"udp://tracker.birkenwald.de:6969/announce",
"udp://tracker.bitsearch.to:1337/announce",
}}
newController.GetConfig = func(v any) (bool, error) {
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
return false, err
}
return true, nil
}
fetcher.Setup(newController)
return fetcher
}
+5
View File
@@ -0,0 +1,5 @@
package http
type config struct {
Connections int `json:"connections"`
}
+178 -102
View File
@@ -4,19 +4,19 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"github.com/monkeyWie/gopeed-core/internal/fetcher" "github.com/GopeedLab/gopeed/internal/controller"
"github.com/monkeyWie/gopeed-core/pkg/base" "github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"io" "io"
"io/ioutil"
"mime" "mime"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url" "net/url"
"os"
"path" "path"
"path/filepath"
"strconv" "strconv"
"strings"
"time" "time"
) )
@@ -33,36 +33,67 @@ func (re *RequestError) Error() string {
return fmt.Sprintf("http request fail,code:%d", re.Code) return fmt.Sprintf("http request fail,code:%d", re.Code)
} }
type chunk struct {
Status base.Status
Begin int64
End int64
Downloaded int64
}
func newChunk(begin int64, end int64) *chunk {
return &chunk{
Status: base.DownloadStatusReady,
Begin: begin,
End: end,
}
}
type Fetcher struct { type Fetcher struct {
*fetcher.DefaultFetcher ctl *controller.Controller
doneCh chan error
res *base.Resource meta *fetcher.FetcherMeta
opts *base.Options
status base.Status status base.Status
chunks []*Chunk chunks []*chunk
clients []*http.Response
file *os.File
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
pauseCh chan interface{} pauseCh chan interface{}
} }
func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) { func (f *Fetcher) Name() string {
return "http"
}
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 err
}
httpReq, err := buildRequest(nil, req) httpReq, err := buildRequest(nil, req)
if err != nil { if err != nil {
return nil, err return err
} }
client := buildClient() client := buildClient()
// 只访问一个字节,测试资源是否支持Range请求 // 只访问一个字节,测试资源是否支持Range请求
httpReq.Header.Set(base.HttpHeaderRange, fmt.Sprintf(base.HttpHeaderRangeFormat, 0, 0)) httpReq.Header.Set(base.HttpHeaderRange, fmt.Sprintf(base.HttpHeaderRangeFormat, 0, 0))
httpResp, err := client.Do(httpReq) httpResp, err := client.Do(httpReq)
if err != nil { if err != nil {
return nil, err return err
} }
// 拿到响应头就关闭,不用加defer // 拿到响应头就关闭,不用加defer
httpResp.Body.Close() httpResp.Body.Close()
res := &base.Resource{ res := &base.Resource{
Req: req,
Range: false, Range: false,
Files: []*base.FileInfo{}, Files: []*base.FileInfo{},
} }
@@ -74,9 +105,9 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
if contentTotal != "" { if contentTotal != "" {
parse, err := strconv.ParseInt(contentTotal, 10, 64) parse, err := strconv.ParseInt(contentTotal, 10, 64)
if err != nil { if err != nil {
return nil, err return err
} }
res.Length = parse res.Size = parse
} }
} else if base.HttpCodeOK == httpResp.StatusCode { } else if base.HttpCodeOK == httpResp.StatusCode {
// 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码 // 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码
@@ -84,97 +115,100 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
if contentLength != "" { if contentLength != "" {
parse, err := strconv.ParseInt(contentLength, 10, 64) parse, err := strconv.ParseInt(contentLength, 10, 64)
if err != nil { if err != nil {
return nil, err return err
} }
res.Length = parse res.Size = parse
} }
} else { } else {
return nil, NewRequestError(httpResp.StatusCode, httpResp.Status) return NewRequestError(httpResp.StatusCode, httpResp.Status)
} }
file := &base.FileInfo{ file := &base.FileInfo{
Size: res.Length, Size: res.Size,
} }
contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition) contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition)
if contentDisposition != "" { if contentDisposition != "" {
_, params, _ := mime.ParseMediaType(contentDisposition) _, params, _ := mime.ParseMediaType(contentDisposition)
filename := params["filename"] filename := params["filePath"]
if filename != "" { if filename != "" {
file.Name = filename file.Name = filename
} }
} }
// Get file filename by URL // Get file filePath by URL
if file.Name == "" && strings.Count(req.URL, "/") > 2 {
file.Name = filepath.Base(req.URL)
}
// unknown file filename
if file.Name == "" { if file.Name == "" {
file.Name = path.Base(httpReq.URL.Path)
}
// unknown file filePath
if file.Name == "" || file.Name == "/" || file.Name == "." {
file.Name = "unknown" file.Name = "unknown"
} }
res.Files = append(res.Files, file) res.Files = append(res.Files, file)
return res, nil res.Name = file.Name
f.meta.Req = req
f.meta.Res = res
return nil
} }
func (f *Fetcher) Create(res *base.Resource, opts *base.Options) error { func (f *Fetcher) Create(opts *base.Options) error {
f.res = res f.meta.Opts = opts
f.opts = opts
f.status = base.DownloadStatusReady f.status = base.DownloadStatusReady
if err := base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil {
return err
}
if err := base.ParseOptsExtra[fhttp.OptsExtra](f.meta.Opts); err != nil {
return err
}
if opts.Extra == nil {
opts.Extra = &fhttp.OptsExtra{}
}
extra := opts.Extra.(*fhttp.OptsExtra)
if extra.Connections == 0 {
var cfg config
exist, err := f.ctl.GetConfig(&cfg)
if err != nil {
return err
}
if exist {
extra.Connections = cfg.Connections
} else {
extra.Connections = 1
}
}
return nil return nil
} }
func (f *Fetcher) Start() (err error) { func (f *Fetcher) Start() (err error) {
// 创建文件 // 创建文件
name := f.filename() name := f.filepath()
_, err = f.Ctl.Touch(name, f.res.Length) f.file, err = f.ctl.Touch(name, f.meta.Res.Size)
if err != nil { if err != nil {
return err return err
} }
f.status = base.DownloadStatusStart f.status = base.DownloadStatusRunning
if f.res.Range { f.chunks = f.splitChunk()
// 每个连接平均需要下载的分块大小
chunkSize := f.res.Length / int64(f.opts.Connections)
f.chunks = make([]*Chunk, f.opts.Connections)
f.clients = make([]*http.Response, f.opts.Connections)
for i := 0; i < f.opts.Connections; i++ {
var (
begin = chunkSize * int64(i)
end int64
)
if i == f.opts.Connections-1 {
// 最后一个分块需要保证把文件下载完
end = f.res.Length - 1
} else {
end = begin + chunkSize - 1
}
chunk := NewChunk(begin, end)
f.chunks[i] = chunk
}
} else {
// 只支持单连接下载
f.chunks = make([]*Chunk, 1)
f.clients = make([]*http.Response, 1)
f.chunks[0] = NewChunk(0, 0)
}
f.fetch() f.fetch()
return return
} }
func (f *Fetcher) Pause() (err error) { func (f *Fetcher) Pause() (err error) {
if base.DownloadStatusStart != f.status { if base.DownloadStatusRunning != f.status {
return return
} }
f.status = base.DownloadStatusPause f.status = base.DownloadStatusPause
if f.cancel != nil {
f.cancel() f.cancel()
<-f.pauseCh <-f.pauseCh
}
return return
} }
func (f *Fetcher) Continue() (err error) { func (f *Fetcher) Continue() (err error) {
if base.DownloadStatusStart == f.status || base.DownloadStatusDone == f.status { if base.DownloadStatusRunning == f.status || base.DownloadStatusDone == f.status {
return return
} }
f.status = base.DownloadStatusStart f.status = base.DownloadStatusRunning
var name = f.filename() f.file, err = os.OpenFile(f.filepath(), os.O_RDWR, os.ModeAppend)
_, err = f.Ctl.Open(name)
if err != nil { if err != nil {
return err return err
} }
@@ -182,6 +216,17 @@ func (f *Fetcher) Continue() (err error) {
return return
} }
func (f *Fetcher) Close() (err error) {
if err = f.Pause(); err != nil {
return
}
return
}
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
return f.meta
}
func (f *Fetcher) Progress() fetcher.Progress { func (f *Fetcher) Progress() fetcher.Progress {
p := make(fetcher.Progress, 0) p := make(fetcher.Progress, 0)
if len(f.chunks) > 0 { if len(f.chunks) > 0 {
@@ -194,19 +239,18 @@ func (f *Fetcher) Progress() fetcher.Progress {
return p return p
} }
func (f *Fetcher) filename() string { func (f *Fetcher) Wait() (err error) {
// 创建文件 return <-f.doneCh
var filename = f.opts.Name }
if filename == "" {
filename = f.res.Files[0].Name func (f *Fetcher) filepath() string {
} return f.meta.Filepath(f.meta.Res.Files[0])
return filepath.Join(f.opts.Path, filename)
} }
func (f *Fetcher) fetch() { func (f *Fetcher) fetch() {
f.ctx, f.cancel = context.WithCancel(context.Background()) f.ctx, f.cancel = context.WithCancel(context.Background())
eg, _ := errgroup.WithContext(f.ctx) eg, _ := errgroup.WithContext(f.ctx)
for i := 0; i < f.opts.Connections; i++ { for i := 0; i < len(f.chunks); i++ {
i := i i := i
eg.Go(func() error { eg.Go(func() error {
return f.fetchChunk(i) return f.fetchChunk(i)
@@ -216,7 +260,7 @@ func (f *Fetcher) fetch() {
go func() { go func() {
err := eg.Wait() err := eg.Wait()
// 下载停止,关闭文件句柄 // 下载停止,关闭文件句柄
f.Ctl.Close(f.filename()) f.file.Close()
if f.status == base.DownloadStatusPause { if f.status == base.DownloadStatusPause {
f.pauseCh <- nil f.pauseCh <- nil
} else { } else {
@@ -225,16 +269,15 @@ func (f *Fetcher) fetch() {
} else { } else {
f.status = base.DownloadStatusDone f.status = base.DownloadStatusDone
} }
f.DoneCh <- err f.doneCh <- err
} }
}() }()
} }
func (f *Fetcher) fetchChunk(index int) (err error) { func (f *Fetcher) fetchChunk(index int) (err error) {
filename := f.filename()
chunk := f.chunks[index] chunk := f.chunks[index]
httpReq, err := buildRequest(f.ctx, f.res.Req) httpReq, err := buildRequest(f.ctx, f.meta.Req)
if err != nil { if err != nil {
return err return err
} }
@@ -256,7 +299,7 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
resp *http.Response resp *http.Response
retry bool retry bool
) )
if f.res.Range { if f.meta.Res.Range {
httpReq.Header.Set(base.HttpHeaderRange, httpReq.Header.Set(base.HttpHeaderRange,
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End)) fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
} else { } else {
@@ -267,7 +310,6 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
if err != nil { if err != nil {
return err return err
} }
f.clients[index] = resp
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent { if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
err = NewRequestError(resp.StatusCode, resp.Status) err = NewRequestError(resp.StatusCode, resp.Status)
return err return err
@@ -285,7 +327,7 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
for { for {
n, err := resp.Body.Read(buf) n, err := resp.Body.Read(buf)
if n > 0 { if n > 0 {
_, err := f.Ctl.Write(filename, chunk.Begin+chunk.Downloaded, buf[:n]) _, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -306,16 +348,45 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
} }
} }
if f.status == base.DownloadStatusPause {
chunk.Status = base.DownloadStatusPause
} else if chunk.Downloaded >= chunk.End-chunk.Begin+1 {
chunk.Status = base.DownloadStatusDone
} else {
if err != nil { if err != nil {
chunk.Status = base.DownloadStatusError chunk.Status = base.DownloadStatusError
} else { return
chunk.Status = base.DownloadStatusDone
} }
isComplete := f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1
if f.status == base.DownloadStatusPause && !isComplete {
chunk.Status = base.DownloadStatusPause
return
}
chunk.Status = base.DownloadStatusDone
return
}
func (f *Fetcher) splitChunk() (chunks []*chunk) {
if f.meta.Res.Range {
connections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
// 每个连接平均需要下载的分块大小
chunkSize := f.meta.Res.Size / int64(connections)
chunks = make([]*chunk, connections)
for i := 0; i < connections; i++ {
var (
begin = chunkSize * int64(i)
end int64
)
if i == connections-1 {
// 最后一个分块需要保证把文件下载完
end = f.meta.Res.Size - 1
} else {
end = begin + chunkSize - 1
}
chunk := newChunk(begin, end)
chunks[i] = chunk
}
} else {
// 只支持单连接下载
chunks = make([]*chunk, 1)
chunks[0] = newChunk(0, 0)
} }
return return
} }
@@ -343,7 +414,7 @@ func buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request
if req.Extra == nil { if req.Extra == nil {
method = http.MethodGet method = http.MethodGet
} else { } else {
extra := req.Extra.(Extra) extra := req.Extra.(*fhttp.ReqExtra)
if extra.Method != "" { if extra.Method != "" {
method = extra.Method method = extra.Method
} else { } else {
@@ -355,7 +426,7 @@ func buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request
} }
} }
if extra.Body != "" { if extra.Body != "" {
body = ioutil.NopCloser(bytes.NewBufferString(extra.Body)) body = bytes.NewBufferString(extra.Body)
} }
} }
@@ -371,8 +442,8 @@ func buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request
return httpReq, nil return httpReq, nil
} }
type FetcherData struct { type fetcherData struct {
Chunks []*Chunk Chunks []*chunk
} }
type FetcherBuilder struct { type FetcherBuilder struct {
@@ -385,25 +456,30 @@ func (fb *FetcherBuilder) Schemes() []string {
} }
func (fb *FetcherBuilder) Build() fetcher.Fetcher { func (fb *FetcherBuilder) Build() fetcher.Fetcher {
return &Fetcher{ return &Fetcher{}
DefaultFetcher: new(fetcher.DefaultFetcher),
pauseCh: make(chan interface{}),
}
} }
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) interface{} { func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
_f := f.(*Fetcher) _f := f.(*Fetcher)
return &FetcherData{ return &fetcherData{
Chunks: _f.chunks, Chunks: _f.chunks,
} }, nil
} }
func (fb *FetcherBuilder) Resume(res *base.Resource, opts *base.Options, data interface{}) fetcher.Fetcher { func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
fetcher := new(FetcherBuilder).Build().(*Fetcher) return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
fs := data.(*FetcherData) fd := v.(*fetcherData)
fetcher.res = res fb := &FetcherBuilder{}
fetcher.opts = opts fetcher := fb.Build().(*Fetcher)
fetcher.chunks = fs.Chunks fetcher.meta = meta
fetcher.status = base.DownloadStatusPause fetcher.status = base.DownloadStatusPause
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
if len(fd.Chunks) == 0 {
fetcher.chunks = fetcher.splitChunk()
} else {
fetcher.chunks = fd.Chunks
}
return fetcher return fetcher
}
} }
+131 -47
View File
@@ -1,10 +1,13 @@
package http package http
import ( import (
"github.com/monkeyWie/gopeed-core/internal/controller" "encoding/json"
"github.com/monkeyWie/gopeed-core/internal/fetcher" "fmt"
"github.com/monkeyWie/gopeed-core/pkg/base" "github.com/GopeedLab/gopeed/internal/controller"
"github.com/monkeyWie/gopeed-core/pkg/test" "github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"net" "net"
"reflect" "reflect"
"testing" "testing"
@@ -13,7 +16,8 @@ import (
func TestFetcher_Resolve(t *testing.T) { func TestFetcher_Resolve(t *testing.T) {
testResolve(test.StartTestFileServer, &base.Resource{ testResolve(test.StartTestFileServer, &base.Resource{
Length: test.BuildSize, Name: test.BuildName,
Size: test.BuildSize,
Range: true, Range: true,
Files: []*base.FileInfo{ Files: []*base.FileInfo{
{ {
@@ -23,7 +27,8 @@ func TestFetcher_Resolve(t *testing.T) {
}, },
}, t) }, t)
testResolve(test.StartTestChunkedServer, &base.Resource{ testResolve(test.StartTestChunkedServer, &base.Resource{
Length: 0, Name: test.BuildName,
Size: 0,
Range: false, Range: false,
Files: []*base.FileInfo{ Files: []*base.FileInfo{
{ {
@@ -38,15 +43,14 @@ func testResolve(startTestServer func() net.Listener, want *base.Resource, t *te
listener := startTestServer() listener := startTestServer()
defer listener.Close() defer listener.Close()
fetcher := buildFetcher() fetcher := buildFetcher()
res, err := fetcher.Resolve(&base.Request{ err := fetcher.Resolve(&base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName, URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
res.Req = nil if !reflect.DeepEqual(want, fetcher.meta.Res) {
if !reflect.DeepEqual(want, res) { t.Errorf("Resolve() got = %v, want %v", fetcher.meta.Res, want)
t.Errorf("Resolve error = %v, want %v", res, want)
} }
} }
@@ -75,7 +79,14 @@ func TestFetcher_DownloadChunked(t *testing.T) {
defer listener.Close() defer listener.Close()
// chunked编码下载 // chunked编码下载
downloadNormal(listener, 1, t) downloadNormal(listener, 1, t)
downloadContinue(listener, 1, t) downloadNormal(listener, 2, t)
}
func TestFetcher_DownloadPost(t *testing.T) {
listener := test.StartTestPostServer()
defer listener.Close()
// post下载
downloadPost(listener, 1, t)
} }
func TestFetcher_DownloadRetry(t *testing.T) { func TestFetcher_DownloadRetry(t *testing.T) {
@@ -102,28 +113,8 @@ func TestFetcher_DownloadResume(t *testing.T) {
downloadResume(listener, 16, t) downloadResume(listener, 16, t)
} }
func downloadReady(listener net.Listener, connections int, t *testing.T) (fetcher fetcher.Fetcher, res *base.Resource, opts *base.Options) { func TestFetcher_Config(t *testing.T) {
fetcher = buildFetcher() fetcher := doDownloadReady(buildConfigFetcher(), test.StartTestFileServer(), 0, t)
res, err := fetcher.Resolve(&base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
})
if err != nil {
t.Fatal(err)
}
opts = &base.Options{
Name: test.DownloadName,
Path: test.Dir,
Connections: connections,
}
err = fetcher.Create(res, opts)
if err != nil {
t.Fatal(err)
}
return
}
func downloadNormal(listener net.Listener, connections int, t *testing.T) {
fetcher, _, _ := downloadReady(listener, connections, t)
err := fetcher.Start() err := fetcher.Start()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -135,21 +126,91 @@ func downloadNormal(listener net.Listener, connections int, t *testing.T) {
want := test.FileMd5(test.BuildFile) want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile) got := test.FileMd5(test.DownloadFile)
if want != got { if want != got {
t.Errorf("Download error = %v, want %v", got, want) t.Errorf("Download() got = %v, want %v", got, want)
} }
} }
func downloadContinue(listener net.Listener, connections int, t *testing.T) { func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
fetcher, _, _ := downloadReady(listener, connections, t) return doDownloadReady(buildFetcher(), listener, connections, t)
}
func doDownloadReady(f fetcher.Fetcher, listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
err := f.Resolve(&base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
})
if err != nil {
t.Fatal(err)
}
var extra any = nil
if connections > 0 {
extra = http.OptsExtra{
Connections: connections,
}
}
opts := &base.Options{
Name: test.DownloadName,
Path: test.Dir,
Extra: extra,
}
err = f.Create(opts)
if err != nil {
t.Fatal(err)
}
return f
}
func downloadNormal(listener net.Listener, connections int, t *testing.T) {
fetcher := downloadReady(listener, connections, t)
err := fetcher.Start() err := fetcher.Start()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
time.Sleep(time.Millisecond * 200) err = fetcher.Wait()
if err != nil {
t.Fatal(err)
}
want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile)
if want != got {
t.Errorf("Download() got = %v, want %v", got, want)
}
}
func downloadPost(listener net.Listener, connections int, t *testing.T) {
fetcher := downloadReady(listener, connections, t)
fetcher.Meta().Req.Extra = &http.ReqExtra{
Method: "POST",
Header: map[string]string{
"Authorization": "Bearer 123456",
},
Body: fmt.Sprintf(`{"name":"%s"}`, test.BuildName),
}
err := fetcher.Start()
if err != nil {
t.Fatal(err)
}
err = fetcher.Wait()
if err != nil {
t.Fatal(err)
}
want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile)
if want != got {
t.Errorf("Download() got = %v, want %v", got, want)
}
}
func downloadContinue(listener net.Listener, connections int, t *testing.T) {
fetcher := downloadReady(listener, connections, t)
err := fetcher.Start()
if err != nil {
t.Fatal(err)
}
time.Sleep(time.Millisecond * 50)
if err := fetcher.Pause(); err != nil { if err := fetcher.Pause(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
time.Sleep(time.Millisecond * 200) time.Sleep(time.Millisecond * 50)
if err := fetcher.Continue(); err != nil { if err := fetcher.Continue(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -160,36 +221,43 @@ func downloadContinue(listener net.Listener, connections int, t *testing.T) {
want := test.FileMd5(test.BuildFile) want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile) got := test.FileMd5(test.DownloadFile)
if want != got { if want != got {
t.Errorf("Download error = %v, want %v", got, want) t.Errorf("Download() got = %v, want %v", got, want)
} }
} }
func downloadError(listener net.Listener, connections int, t *testing.T) { func downloadError(listener net.Listener, connections int, t *testing.T) {
fetcher, _, _ := downloadReady(listener, connections, t) fetcher := downloadReady(listener, connections, t)
err := fetcher.Start() err := fetcher.Start()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
err = fetcher.Wait() err = fetcher.Wait()
if err == nil { if err == nil {
t.Errorf("Download error = %v, want %v", err, nil) t.Errorf("Download() got = %v, want %v", err, nil)
} }
} }
func downloadResume(listener net.Listener, connections int, t *testing.T) { func downloadResume(listener net.Listener, connections int, t *testing.T) {
fetcher, res, opts := downloadReady(listener, connections, t) fetcher := downloadReady(listener, connections, t)
err := fetcher.Start() err := fetcher.Start()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
fb := new(FetcherBuilder) fb := new(FetcherBuilder)
time.Sleep(time.Second * 2) time.Sleep(time.Millisecond * 50)
data := fb.Store(fetcher) data, err := fb.Store(fetcher)
time.Sleep(time.Second * 1) if err != nil {
t.Fatal(err)
}
time.Sleep(time.Millisecond * 50)
fetcher.Pause() fetcher.Pause()
fetcher = fb.Resume(res, opts, data) _, f := fb.Restore()
f(fetcher.Meta(), data)
if err != nil {
t.Fatal(err)
}
fetcher.Setup(controller.NewController()) fetcher.Setup(controller.NewController())
fetcher.Continue() fetcher.Continue()
@@ -200,7 +268,7 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
want := test.FileMd5(test.BuildFile) want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile) got := test.FileMd5(test.DownloadFile)
if want != got { if want != got {
t.Errorf("Download error = %v, want %v", got, want) t.Errorf("Download() got = %v, want %v", got, want)
} }
} }
@@ -209,3 +277,19 @@ func buildFetcher() *Fetcher {
fetcher.Setup(controller.NewController()) fetcher.Setup(controller.NewController())
return fetcher.(*Fetcher) return fetcher.(*Fetcher)
} }
func buildConfigFetcher() fetcher.Fetcher {
fetcher := new(FetcherBuilder).Build()
newController := controller.NewController()
mockCfg := config{
Connections: 16,
}
newController.GetConfig = func(v any) (bool, error) {
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
return false, err
}
return true, nil
}
fetcher.Setup(newController)
return fetcher
}
-24
View File
@@ -1,24 +0,0 @@
package http
import "github.com/monkeyWie/gopeed-core/pkg/base"
type Chunk struct {
Status base.Status
Begin int64
End int64
Downloaded int64
}
func NewChunk(begin int64, end int64) *Chunk {
return &Chunk{
Status: base.DownloadStatusReady,
Begin: begin,
End: end,
}
}
type Extra struct {
Method string
Header map[string]string
Body string
}
@@ -1,12 +1,14 @@
package test package test
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"math/rand" "math/rand"
"net" "net"
"net/http" "net/http"
"os" "os"
"time"
) )
const ( const (
@@ -25,6 +27,25 @@ func StartTestFileServer() net.Listener {
}) })
} }
type SlowFileServer struct {
delay time.Duration
handler http.Handler
}
func (s *SlowFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
time.Sleep(s.delay)
s.handler.ServeHTTP(w, r)
}
func StartTestSlowFileServer(delay time.Duration) net.Listener {
return startTestServer(func() http.Handler {
return &SlowFileServer{
delay: delay,
handler: http.FileServer(http.Dir(Dir)),
}
})
}
func StartTestChunkedServer() net.Listener { func StartTestChunkedServer() net.Listener {
return startTestServer(func() http.Handler { return startTestServer(func() http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
@@ -61,6 +82,29 @@ func StartTestRetryServer() net.Listener {
}) })
} }
func StartTestPostServer() net.Listener {
return startTestServer(func() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
if request.Method == "POST" && request.Header.Get("Authorization") != "" {
var data map[string]interface{}
if err := json.NewDecoder(request.Body).Decode(&data); err != nil {
panic(err)
}
if data["name"] == BuildName {
file, err := os.Open(BuildFile)
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(writer, file)
}
}
})
return mux
})
}
func StartTestErrorServer() net.Listener { func StartTestErrorServer() net.Listener {
counter := 0 counter := 0
return startTestServer(func() http.Handler { return startTestServer(func() http.Handler {
@@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"os" "os"
"path/filepath"
) )
func FileMd5(filePath string) string { func FileMd5(filePath string) string {
@@ -26,7 +27,29 @@ func FileMd5(filePath string) string {
return hex.EncodeToString(hash.Sum(nil)) return hex.EncodeToString(hash.Sum(nil))
} }
func DirMd5(dirPath string) string {
hash := md5.New()
filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
if _, err := io.Copy(hash, file); err != nil {
return err
}
return nil
})
return hex.EncodeToString(hash.Sum(nil))
}
func ToJson(v interface{}) string { func ToJson(v interface{}) string {
buf, _ := json.Marshal(v) buf, _ := json.Marshal(v)
return string(buf) return string(buf)
} }
func JsonEqual(v1 any, v2 any) bool {
return ToJson(v1) == ToJson(v2)
}
+7 -13
View File
@@ -1,16 +1,14 @@
package base package base
import "errors" type Status string
type Status int
const ( const (
DownloadStatusPrepare Status = iota DownloadStatusReady Status = "ready" // task create but not start
DownloadStatusReady DownloadStatusRunning Status = "running"
DownloadStatusStart DownloadStatusPause Status = "pause"
DownloadStatusPause DownloadStatusWait Status = "wait" // task is wait for running
DownloadStatusError DownloadStatusError Status = "error"
DownloadStatusDone DownloadStatusDone Status = "done"
) )
const ( const (
@@ -24,7 +22,3 @@ const (
HttpHeaderRangeFormat = "bytes=%d-%d" HttpHeaderRangeFormat = "bytes=%d-%d"
) )
var (
DeleteErr = errors.New("delete")
)
+8
View File
@@ -0,0 +1,8 @@
package base
import "errors"
var (
NotFound = errors.New("not found")
BadParams = errors.New("bad params")
)
+58 -27
View File
@@ -1,40 +1,71 @@
package base package base
// 下载请求 import (
"github.com/GopeedLab/gopeed/pkg/util"
)
// Request download request
type Request struct { type Request struct {
// 下载链接 URL string `json:"url"`
Schema string Extra any `json:"extra"`
// 下载链接
URL string
// 附加信息
Extra interface{}
} }
// 资源信息 // Resource download resource
type Resource struct { type Resource struct {
Req *Request Name string `json:"name"`
// 资源总大小 Size int64 `json:"size"`
Length int64 // is support range download
// 是否支持断点下载 Range bool `json:"range"`
Range bool RootDir string `json:"rootDir"`
// 资源所包含的文件列表 // file list
Files []*FileInfo Files []*FileInfo `json:"files"`
Hash string `json:"hash"`
} }
type FileInfo struct { type FileInfo struct {
Name string Name string `json:"name"`
Path string Path string `json:"path"`
Size int64 Size int64 `json:"size"`
} }
// 下载选项 // Options for download
type Options struct { type Options struct {
// 保存文件名 // Download file name
Name string Name string `json:"name"`
// 保存目录 // Download file path
Path string Path string `json:"path"`
// 并发连接数 // Select file indexes to download
Connections int SelectFiles []int `json:"selectFiles"`
// 下载的文件下标列表 // Extra info for specific fetcher
SelectFiles []int Extra any `json:"extra"`
}
func ParseReqExtra[E any](req *Request) error {
if req.Extra == nil {
return nil
}
if _, ok := req.Extra.(*E); ok {
return nil
}
var t E
if err := util.MapToStruct(req.Extra, &t); err != nil {
return err
}
req.Extra = &t
return nil
}
func ParseOptsExtra[E any](opts *Options) error {
if opts.Extra == nil {
return nil
}
if _, ok := opts.Extra.(*E); ok {
return nil
}
var t E
if err := util.MapToStruct(opts.Extra, &t); err != nil {
return err
}
opts.Extra = &t
return nil
} }
+575 -108
View File
File diff suppressed because it is too large. Load diff
+143 -13
View File
@@ -1,29 +1,34 @@
package download package download
import ( import (
"github.com/monkeyWie/gopeed-core/internal/protocol/http" "github.com/GopeedLab/gopeed/internal/test"
"github.com/monkeyWie/gopeed-core/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/monkeyWie/gopeed-core/pkg/test" "github.com/GopeedLab/gopeed/pkg/protocol/http"
"reflect" "reflect"
"sync" "sync"
"testing" "testing"
"time"
) )
func TestDownloader_Resolve(t *testing.T) { func TestDownloader_Resolve(t *testing.T) {
listener := test.StartTestFileServer() listener := test.StartTestFileServer()
defer listener.Close() defer listener.Close()
downloader := NewDownloader(new(http.FetcherBuilder)) downloader := NewDownloader(nil)
if err := downloader.Setup(); err != nil {
t.Fatal(err)
}
defer downloader.Clear()
req := &base.Request{ req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName, URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
} }
_, res, err := downloader.Resolve(req) rr, err := downloader.Resolve(req)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
want := &base.Resource{ want := &base.Resource{
Req: req, Name: test.BuildName,
Length: test.BuildSize, Size: test.BuildSize,
Range: true, Range: true,
Files: []*base.FileInfo{ Files: []*base.FileInfo{
{ {
@@ -33,8 +38,8 @@ func TestDownloader_Resolve(t *testing.T) {
}, },
}, },
} }
if !reflect.DeepEqual(res, want) { if !reflect.DeepEqual(want, rr.Res) {
t.Errorf("Resolve error = %s, want %s", test.ToJson(res), test.ToJson(want)) t.Errorf("Resolve() got = %v, want %v", rr.Res, want)
} }
} }
@@ -42,11 +47,15 @@ func TestDownloader_Create(t *testing.T) {
listener := test.StartTestFileServer() listener := test.StartTestFileServer()
defer listener.Close() defer listener.Close()
downloader := NewDownloader(new(http.FetcherBuilder)) downloader := NewDownloader(nil)
if err := downloader.Setup(); err != nil {
t.Fatal(err)
}
defer downloader.Clear()
req := &base.Request{ req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName, URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
} }
taskID, res, err := downloader.Resolve(req) rr, err := downloader.Resolve(req)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -58,10 +67,12 @@ func TestDownloader_Create(t *testing.T) {
wg.Done() wg.Done()
} }
}) })
err = downloader.Create(taskID, res, &base.Options{ _, err = downloader.Create(rr.ID, &base.Options{
Path: test.Dir, Path: test.Dir,
Name: test.DownloadName, Name: test.DownloadName,
Extra: http.OptsExtra{
Connections: 4, Connections: 4,
},
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -70,6 +81,125 @@ func TestDownloader_Create(t *testing.T) {
want := test.FileMd5(test.BuildFile) want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile) got := test.FileMd5(test.DownloadFile)
if want != got { if want != got {
t.Errorf("Download error = %v, want %v", got, want) t.Errorf("Download() got = %v, want %v", got, want)
}
}
func TestDownloader_StoreAndRestore(t *testing.T) {
listener := test.StartTestSlowFileServer(time.Millisecond * 2000)
defer listener.Close()
downloader := NewDownloader(&DownloaderConfig{
Storage: NewBoltStorage("./"),
})
if err := downloader.Setup(); err != nil {
t.Fatal(err)
}
defer downloader.Clear()
req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
}
rr, err := downloader.Resolve(req)
if err != nil {
t.Fatal(err)
}
id, err := downloader.Create(rr.ID, &base.Options{
Path: test.Dir,
Name: test.DownloadName,
Extra: http.OptsExtra{
Connections: 4,
},
})
if err != nil {
t.Fatal(err)
}
time.Sleep(time.Millisecond * 1001)
err = downloader.Pause(id)
if err != nil {
t.Fatal(err)
}
downloader.Close()
downloader = NewDownloader(&DownloaderConfig{
Storage: NewBoltStorage("./"),
})
if err := downloader.Setup(); err != nil {
t.Fatal(err)
}
task := downloader.GetTask(id)
if task == nil {
t.Fatal("task is nil")
}
var wg sync.WaitGroup
wg.Add(1)
downloader.Listener(func(event *Event) {
if event.Key == EventKeyDone {
wg.Done()
}
})
err = downloader.Continue(id)
wg.Wait()
if err != nil {
t.Fatal(err)
}
want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile)
if want != got {
t.Errorf("StoreAndResume() got = %v, want %v", got, want)
}
downloader.Clear()
}
func TestDownloader_Protocol_Config(t *testing.T) {
downloader := NewDownloader(nil)
if err := downloader.Setup(); err != nil {
t.Fatal(err)
}
defer downloader.Clear()
var httpCfg map[string]any
exits, err := downloader.getProtocolConfig("http", &httpCfg)
if err != nil {
t.Fatal(err)
}
if exits {
t.Errorf("getProtocolConfig() got = %v, want %v", exits, false)
}
storeCfg := &DownloaderStoreConfig{
DownloadDir: "./downloads",
ProtocolConfig: map[string]any{
"http": map[string]any{
"connections": 4,
},
"bt": map[string]any{
"trackerSubscribeUrls": []string{
"https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/best.txt",
},
"trackers": []string{
"udp://tracker.coppersurfer.tk:6969/announce",
"udp://tracker.leechers-paradise.org:6969/announce",
},
},
},
Extra: map[string]any{
"theme": "dark",
},
}
if err = downloader.PutConfig(storeCfg); err != nil {
t.Fatal(err)
}
newStoreCfg, err := downloader.GetConfig()
if err != nil {
t.Fatal(err)
}
if !test.JsonEqual(storeCfg, newStoreCfg) {
t.Errorf("GetConfig() got = %v, want %v", test.ToJson(storeCfg), test.ToJson(newStoreCfg))
} }
} }
+1
View File
@@ -8,6 +8,7 @@ const (
EventKeyContinue = "continue" EventKeyContinue = "continue"
EventKeyProgress = "progress" EventKeyProgress = "progress"
EventKeyError = "error" EventKeyError = "error"
EventKeyDelete = "delete"
EventKeyDone = "done" EventKeyDone = "done"
EventKeyFinally = "finally" EventKeyFinally = "finally"
) )
+114
View File
@@ -0,0 +1,114 @@
package download
import (
"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
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
Storage Storage
*DownloaderStoreConfig
}
func (cfg *DownloaderConfig) Init() *DownloaderConfig {
if cfg.Controller == nil {
cfg.Controller = controller.NewController()
}
if len(cfg.FetchBuilders) == 0 {
cfg.FetchBuilders = []fetcher.FetcherBuilder{
new(http.FetcherBuilder),
new(bt.FetcherBuilder),
}
}
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 {
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
}
+228
View File
@@ -0,0 +1,228 @@
package download
import (
"encoding/json"
"go.etcd.io/bbolt"
"os"
"path/filepath"
"reflect"
)
type Storage interface {
Setup(buckets []string) error
Put(bucket string, key string, v any) error
Get(bucket string, key string, v any) (bool, error)
List(bucket string, v any) error
Pop(bucket string, key string, v any) error
Delete(bucket string, key string) error
Close() error
Clear() error
}
func changeValue(p any, v any) {
if v == nil {
return
}
rp := reflect.ValueOf(p)
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Slice {
if rv.Len() == 0 {
return
}
// get underlying type
tp := reflect.TypeOf(p).Elem().Elem()
for i := 0; i < rv.Len(); i++ {
// convert to underlying type
vv := rv.Index(i).Elem().Convert(tp)
rp.Elem().Set(reflect.Append(rp.Elem(), vv))
}
} else if rv.Kind() == reflect.Ptr {
rp.Elem().Set(rv.Elem())
} else {
rp.Elem().Set(rv)
}
}
var memData = make(map[string]map[string]any)
type MemStorage struct {
}
func NewMemStorage() *MemStorage {
return &MemStorage{}
}
func (n *MemStorage) Setup(buckets []string) error {
for _, bucket := range buckets {
if _, ok := memData[bucket]; !ok {
memData[bucket] = make(map[string]any)
}
}
return nil
}
func (n *MemStorage) Put(bucket string, key string, v any) error {
memData[bucket][key] = v
return nil
}
func (n *MemStorage) Get(bucket string, key string, v any) (bool, error) {
if dv, ok := memData[bucket][key]; ok {
changeValue(v, dv)
return true, nil
}
return false, nil
}
func (n *MemStorage) List(bucket string, v any) error {
data := memData[bucket]
list := make([]any, 0)
for _, v := range data {
list = append(list, v)
}
changeValue(v, list)
return nil
}
func (n *MemStorage) Pop(bucket string, key string, v any) error {
data := memData[bucket]
changeValue(v, data[key])
delete(data, key)
return nil
}
func (n *MemStorage) Delete(bucket string, key string) error {
delete(memData[bucket], key)
return nil
}
func (n *MemStorage) Close() error {
return nil
}
func (n *MemStorage) Clear() error {
memData = make(map[string]map[string]any)
return nil
}
const (
dbFile = "gopeed.db"
)
type BoltStorage struct {
db *bbolt.DB
path string
}
func NewBoltStorage(dir string) *BoltStorage {
path := filepath.Join(dir, dbFile)
db, err := bbolt.Open(path, 0600, nil)
if err != nil {
panic(err)
}
return &BoltStorage{
db: db,
path: path,
}
}
func (b *BoltStorage) Setup(buckets []string) error {
return b.db.Update(func(tx *bbolt.Tx) error {
for _, bucket := range buckets {
_, err := tx.CreateBucketIfNotExists([]byte(bucket))
if err != nil {
return err
}
}
return nil
})
}
func (b *BoltStorage) Put(bucket string, key string, v any) error {
buf, err := json.Marshal(v)
if err != nil {
return err
}
return b.db.Update(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte(bucket))
return b.Put([]byte(key), buf)
})
}
func (b *BoltStorage) Get(bucket string, key string, v any) (bool, error) {
var data []byte
err := b.db.View(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte(bucket))
data = b.Get([]byte(key))
return nil
})
if err != nil {
return false, err
}
if data == nil {
return false, nil
}
if err := json.Unmarshal(data, v); err != nil {
return false, err
}
return true, nil
}
func (b *BoltStorage) List(bucket string, v any) error {
list := make([]any, 0)
tv := reflect.TypeOf(v).Elem().Elem()
if err := b.db.View(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte(bucket))
if err := b.ForEach(func(k, v []byte) error {
data := reflect.New(tv.Elem()).Interface()
if err := json.Unmarshal(v, &data); err != nil {
return err
}
list = append(list, data)
return nil
}); err != nil {
return err
}
return nil
}); err != nil {
return err
}
changeValue(v, list)
return nil
}
func (b *BoltStorage) Pop(bucket string, key string, v any) error {
var data []byte
err := b.db.Update(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte(bucket))
kb := []byte(key)
data = b.Get(kb)
return b.Delete(kb)
})
if err != nil {
return err
}
return json.Unmarshal(data, v)
}
func (b *BoltStorage) Delete(bucket string, key string) error {
return b.db.Update(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte(bucket))
return b.Delete([]byte(key))
})
}
func (b *BoltStorage) Close() error {
return b.db.Close()
}
func (b *BoltStorage) Clear() error {
if err := b.Close(); err != nil {
return err
}
if err := os.Remove(b.path); err != nil {
return err
}
return nil
}
-29
View File
@@ -1,29 +0,0 @@
package download
import (
"github.com/google/uuid"
"github.com/monkeyWie/gopeed-core/internal/fetcher"
"github.com/monkeyWie/gopeed-core/pkg/base"
"github.com/monkeyWie/gopeed-core/pkg/util"
"sync"
)
type Task struct {
ID string
Res *base.Resource
Opts *base.Options
Status base.Status
Files map[string]*base.FileInfo
Progress *Progress
fetcher fetcher.Fetcher
timer *util.Timer
locker *sync.Mutex
}
func NewTask() *Task {
return &Task{
ID: uuid.New().String(),
Status: base.DownloadStatusPrepare,
}
}
Loaded 50 of 271 files, more files were not shown because too many files have changed in this diff. Show more