Compare commits

...

652 Commits

Author SHA1 Message Date
xiaoyaofenfen d05d19acd8 Merge pull request #548 from qinweixian/patch-1
修复后台上传脚本bug
2026-09-07 09:29:06 +08:00
秦伟先 5179f914ed 修复后台上传脚本bug 2026-09-03 10:42:52 +08:00
tickstep 2feaa3cf82 update readme file 2026-07-05 09:42:32 +08:00
tickstep studio team cedd43d402 update version to v0.4.0 2026-06-29 10:42:16 +08:00
tickstep studio team e62bdbb9f1 refractor OSS url parser 2026-06-29 10:27:00 +08:00
tickstep studio team acc408f0a0 fix download url expired calc error issue #544 #543 #542 2026-06-29 09:29:05 +08:00
tickstep studio team db28ea6732 update version to v0.3.9 2026-04-20 19:41:14 +08:00
tickstep studio team a9bc1076a5 update aliyunpan-api to v0.2.9 2026-04-20 19:12:16 +08:00
tickstep e20ea479c4 fix update config file error 2026-04-12 08:32:26 +08:00
tickstep studio team 42f044ba38 update readme file 2026-03-02 11:17:31 +08:00
tickstep studio team 88f1ff2688 update manual file 2026-03-02 11:05:41 +08:00
feny ca09039e48 fix the issue that -ui print output flickering 2026-02-18 08:24:10 +08:00
tickstep studio team cf38ad51fd add -ui option for upload 2026-02-13 17:27:50 +08:00
tickstep studio team 30cbced50c rename download_dashboard to dashboard_panel 2026-02-13 17:23:09 +08:00
tickstep studio team 0946939325 update manual file 2026-02-12 11:03:58 +08:00
tickstep studio team da13b046b8 update readme file 2026-02-12 10:56:37 +08:00
tickstep studio team 268e4a30d1 update readme file 2026-02-12 10:51:48 +08:00
tickstep studio team a0ca0f8089 update readme file 2026-02-12 10:44:34 +08:00
tickstep studio team 2d99a39606 Merge branch 'main' of github.com:tickstep/aliyunpan 2026-02-12 10:35:25 +08:00
tickstep 2965179ae8 update version to v0.3.8 2026-02-11 20:23:40 +08:00
tickstep 0565482121 update golang.org/x/sys to v0.15.0 2026-02-11 20:21:14 +08:00
tickstep 1822d3c2b2 Merge branch 'main' of github.com:tickstep/aliyunpan 2026-02-11 19:33:53 +08:00
tickstep 6672aa4cd4 update aliyunpan-api to v0.2.8 2026-02-11 19:33:37 +08:00
tickstep studio team c69a27c9b1 add verbose log for upload 2026-02-10 15:34:07 +08:00
tickstep studio team 4feacbcdbf fix login get ip error issue 2026-02-10 15:33:49 +08:00
tickstep studio team 1916dda8f8 fix update issue 2026-02-10 14:52:34 +08:00
tickstep studio team 8b5eb14eb6 add more log to track download url expired issue #501 2026-02-05 20:47:19 +08:00
tickstep studio team d3d9690d55 beautify download ui dashboard 2026-02-05 20:14:56 +08:00
tickstep studio team 89a6ef326c beautify download ui dashboard 2026-02-05 17:31:21 +08:00
tickstep studio team 35dfb35429 handle file already closed error for upload #509 #512 2026-02-04 17:46:11 +08:00
tickstep studio team 3c7ebb5f62 optimize rm command, faster remove file without wildcard #530 2026-02-04 11:00:38 +08:00
tickstep f796f40664 optimize update logic 2026-02-03 21:25:21 +08:00
tickstep studio team 0d9700c84c fix the error in the total download size statistics 2026-02-03 16:41:40 +08:00
tickstep studio team 09ee162ba4 add slice parallel option for downoad #529 2026-02-03 16:30:36 +08:00
tickstep studio team ff0e3dbfa9 optimize downoad ui dashboard 2026-02-03 15:52:06 +08:00
tickstep studio team 4c5f45e5ab delete useless code 2026-02-03 15:02:20 +08:00
tickstep studio team 02326b0bd7 fix terminal abnormalities 2026-02-03 14:40:04 +08:00
tickstep 350fec4275 add -ui option for download 2026-02-03 11:23:57 +08:00
xiaoyaofenfen 70a14dd1a6 Merge pull request #525 from SpookySandwich/main
【功能建议】改善下载界面
2026-02-02 18:30:37 +08:00
CharliePu 23dad72914 improve download tui 2026-01-06 17:50:41 +08:00
xiaoyaofenfen 9fa547ddc8 Merge pull request #520 from sikaisky/main
fix uploadEntity.PartInfoList len 0 error
2025-11-25 14:54:25 +08:00
sikai 2898037ec5 fix uploadEntity.PartInfoList len 0 error 2025-11-09 10:26:19 +08:00
tickstep 70c14fd66f open file with readonly mode 2025-08-11 11:17:20 +08:00
tickstep 8913597716 optimize login routine logic 2025-08-07 17:43:14 +08:00
tickstep 9b276f2caf optimize token refresh task 2025-08-06 15:34:36 +08:00
tickstep e8bda159fa handle api common error 2025-08-05 23:08:24 +08:00
tickstep e1ac6a95b9 fix share url error 2025-04-15 21:47:00 +08:00
tickstep cef9c6c7c2 update version to v0.3.7 2025-03-23 20:51:06 +08:00
tickstep 17e8e182ee fix word spell error 2025-03-23 20:33:03 +08:00
tickstep 4b9ded5b9e add resource dirive id for JS plugin context 2025-03-23 20:22:37 +08:00
tickstep f939f515f4 add download JS plugin sample #40 2025-03-23 20:03:57 +08:00
tickstep 0e0486914c sort by file name for download file list 2025-03-23 14:18:04 +08:00
tickstep a86a464e67 add downloadActionId for download JS plugin 2025-03-22 15:59:47 +08:00
tickstep c932e3a231 add md5 hash function for JS plugin 2025-03-22 15:32:10 +08:00
tickstep f79645bfa7 add friendly prompt for not found file path 2025-03-22 15:17:47 +08:00
tickstep 7786dc6c40 add file path auto complete for local command 2025-03-21 21:33:10 +08:00
tickstep d7b4d0c846 support local file path auto completer for lls / lcd command 2025-03-19 21:31:00 +08:00
tickstep e669063be9 fix path completer empty match issue 2025-03-19 17:33:55 +08:00
tickstep e63ba4478c refactor pan file path auto completer function 2025-03-19 17:22:04 +08:00
tickstep 23617d810b add prompt for adrive third vip #493 2025-03-19 15:31:39 +08:00
tickstep 3d87c6244f fix local windows path recognize error 2025-03-08 10:14:12 +08:00
tickstep 4f7941caf5 adapt local file path for windows 2025-03-07 23:22:17 +08:00
tickstep 095d06d99c add sort option for lls command 2025-03-07 22:49:54 +08:00
tickstep 7fbe082e58 add lcd lpwd lls command #98 2025-03-07 16:19:54 +08:00
tickstep 34a6242bde update manual 2025-03-07 10:50:01 +08:00
tickstep 71df114d8a tidy the accept complete file name list config 2025-03-07 10:03:52 +08:00
tickstep 622984ee26 update manual 2025-03-06 11:20:02 +08:00
tickstep 04ec8658e4 update plugin manual 2025-03-06 11:15:16 +08:00
tickstep 6f0d03bd58 add javascript plugin manual 2025-03-06 11:06:50 +08:00
tickstep 94c24ebf16 add sample code for js plugin 2025-03-05 20:47:56 +08:00
tickstep fb07969bb1 append PanFS.deleteFile() usage to manual 2025-03-05 20:14:20 +08:00
tickstep 9f83bad7d6 add a println function for plugin to print log text in console windows 2025-03-05 19:56:02 +08:00
tickstep 91b1e724fa add DeletePanFile plugin function #489 2025-03-05 17:33:57 +08:00
tickstep 787d3e1099 add issue template 2025-03-03 15:15:23 +08:00
tickstep 87f951b5f5 add remove file plugin callback #70 2025-03-03 11:46:08 +08:00
tickstep fdafcaed42 add sync task done plugin callback #304 2025-03-01 08:31:15 +08:00
tickstep 9bef162dad revision comments 2025-02-28 14:53:46 +08:00
tickstep 6b638d7817 fix NoSuchUpload error 2025-02-27 16:21:20 +08:00
tickstep ce6d0266bf fix upload database read error #263 2025-02-27 15:45:55 +08:00
tickstep acdc2c93ee fix uploaded file size statistics error 2025-02-27 15:17:08 +08:00
tickstep e9702ea449 update docker file 2025-02-26 11:16:41 +08:00
tickstep 212a8ef764 update dockerfile 2025-02-26 10:39:32 +08:00
tickstep 057362d5a9 update version to v0.3.6 2025-02-25 21:51:40 +08:00
tickstep ae3590f84a add javascript plugin sample to manual file 2025-02-25 10:01:59 +08:00
tickstep 770cd0356f update aliyunpan-api to v0.2.6 2025-02-24 22:05:24 +08:00
tickstep 12bca5683c fix upload file occurs PartNotSequential error #487 #486 #466 2025-02-24 15:54:03 +08:00
tickstep 68f4c44a55 add more logs for upload 2025-02-23 09:06:18 +08:00
tickstep 3aae75a46e fix upload part num issue 2025-02-22 23:08:21 +08:00
tickstep 44e7402b27 fix part num error 2025-02-22 17:44:34 +08:00
tickstep 499b08dd72 append album document 2025-02-18 14:59:18 +08:00
tickstep 29ba6903e6 show target drive name for upload file 2025-02-18 14:44:34 +08:00
tickstep 6d1687e519 add album command 2025-02-18 14:10:13 +08:00
tickstep d8d00600d8 add album permission for login 2025-02-18 14:08:27 +08:00
tickstep 304c4d4f93 adapt album new api 2025-02-14 16:03:35 +08:00
tickstep 4c75a3d6ee fix version string parse error #484 2025-02-05 09:45:56 +08:00
tickstep 34d58d3ebc update version to v0.3.5 2025-01-05 23:06:35 +08:00
tickstep 8410f5977c update copyright tag 2025-01-05 22:26:20 +08:00
tickstep 9730bb8a23 upgrade aliyunpan-api 2025-01-05 22:25:01 +08:00
tickstep 6ed0e6bae0 refactor upload command 2025-01-04 10:57:21 +08:00
tickstep 2b283b727e update copyright tag 2025-01-01 09:27:50 +08:00
tickstep f3d352800b adapt fast share api 2024-12-03 14:04:00 +08:00
tickstep 2eaba0c3c8 add fast share create command 2024-12-03 11:15:38 +08:00
tickstep 4a59f246f2 show error for user that file is too big to upload #469 #460 2024-11-10 13:31:35 +08:00
xiaoyaofenfen d45ef8c1b9 Merge pull request #455 from cuishuang/main
Remove duplicate and redundant code
2024-10-29 13:49:09 +08:00
tickstep ed26496bb7 add share command using openapi 2024-10-29 11:34:58 +08:00
tickstep 1e2bcee3e7 refactor share to share_web 2024-10-26 14:53:13 +08:00
cuishuang 1415e81eec Remove duplicate and redundant code
Signed-off-by: cuishuang <imcusg@gmail.com>
2024-09-27 10:47:22 +08:00
tickstep 7ebaf6ec17 update Dockerfile 2024-09-21 18:50:59 +08:00
tickstep 1e205b52a3 update version v0.3.4 2024-09-21 15:50:41 +08:00
tickstep 790056a488 add comments for upload 2024-09-21 15:49:22 +08:00
tickstep 68c89ec317 using aliyunpan api v0.2.4 & upgrade go to 1.22 2024-09-21 15:21:40 +08:00
tickstep 2849dbb472 add default ip type as ipv4 for docker 2024-09-21 14:13:56 +08:00
tickstep 68437a7a4c update docker file 2024-09-20 11:04:21 +08:00
tickstep 05f695a882 add scan interval time for sync command 2024-09-20 10:53:35 +08:00
tickstep fbfa313c1e add backgroup usage for donwload & upload command 2024-09-09 16:52:03 +08:00
tickstep ff97c172b6 show ips for sync docker 2024-09-04 15:40:31 +08:00
tickstep 47baab44a7 fix file upload PartNotSequential error #445 2024-08-30 11:49:43 +08:00
tickstep 0b5bd60702 add env command 2024-08-18 17:03:10 +08:00
tickstep 6f45a8048d add left time for upload 2024-08-16 22:57:15 +08:00
tickstep 7132342ed3 fix build error with go1.22 2024-08-12 20:51:44 +08:00
tickstep d85758b8b0 update version v0.3.3 2024-08-11 16:57:13 +08:00
tickstep 76e657db74 fix login error 2024-08-11 16:26:20 +08:00
tickstep 7870077565 add multi user download feature usage 2024-08-11 16:20:56 +08:00
tickstep 0a52136c2c update aliyunpan-api to v0.2.2 2024-08-11 14:51:50 +08:00
tickstep 210747fe14 add XDG prompt 2024-08-11 14:21:51 +08:00
tickstep 1f3131f718 remove useless prompt 2024-08-11 14:09:22 +08:00
tickstep 630866798b add multi user download option for download command 2024-08-10 08:16:02 +08:00
tickstep f56d103849 add multi user download option for download command 2024-08-09 21:26:50 +08:00
tickstep 97ee446b32 add multi user download option for download command 2024-08-09 21:20:40 +08:00
tickstep c05148df16 fix download exceed max concurrency limit 2024-08-09 12:57:46 +08:00
tickstep 7dfd89c29d fix download occurs zero speed issue #444 2024-08-09 10:17:02 +08:00
tickstep 0d823de18f make download parallel rule meets openapi limits 2024-08-09 09:43:49 +08:00
feny 605380ce70 show third party vip info for who command 2024-08-07 09:09:43 +08:00
tickstep e1428f2097 adapt XDG folder #433 2024-07-03 21:42:48 +08:00
tickstep b368060ffb update version v0.3.2 2024-05-01 07:39:06 +08:00
tickstep 504d38db90 remove none support commands 2024-04-27 22:18:38 +08:00
tickstep 4a0ad141dc remove none OpenAPI commands 2024-04-27 19:13:59 +08:00
tickstep a7a386e9a6 update readme file 2024-04-26 10:54:03 +08:00
tickstep 6506acba9a update readme 2024-04-23 10:12:52 +08:00
tickstep 1e6004d367 update document 2024-04-22 16:38:24 +08:00
tickstep 9796673138 update version to v0.3.1 2024-04-21 19:22:51 +08:00
tickstep b506bcc84d fix x-retry-after issue #408 2024-04-21 18:46:34 +08:00
tickstep 989abc71ee add run cycle mode config for docker 2024-04-21 18:43:01 +08:00
tickstep ebed4ff62f complete one time cycle mode 2024-04-21 18:37:46 +08:00
tickstep 465d8fa4e9 add one time backup for sync #414 2024-04-21 11:32:49 +08:00
tickstep e9ae974017 set local address binding match prefer ip type 2024-04-08 19:36:32 +08:00
tickstep 54dd4e766b fix switch user error 2024-04-02 09:42:08 +08:00
tickstep 4727b9696d fix token plugin param error 2024-04-01 23:25:34 +08:00
tickstep 4769d17281 set webapi optional 2024-04-01 22:46:24 +08:00
tickstep 03f3d37fb8 update readme file 2024-03-26 19:23:47 +08:00
tickstep d1a577810c support address name for local address binding #402 2024-03-26 19:09:58 +08:00
tickstep 670b5c5dad update readme file 2024-03-25 14:48:03 +08:00
tickstep 9d084610e5 complete backup policy for sync commnad 2024-03-20 21:29:59 +08:00
tickstep 9962e63c81 add sync policy for download&upload mode 2024-03-20 18:04:30 +08:00
tickstep f7b8d26964 add time format method 2024-03-20 10:03:01 +08:00
tickstep 280421fd88 fix macOS arm64 version update check error 2024-03-17 19:48:09 +08:00
tickstep 02b6d529d0 update readme file 2024-03-17 19:32:48 +08:00
tickstep 561c50956f update version to v0.3.0 2024-03-17 16:58:47 +08:00
tickstep 6a39dd78f6 change share usage 2024-03-17 16:53:00 +08:00
tickstep a5ff0780b7 update aliyunapn-api version to v0.2.0 2024-03-17 16:28:53 +08:00
tickstep 1d2342efb5 change sync manual 2024-03-17 16:21:12 +08:00
tickstep 2e3916be7e modify sync document 2024-03-17 16:17:13 +08:00
tickstep 68b3a4aba6 update sync docker file 2024-03-17 15:53:28 +08:00
tickstep 82c62a2d51 disable file record as default 2024-03-17 14:30:06 +08:00
tickstep a02638272f add upload indicator for sync command 2024-03-17 14:26:53 +08:00
tickstep 87d3770e5b add download indicator for sync command 2024-03-17 11:34:40 +08:00
tickstep 8ae323eaa6 add prompt for sync command 2024-03-17 09:33:53 +08:00
tickstep 21770fec89 fix path sperator error in windows OS 2024-03-16 23:13:53 +08:00
tickstep 291fbd54c8 fix build script error 2024-03-16 22:37:32 +08:00
tickstep 281ba818cd support resource drive for sync command 2024-03-16 22:06:20 +08:00
tickstep df53c7f44b optimize upload code 2024-03-16 21:28:21 +08:00
tickstep 5cab8cc6a5 remove test code 2024-03-16 20:54:03 +08:00
tickstep 25202861c5 skip symlink file for upload mode 2024-03-16 20:44:19 +08:00
tickstep 3fcbbe9b40 remove no use flag for sync command 2024-03-16 17:13:10 +08:00
tickstep ae3be4ce6f remove sync mode usage 2024-03-16 17:06:55 +08:00
tickstep ae81b59926 fix download url expired issue for sync 2024-03-16 17:02:33 +08:00
tickstep c0da3de6c6 refactor download mode for sync 2024-03-16 15:58:54 +08:00
tickstep 134531d64a refactor sync upload mode 2024-03-13 15:31:28 +08:00
tickstep 0064a23f64 fix refresh open token may occurs nil pointer exception 2024-03-11 15:21:40 +08:00
tickstep 425dddf92b fix refresh web token occurs nil pointer exception 2024-03-11 15:19:56 +08:00
tickstep 31aaadfcc1 refactor mv command 2024-03-07 14:22:56 +08:00
tickstep 1f10f1e982 remove useless code 2024-03-07 14:22:40 +08:00
tickstep a9f9f18f09 refactor file rm command 2024-03-07 09:27:29 +08:00
tickstep 5e90f96577 add cp command 2024-03-06 22:53:30 +08:00
tickstep 5b008b935d not support album download in download task 2024-03-06 21:35:56 +08:00
tickstep bdc0a15bdc fix download url expired issue 2024-03-05 18:19:41 +08:00
tickstep 26c7201f4e add fast share document 2024-03-04 15:17:31 +08:00
tickstep 7da4fedc54 remove album command temporary 2024-03-04 14:35:48 +08:00
tickstep 957a1a1bd1 make download file parallel number match adrive rule 2024-03-04 14:25:20 +08:00
tickstep 92133d185e remove download limit prompt 2024-03-04 14:00:32 +08:00
tickstep 239b794f8b update readme file 2024-03-04 11:36:24 +08:00
tickstep b8f35e8647 update README file 2024-03-04 10:43:06 +08:00
tickstep c465277a8d update README file 2024-03-04 10:42:18 +08:00
tickstep 281552eefa make image smaller 2024-03-04 10:27:53 +08:00
tickstep d26d4d273e fix spell error 2024-03-04 09:05:33 +08:00
tickstep f531c7bc70 fix login refresh token issue 2024-03-03 23:20:27 +08:00
tickstep 443eac7589 not support upload & download using InternalUrl 2024-03-03 22:22:50 +08:00
tickstep 6aecb98595 update README file 2024-03-03 22:00:24 +08:00
tickstep 62c50726a5 update document 2024-03-03 21:56:48 +08:00
tickstep a7034e300f set PreHash threshold as 100MB 2024-03-03 16:23:03 +08:00
tickstep c86afbd764 add PreHash check for file upload 2024-03-03 16:20:50 +08:00
tickstep d83f87942c adapt WebPanClient naming 2024-03-03 14:07:35 +08:00
tickstep b79cd7bafb adapt webapi new struct 2024-03-03 14:02:05 +08:00
tickstep 4c0c8a4ff9 fix upload file error 2024-03-03 10:50:23 +08:00
tickstep bb231dd45e using openapi for sync command 2024-03-03 10:12:44 +08:00
tickstep bec492fe65 fix refresh & save new token error 2024-03-03 09:11:58 +08:00
tickstep 69b76a15ca use openapi for upload commnad 2024-03-02 22:58:41 +08:00
tickstep 3bdc27ce66 fix download file error when file size big than 100MB 2024-03-02 22:38:36 +08:00
tickstep 9dd2482b58 add openapi token refresh hook 2024-03-02 22:10:54 +08:00
tickstep e8b87a02dc rename refresh token method 2024-03-02 21:26:37 +08:00
tickstep 787aa53ce7 fix web api client issue 2024-03-02 21:22:02 +08:00
tickstep 4a623b443c not support album drive 2024-03-02 19:29:12 +08:00
tickstep 136dfb082f add webapi check 2024-03-02 19:25:55 +08:00
tickstep e4d73d8b42 add check for webapi using 2024-03-02 19:23:54 +08:00
tickstep 47123f2b86 not support album drive 2024-03-02 19:14:43 +08:00
tickstep 9d6dfd0d0a short web token refresh time interval 2024-03-02 19:11:58 +08:00
tickstep 3c0da7fc1d use openapi as default api for commands 2024-03-02 17:38:21 +08:00
tickstep 2fe5cc5414 remove no support command 2024-03-02 10:25:35 +08:00
tickstep e261e04978 add login prompt 2024-03-02 10:01:22 +08:00
tickstep e5f63c60e9 change login routine 2024-03-02 00:55:46 +08:00
tickstep c350cadba4 remove webdav code 2024-03-01 11:35:18 +08:00
tickstep 4e8c4d6e9c fix share file save issue 2024-03-01 11:32:10 +08:00
xiaoyaofenfen a282aabbe7 Merge pull request #367 from reveever/main
新增命令支持保存分享文件
2024-03-01 11:27:32 +08:00
tickstep c81855f9c2 update to version v0.2.9 2024-01-30 16:31:50 +08:00
tickstep 65b7f28aa6 update api version to v0.1.8 2024-01-30 15:08:26 +08:00
xiaoyaofenfen 8afdb8f701 Merge pull request #381 from Sowevo/main
增加忽略SHA1跳过同名文件的功能
2024-01-30 14:51:36 +08:00
tickstep ead6ecc218 remove device offline action 2024-01-30 14:32:43 +08:00
tickstep a42f14f013 update copyright info 2024-01-30 14:13:23 +08:00
tickstep 54fff27fbe add download 100MB limit prompt for user 2024-01-30 14:10:43 +08:00
tickstep b0a1ea8591 use new api for recycle bin clear action 2024-01-30 12:01:41 +08:00
sowevo 1b78dff61e add skip args 2024-01-26 15:10:27 +08:00
sowevo f838ebfe24 add skip args 2024-01-26 15:01:27 +08:00
sowevo 7f5e19812e add skip args 2024-01-26 11:40:04 +08:00
ReveEver bbff32c8c6 add save cmd for saving shared files 2023-12-10 21:44:48 +08:00
xiaoyaofenfen 6a7da9f180 update readme file 2023-10-11 11:05:51 +08:00
xiaoyaofenfen 06184249d0 add prefer ip type config 2023-10-09 16:09:34 +08:00
xiaoyaofenfen b983d4c335 add usage demo for drive select 2023-10-09 09:33:18 +08:00
tickstep d061a28559 update version to v0.2.8 2023-10-09 01:17:32 +08:00
tickstep e91e66aadf add loong64 build script 2023-10-08 23:09:36 +08:00
tickstep 71f55424d0 update bolt version to v1.3.4 2023-10-08 23:08:49 +08:00
tickstep c6c4d871ef update aliyunpan-api to v0.1.7 2023-10-08 20:01:05 +08:00
tickstep 50e6200607 update document & readme file 2023-10-08 19:41:27 +08:00
xiaoyaofenfen c363d90e72 rename album usage 2023-10-08 17:22:36 +08:00
xiaoyaofenfen 048a6a5438 no limit the upload parallels threads #343 2023-10-08 16:55:20 +08:00
xiaoyaofenfen 19afa16bfe add album download command 2023-10-08 16:41:55 +08:00
xiaoyaofenfen b559f41e47 remove locate command 2023-10-08 14:56:47 +08:00
xiaoyaofenfen 33a5484e4a modify drive list usage 2023-10-08 14:54:59 +08:00
xiaoyaofenfen 34254bf822 format used disk quota 2023-10-08 14:23:58 +08:00
xiaoyaofenfen 66540c9457 add xcp command manual 2023-10-08 14:19:32 +08:00
xiaoyaofenfen c522b530d7 add xcp command 2023-10-08 14:12:18 +08:00
xiaoyaofenfen 3d130949c0 add share link create check 2023-10-08 09:43:27 +08:00
tickstep 1365471639 remove webdav command 2023-10-07 19:26:27 +08:00
xiaoyaofenfen 151574e154 support fast share link create 2023-10-07 17:20:16 +08:00
xiaoyaofenfen 55e989d821 adapt resource drive 2023-10-07 16:42:43 +08:00
tickstep 0edf7d0af2 label forbidden status for share list 2023-04-30 10:01:32 +08:00
tickstep ccc285ce5e add brew usage in readme file 2023-04-06 21:46:48 +08:00
tickstep 9992ffb3bc not exit app when config file read error 2023-04-02 22:23:55 +08:00
tickstep 25d916b744 update version to v0.2.7 2023-04-02 10:30:29 +08:00
tickstep 471ee93417 update version to v0.1.6 2023-04-02 10:02:14 +08:00
tickstep 78e290c2a4 optimize device session signature error #257 2023-04-02 09:58:20 +08:00
tickstep 23fe54ce0b refactor create session 2023-04-02 09:37:42 +08:00
tickstep 8d2c40f1ce fix too many request error for tree cmd 2023-04-02 08:53:59 +08:00
tickstep fa144a45a1 update aliyunpan-api to v0.1.5 2023-04-01 23:08:50 +08:00
tickstep 2740a15cbf fix device session signature error for download 2023-04-01 23:00:35 +08:00
tickstep 3be79d1181 fix device session signature error for upload #257 2023-04-01 22:48:39 +08:00
tickstep ab215736a9 refactor create session 2023-04-01 22:17:17 +08:00
tickstep 45877a4408 fix make dir faild issue #276 2023-04-01 20:30:29 +08:00
tickstep 50a05a6bec add run command for test 2023-04-01 20:20:32 +08:00
tickstep e33d23831a add winget usage to readme file 2023-03-25 08:34:44 +08:00
xiaoyaofenfen cbf372bde0 fix file info query error #232 2023-02-23 15:43:51 +08:00
xiaoyaofenfen 8d14617397 fix create folder error for upload command #244 2023-02-23 15:27:27 +08:00
xiaoyaofenfen 12d866b28a add file size percentage for upload & download command #236 2023-02-23 15:12:35 +08:00
xiaoyaofenfen 85d79a9657 add file size filter & file size showing for tree command #236 2023-02-23 14:43:02 +08:00
xiaoyaofenfen 828c1fe39a add device logout #249 2023-02-23 13:50:55 +08:00
tickstep c5a5cb906d fix signature update error 2023-02-19 17:36:23 +08:00
tickstep 4ba61c19c8 add device id config for docker 2023-02-19 17:04:45 +08:00
tickstep 571baa1482 update version to v0.2.6 2023-02-16 22:54:40 +08:00
tickstep 4575dfdaa8 add deregister device guide in readme file 2023-02-16 22:40:02 +08:00
tickstep efbf0b7bdb add deregister device guide in readme file 2023-02-16 22:39:06 +08:00
tickstep 416c5cd57e fix signature error 2023-02-16 22:25:09 +08:00
xiaoyaofenfen e0e8bc3284 add skip confirm flag for rename command 2023-01-03 11:12:26 +08:00
xiaoyaofenfen 5b22c4400a fix that the folder will created repeatly while uploading file 2023-01-03 10:57:08 +08:00
tickstep c9c33217d2 update version to v0.2.5 2023-01-01 14:08:28 +08:00
tickstep 79dcf40792 support batch rename #215 2023-01-01 13:42:49 +08:00
tickstep d618686da9 add tips for proxy config 2022-12-29 11:14:31 +08:00
tickstep 5f52145378 update aliyunpan-api to v0.1.3 2022-12-29 10:46:02 +08:00
tickstep bd958fb8df remove file locker 2022-12-27 18:58:49 +08:00
tickstep 8fc8d9b6f7 fix tab error #212 2022-12-27 17:19:22 +08:00
tickstep 70ddb7e0c1 expland expired time to fix url expired issue #214 #207 #208 2022-12-27 10:06:19 +08:00
tickstep 0acd6a43d6 fix URL expired issue #208 2022-12-26 21:24:07 +08:00
tickstep 97a22812a2 fix LS commnad list root folder error 2022-12-24 17:10:07 +08:00
tickstep 118e521d63 fix UploadPartNotSeq issue #208 2022-12-24 17:04:38 +08:00
tickstep f8c4961608 remove upload useless code 2022-12-21 22:08:32 +08:00
xiaoyaofenfen ed38002e65 fix token refresh routine not exit error 2022-12-21 16:27:39 +08:00
xiaoyaofenfen b74d39da41 change config tips 2022-12-21 15:44:20 +08:00
xiaoyaofenfen aabab869c4 show current config folder for config command 2022-12-21 15:41:00 +08:00
xiaoyaofenfen bb8dad7262 make download/upload/sync command singleton #208 #207 2022-12-21 15:30:35 +08:00
xiaoyaofenfen 9f9fb6a320 change log folder permission 2022-12-21 15:29:15 +08:00
xiaoyaofenfen 84d7ec923f remove rapid upload limit 2022-12-20 11:54:20 +08:00
xiaoyaofenfen 07746fc15c add sync file record #206 2022-12-19 20:41:05 +08:00
xiaoyaofenfen c5d15b058a add download file record #206 2022-12-19 19:50:33 +08:00
xiaoyaofenfen ea9279e45e add upload file record #206 2022-12-19 19:41:13 +08:00
xiaoyaofenfen 6abab46db0 add file recorder config 2022-12-19 19:40:42 +08:00
xiaoyaofenfen 6e8ee94285 add file recorder library 2022-12-19 19:17:31 +08:00
xiaoyaofenfen 1208ccad08 add file recorder library 2022-12-19 19:16:39 +08:00
xiaoyaofenfen d23802e8cc auto resize upload block size for big file #205 2022-12-13 16:32:08 +08:00
xiaoyaofenfen 59646b07fe add upload block size usage documents 2022-12-13 09:54:22 +08:00
xiaoyaofenfen 34dd8a98fe donot load js file everytime 2022-12-13 09:53:29 +08:00
xiaoyaofenfen b6780931cc fix webdav token plugin callback error #203 2022-12-12 14:07:58 +08:00
xiaoyaofenfen fb705365b1 add token plugin callback for login routine #203 2022-12-10 14:47:00 +08:00
xiaoyaofenfen 1c6f6c772c refractor login routine 2022-12-10 14:07:28 +08:00
xiaoyaofenfen 7726888365 add email notify sample for manual document 2022-12-08 15:59:16 +08:00
tickstep eda44658c9 update version to v0.2.4 2022-12-07 22:01:23 +08:00
tickstep 0b6c77b96f update aliyunpan-api version 2022-12-07 21:45:39 +08:00
tickstep f88e921b04 add kv dictionary for plugin 2022-12-07 21:31:33 +08:00
xiaoyaofenfen c3dadd00b7 add token plugin callback for token refresh routine 2022-12-07 17:35:03 +08:00
xiaoyaofenfen 90a889f0eb fix webdav token refresh erorr 2022-12-07 17:24:02 +08:00
xiaoyaofenfen 5935f78824 fix token refresh update bug #167 2022-12-07 17:09:24 +08:00
xiaoyaofenfen 56ba938c72 add userTokenRefreshFinishCallback plugin api #172 2022-12-07 16:45:59 +08:00
xiaoyaofenfen 6ade77c4bf add send email api for plugin 2022-12-07 10:34:46 +08:00
xiaoyaofenfen ecacda2596 update manual doc 2022-12-07 09:32:13 +08:00
xiaoyaofenfen 695acce3f4 add send email api for plugin 2022-12-06 20:40:19 +08:00
xiaoyaofenfen 98de5b931c fix too many requests error for upload 2022-12-06 19:48:31 +08:00
xiaoyaofenfen a1577f19c5 update readme file 2022-12-06 15:40:30 +08:00
xiaoyaofenfen f1fb75cfe4 short token refresh check time 2022-12-06 13:46:19 +08:00
xiaoyaofenfen 7ee48fa726 download command support exclude name patternt 2022-12-06 11:28:36 +08:00
xiaoyaofenfen 15084b3ac2 refactor upload exclude file method 2022-12-06 09:52:12 +08:00
xiaoyaofenfen 4a4796218e download command support wildcard pattern 2022-12-06 09:29:49 +08:00
xiaoyaofenfen 9ccaf8a25d add usage 2022-12-06 08:41:07 +08:00
xiaoyaofenfen dc469e87d2 share set command support wildcard pattern 2022-12-05 20:43:35 +08:00
xiaoyaofenfen 67c288318e locate command support wildcard pattern 2022-12-05 20:25:35 +08:00
xiaoyaofenfen 36a5eed5af tree command support wildcard pattern 2022-12-05 20:19:52 +08:00
xiaoyaofenfen fb24f703c2 rm command support wildcard pattern 2022-12-05 20:10:02 +08:00
xiaoyaofenfen 0d203ece9a mv command support wildcard pattern 2022-12-05 20:04:36 +08:00
xiaoyaofenfen 9be7080ae3 fix bad gateway error 2022-12-05 19:59:37 +08:00
xiaoyaofenfen b8aeead58c ls command support wildcard patterns 2022-12-05 17:25:21 +08:00
xiaoyaofenfen a15f704672 cd command support wildcard patterns 2022-12-05 17:14:03 +08:00
xiaoyaofenfen 4173889120 add wildchar path match method 2022-12-05 16:56:17 +08:00
xiaoyaofenfen 077d18f471 fix QrCode login error 2022-12-01 14:13:08 +08:00
xiaoyaofenfen 1c454060c1 handle sync delete file error #199 2022-12-01 11:39:37 +08:00
xiaoyaofenfen a455af2084 handle download delete file error #199 2022-12-01 11:32:32 +08:00
xiaoyaofenfen 5f6cdcab64 fix upload file no permission error for sync #188 2022-11-14 10:26:31 +08:00
xiaoyaofenfen 9db89a64b0 Merge pull request #190 from ZemingLiu/main
set supported architectures in apt source file
2022-11-12 10:35:23 +08:00
Zeming.Liu b600dd17c7 set supported architectures in apt source file 2022-11-12 01:34:13 +08:00
tickstep 3b18f9af78 fix complie error for go18+ sdk #175 2022-11-05 18:13:10 +08:00
tickstep 00e1c3e3d4 fix illegal download file issue #183 2022-11-02 21:52:11 +08:00
xiaoyaofenfen 2e4a6d94df modify mips build script #182 2022-10-31 17:13:32 +08:00
xiaoyaofenfen bf91a47009 add userId for sync task #176 2022-10-28 10:40:59 +08:00
xiaoyaofenfen a2f33f32bf fix share error message missing bug 2022-10-18 09:14:40 +08:00
xiaoyaofenfen f511e17091 fix download file error for upgrade 2022-10-17 20:01:36 +08:00
tickstep 3ad528bc8e update version to v0.2.3 2022-10-16 09:23:45 +08:00
tickstep 1c80403bc3 support get download url for folder 2022-10-16 09:15:37 +08:00
tickstep 8c08a322a1 update aliyunpan-api module 2022-10-15 22:37:03 +08:00
tickstep a2a46bdeeb locate can save download links to file 2022-10-15 22:25:22 +08:00
tickstep ae077101f1 fix update check error 2022-10-15 16:49:17 +08:00
xiaoyaofenfen 41fd68411e add document for locate command 2022-10-14 15:21:50 +08:00
xiaoyaofenfen a81d97c8bc add timeout option for upload #157 2022-10-14 14:22:28 +08:00
xiaoyaofenfen 13451ccf4c add locate command to get directly download url for file 2022-10-14 11:03:40 +08:00
xiaoyaofenfen 643d35a122 refactor mv to support wildcard pattern 2022-10-14 09:32:11 +08:00
xiaoyaofenfen 79833da4fc refactor rm to support wildcard pettern 2022-10-14 09:16:35 +08:00
xiaoyaofenfen f90829aaab add sync file finish callback for plugin #166 2022-10-10 11:35:40 +08:00
tickstep 072e7e9261 add total file size statistic for tree 2022-10-09 19:09:17 +08:00
tickstep b27e9b24d7 remove useless code 2022-10-09 19:08:47 +08:00
tickstep 8f79a7e807 support wildcard for move command #89 2022-10-07 12:11:34 +08:00
tickstep 81d7bd22c9 support wildcard for remove command 2022-10-07 11:41:27 +08:00
tickstep 47e8b004b8 add memo for download 2022-10-01 16:23:18 +08:00
tickstep 238d692f73 update version to v0.2.2 2022-09-30 00:19:24 +08:00
xiaoyaofenfen b85be20cba update document for sync command 2022-09-29 19:48:25 +08:00
xiaoyaofenfen a283320eff update document for webdav 2022-09-29 19:26:20 +08:00
xiaoyaofenfen c9eb8e06db support scan file to build sync database for sync command #155 2022-09-29 19:09:07 +08:00
xiaoyaofenfen bd8b1f43e6 change prompt 2022-09-29 15:21:58 +08:00
xiaoyaofenfen 65b0dedb9b handle local file lock state for sync download file #162 2022-09-28 11:46:13 +08:00
xiaoyaofenfen f3b1b84439 add local file modified delay time to fix issue #112 2022-09-27 20:47:45 +08:00
xiaoyaofenfen fe370ca953 adapt modified file for sync upload #112 2022-09-27 19:45:08 +08:00
xiaoyaofenfen 43f7883155 optimize sync task to fix issue #155 #125 2022-09-27 14:29:21 +08:00
xiaoyaofenfen 2668e79564 check local sync folder state to fix issue #156 2022-09-27 14:25:48 +08:00
xiaoyaofenfen 25c550b4d8 add token refresh for webdav #163 2022-09-24 15:25:03 +08:00
xiaoyaofenfen 4c5f217257 add token refresh for webdav 2022-09-24 15:09:31 +08:00
xiaoyaofenfen 93b55014d0 support readonly mode for webadv #153 2022-09-23 16:49:26 +08:00
xiaoyaofenfen 7307c7f37f format code 2022-09-23 11:59:33 +08:00
tickstep 955a563b94 fix linux service error 2022-09-07 18:28:31 +08:00
tickstep 666842f972 add code sample for javascript plugin 2022-09-04 16:46:03 +08:00
tickstep fe22d18aa7 add code sample for javascript plugin 2022-09-04 16:43:29 +08:00
tickstep 66d4b14ff9 add code sample for javascript plugin 2022-09-04 16:37:03 +08:00
xiaoyaofenfen 08c9d58ef8 fix relative path error for sync command to fix issue #135 2022-08-31 11:51:28 +08:00
xiaoyaofenfen 4e7545e6e6 add show full path option for tree command 2022-08-31 11:06:57 +08:00
xiaoyaofenfen ee9c5db8b3 add file & dir statistic for tree command 2022-08-31 10:36:33 +08:00
tickstep 9c965b5b64 update version to v0.2.1 2022-08-30 23:18:03 +08:00
tickstep 83d2895f78 update readme file 2022-08-30 22:52:47 +08:00
tickstep e8c86672ff Merge branch 'main' of github.com:tickstep/aliyunpan 2022-08-28 11:31:11 +08:00
tickstep cb4c9fbeb7 update api version to v0.1.0 2022-08-28 11:30:50 +08:00
xiaoyaofenfen 0c80fca74e fix download save dir error 2022-08-26 17:35:20 +08:00
tickstep 3faf4d3f43 update readme file 2022-08-23 23:20:25 +08:00
xiaoyaofenfen d7e4772417 add video transcode request after upload video file to fix issue #53 2022-08-22 14:27:52 +08:00
xiaoyaofenfen 8b1515c2cf update readme file 2022-08-22 09:27:42 +08:00
tickstep fa3ffeefd9 update version to v0.2.0 2022-08-21 19:05:07 +08:00
tickstep e8f73e5a37 update readme file 2022-08-21 16:35:11 +08:00
tickstep 1cf3db39b1 fix download path error to fix issue #130 2022-08-21 08:39:32 +08:00
xiaoyaofenfen 15bef9fcc3 change index number of table starts as one 2022-08-20 16:37:28 +08:00
xiaoyaofenfen 3e1b87f5f2 add tree command 2022-08-19 17:27:54 +08:00
xiaoyaofenfen 5c19a07a31 update linux service config file 2022-08-19 14:36:39 +08:00
xiaoyaofenfen 5782bd7cd9 donot listener the input stream to fix issue #132 2022-08-19 14:28:08 +08:00
xiaoyaofenfen a8850e2479 handle local file discard error for sync 2022-08-19 11:53:23 +08:00
xiaoyaofenfen 7978788ccd change description 2022-08-19 11:52:37 +08:00
xiaoyaofenfen 8a6723cb06 disable delete file for upload & download mode to fix issue #140 #136 2022-08-18 18:36:38 +08:00
tickstep f303cc7338 update version to v0.1.9 2022-08-13 21:37:26 +08:00
tickstep a2afb9990c update readme file 2022-08-13 21:30:04 +08:00
tickstep 9016c12c26 add log flag for sync command 2022-08-13 19:38:10 +08:00
tickstep d0ad94aa65 add priority option for sync docker 2022-08-13 18:43:29 +08:00
tickstep 594e635cde add priority option for sync config 2022-08-13 18:38:29 +08:00
tickstep 6787269751 add priority for sync to fix issue #131 2022-08-13 17:50:07 +08:00
tickstep 5f10365604 refactor sync code 2022-08-13 16:54:42 +08:00
tickstep 0a7f520a8c update aliyunpan-api library 2022-08-13 15:49:28 +08:00
tickstep 2d3874ea7f add manual for zip package 2022-08-13 15:41:19 +08:00
tickstep 2e35149ac5 update readme file 2022-08-13 14:23:52 +08:00
tickstep 74e5a6f409 update readme file 2022-08-13 14:08:04 +08:00
tickstep f98d201f1e update readme file 2022-08-13 14:06:32 +08:00
tickstep aa17816ed1 update readme file 2022-08-13 14:01:53 +08:00
tickstep 0668e743f5 update readme file 2022-08-13 13:58:46 +08:00
tickstep d8da7cefc3 update readme file 2022-08-13 12:40:29 +08:00
tickstep bf75461a8b add manual document 2022-08-13 12:33:18 +08:00
xiaoyaofenfen 8f7d4920ae fix issue #82 2022-08-10 14:32:28 +08:00
xiaoyaofenfen fa475768fa fix delete pan file occurs error to fix issue #126 2022-08-10 14:09:02 +08:00
xiaoyaofenfen c74848b5bc add export share item list to fix issue #121 2022-08-10 11:34:48 +08:00
xiaoyaofenfen 83914ad5e1 support show all share item to fix issue #120 2022-08-10 10:31:27 +08:00
xiaoyaofenfen 43f4b23981 fix save & saveto path error to fix issue #130 2022-08-09 20:34:22 +08:00
xiaoyaofenfen 4833c787ac fix file join error 2022-08-09 20:33:46 +08:00
xiaoyaofenfen f13177214e fix relative path error to fix issue #119 2022-08-09 19:29:42 +08:00
xiaoyaofenfen d90894898f fix path format error 2022-08-09 19:27:46 +08:00
tickstep ac5eb94305 use cache for file path query api 2022-08-06 21:54:46 +08:00
xiaoyaofenfen ba3ae4028b use file path cache to fix issue #126 #118 2022-08-02 17:19:59 +08:00
xiaoyaofenfen 416b2e2e6a fix download local folder detect error 2022-07-07 11:21:01 +08:00
xiaoyaofenfen 23fa29ef54 add config env tips for usages 2022-07-07 11:01:52 +08:00
tickstep 6031beee6d update version to v0.1.8 2022-07-03 13:15:38 +08:00
tickstep 5f62a93ce4 format create folder prompt 2022-07-03 12:24:26 +08:00
tickstep 811ebdf805 update document 2022-07-03 11:57:18 +08:00
tickstep a218f47f9e upgrade aliyunpan-api denpendency 2022-07-03 11:50:36 +08:00
tickstep b22a79fdb2 support upload empty folder 2022-07-03 11:45:42 +08:00
tickstep 8456aa7b35 support symbolic file for download command 2022-07-03 11:09:57 +08:00
tickstep 797239361c add symlink file util function 2022-07-03 10:32:39 +08:00
tickstep b3a7a375d0 add real path retrieve util 2022-07-03 07:45:40 +08:00
tickstep 3fa8c3eee4 optimize sync process 2022-07-02 21:41:21 +08:00
tickstep 8dd7c6b7cd support sync empty folder for sync command 2022-07-02 18:59:00 +08:00
tickstep 99cfb7d09d fix delete file error for sync command to fix issue #111 2022-07-02 17:24:41 +08:00
tickstep bf4027601c optimize get all file list performance 2022-07-02 16:24:11 +08:00
tickstep 714c912c54 hanlder network error to fix issue #111 2022-07-02 09:46:33 +08:00
tickstep ab49739513 support macOS symbolic file for upload 2022-07-02 07:27:49 +08:00
xiaoyaofenfen b4e405d0f8 support symbolic file for upload command 2022-06-28 15:10:54 +08:00
xiaoyaofenfen 6ce355ead8 add bolt delete test 2022-06-28 15:10:29 +08:00
xiaoyaofenfen 884d8d356c add symlink file lib 2022-06-28 13:49:32 +08:00
xiaoyaofenfen dd4f8931bf js plugin support folder filter for download 2022-06-27 15:41:02 +08:00
xiaoyaofenfen f6daf215b9 js plugin support folder filter for upload 2022-06-27 15:27:26 +08:00
tickstep 7d0da94671 update version to v0.1.7 2022-06-26 12:16:39 +08:00
tickstep 95dcd3307f rename service config file 2022-06-26 12:03:57 +08:00
tickstep d376c19ede add verbose log file path 2022-06-26 11:51:34 +08:00
xiaoyaofenfen b8949b11f0 update readme document 2022-06-25 16:59:28 +08:00
tickstep e8ca54cf4b optimize sync to reduce CPU resource for docker 2022-06-24 22:25:40 +08:00
xiaoyaofenfen 8632c268f0 optimize sync task to reduce CPU resource 2022-06-24 10:36:03 +08:00
xiaoyaofenfen 1c04ab9c7b optimize sync scan task 2022-06-23 16:40:10 +08:00
xiaoyaofenfen 779f21de65 update sync docker document 2022-06-22 08:50:14 +08:00
xiaoyaofenfen 7b720dbb5e add windowns service config document for sync 2022-06-21 13:58:52 +08:00
tickstep a867359da3 remove armv5 docker for script 2022-06-20 22:40:04 +08:00
xiaoyaofenfen f003ff9f44 fix access local file error for sync 2022-06-20 20:38:22 +08:00
xiaoyaofenfen 84a833db5b fix concurrent plugin access error for sync 2022-06-20 20:37:54 +08:00
xiaoyaofenfen b64c07f079 remove backup command 2022-06-20 17:28:25 +08:00
xiaoyaofenfen ac93c7e184 update readme file 2022-06-20 15:10:27 +08:00
xiaoyaofenfen bc2ab0625f add linux service config sample 2022-06-20 14:39:31 +08:00
xiaoyaofenfen 3d1aa1c8bb change scripts location 2022-06-20 14:22:05 +08:00
xiaoyaofenfen 6b0c815597 update readme file 2022-06-20 09:52:34 +08:00
tickstep a914eb5338 add bolt db test 2022-06-19 15:18:47 +08:00
tickstep d707895acc modify docker script 2022-06-18 12:33:42 +08:00
tickstep 5b877c8fd8 update version to v0.1.6 2022-06-18 11:52:50 +08:00
tickstep 8db035d4e5 add docker sync script 2022-06-18 11:26:16 +08:00
tickstep 5efbd9c306 add fetch refresh token cmd 2022-06-18 11:19:47 +08:00
tickstep 6c1ab9d6e0 add document for sync 2022-06-18 11:01:33 +08:00
tickstep dd72192db6 change pan scan interval 2022-06-18 08:39:18 +08:00
tickstep 621925709d change sync plugin sample 2022-06-18 08:38:57 +08:00
tickstep 57e2ecdb72 fix sync prepare error 2022-06-17 23:37:32 +08:00
tickstep 6669b64027 fix download zero file error 2022-06-17 22:34:40 +08:00
xiaoyaofenfen 117a4f73db fix upload bug for sync 2022-06-17 17:14:50 +08:00
xiaoyaofenfen ed11869074 add copy file for win build script 2022-06-17 10:38:34 +08:00
xiaoyaofenfen 379ce404e4 add cmd config for sync 2022-06-17 10:27:53 +08:00
tickstep b36b14e03a fix zero file sha1 calc error 2022-06-17 06:59:04 +08:00
tickstep 8ac48bbda5 fix file & folder perm eror 2022-06-16 22:30:12 +08:00
xiaoyaofenfen ba407cb74b do upload retry to fix issue #90 2022-06-16 19:58:49 +08:00
xiaoyaofenfen 3b802d5cc6 fix issue #103 2022-06-16 14:38:45 +08:00
xiaoyaofenfen 09051e1e7a change notes 2022-06-16 14:20:49 +08:00
xiaoyaofenfen f458dc5353 add health check for webdav docker 2022-06-16 14:01:30 +08:00
xiaoyaofenfen 1d8e16f5c9 add health check for sync docker 2022-06-16 13:47:01 +08:00
xiaoyaofenfen 3ef5e67d76 fix mkdir error for dockerfile 2022-06-16 08:54:09 +08:00
xiaoyaofenfen a88487dffd fix mkdir error for dockerfile 2022-06-16 08:53:17 +08:00
tickstep 855b640d9f change sync prompt 2022-06-15 23:02:19 +08:00
tickstep 911b07cc8a fix dockerfile error 2022-06-15 22:33:23 +08:00
xiaoyaofenfen bc86c25549 change service name for sync docker 2022-06-15 10:01:51 +08:00
xiaoyaofenfen 13daf9480b add plugin for sync docker 2022-06-15 09:58:32 +08:00
xiaoyaofenfen 9f1e565dae add sync handler sample file for plugin 2022-06-15 08:46:14 +08:00
xiaoyaofenfen e5bf8da4de add plugin support for sync 2022-06-14 21:05:40 +08:00
tickstep 37f3b67002 add sync docker build script 2022-06-13 23:28:06 +08:00
tickstep 86ae72f76e add sync sample file for build 2022-06-13 22:16:03 +08:00
tickstep 5a018dd4ce update aliyunapn api 2022-06-12 23:17:27 +08:00
tickstep 09740df140 rename webdav docker file 2022-06-12 23:07:44 +08:00
tickstep b5dde87145 change scan eplase 2022-06-12 17:48:14 +08:00
tickstep d1836d3df8 add more log for sync 2022-06-12 17:44:16 +08:00
tickstep 38a7f63cc6 add rate limit for sync 2022-06-12 17:03:05 +08:00
tickstep 350c879f66 add download block size option for sync 2022-06-12 08:03:14 +08:00
tickstep 12a598015f add sync drive config sample file 2022-06-10 22:56:48 +08:00
tickstep b4049b35e5 label backup as deprecated command 2022-06-10 22:54:03 +08:00
tickstep 1b2c8ae1ce fix no sync task error 2022-06-10 22:48:29 +08:00
tickstep 232ab535ed add sync command 2022-06-10 22:39:54 +08:00
xiaoyaofenfen 979b3d9bde adapt user internal url for sync 2022-06-10 14:36:52 +08:00
xiaoyaofenfen debaf655f0 add sync config 2022-06-10 13:55:00 +08:00
xiaoyaofenfen f1c6379e33 add comment 2022-06-09 20:05:30 +08:00
xiaoyaofenfen f692e2d18b add sync command 2022-06-09 19:58:16 +08:00
xiaoyaofenfen d0e7715f4d add delete local / pan file action 2022-06-09 14:52:49 +08:00
xiaoyaofenfen dbae151f22 fix sync download / upload bug 2022-06-06 19:13:37 +08:00
xiaoyaofenfen ad5c44aaf9 add golang-set dependency 2022-06-06 08:56:04 +08:00
tickstep 2ec8592685 add upload func for sync 2022-06-05 22:19:16 +08:00
tickstep f804a42a19 fix bug 2022-06-05 00:00:28 +08:00
tickstep ab6290e7a3 fix sync download bug 2022-06-04 16:45:45 +08:00
tickstep 8955eeec92 fix sync download file bug 2022-06-04 13:20:33 +08:00
tickstep f2bcc977cc add download action for sync 2022-06-01 21:55:03 +08:00
tickstep 437c2e31b9 rename file 2022-05-24 23:01:08 +08:00
xiaoyaofenfen 25d7ff406c add sync task manager 2022-05-24 19:56:06 +08:00
tickstep 37f4bf05e4 add sync pan file routine 2022-05-22 17:47:48 +08:00
tickstep a582cba76a add sync task 2022-05-22 16:42:57 +08:00
xiaoyaofenfen f9dbb6ffa2 add AddFileList method for sync library 2022-05-17 19:48:39 +08:00
xiaoyaofenfen d7434d29a6 refactor sync library code 2022-05-17 15:15:38 +08:00
xiaoyaofenfen 7f4675331f add local file sync db 2022-05-16 14:22:56 +08:00
xiaoyaofenfen 24765ba2af refactor sync drive library 2022-05-13 15:34:44 +08:00
tickstep 01b9929c4b add Update method for sync db 2022-05-10 10:34:57 +08:00
tickstep 557b36617a add Delete method for sync db 2022-05-10 10:18:19 +08:00
xiaoyaofenfen 232155c5f3 add sync drive library 2022-05-07 15:03:23 +08:00
tickstep 847546424b refractor code 2022-05-01 11:46:22 +08:00
tickstep 8e422c85a4 fix issue #86 2022-04-30 21:46:13 +08:00
tickstep 7c6145aee4 update version to v0.1.5 2022-04-30 07:28:01 +08:00
tickstep b645f6caba fix upload hook handle file name error 2022-04-30 07:18:13 +08:00
tickstep 45c4a30f22 update version to v0.1.4 2022-04-30 00:25:35 +08:00
tickstep 594ae354e1 update javascript doc 2022-04-30 00:14:16 +08:00
tickstep 2da542fc22 update javascript doc 2022-04-29 23:55:50 +08:00
tickstep 52fbe51499 add deleteFile func for javascript plugin 2022-04-29 23:37:18 +08:00
tickstep 90cec4b827 add javascript plugin utils function 2022-04-29 22:10:09 +08:00
tickstep f6bdb4ae60 set default key string to fix issue #86 2022-04-29 21:51:37 +08:00
tickstep 85c9cf0ecb update aliyunpan-api to fix issue #80 2022-04-29 21:39:11 +08:00
xiaoyaofenfen 32b57bf71f add album add file command 2022-04-29 14:43:41 +08:00
xiaoyaofenfen 9c7f9a81f6 add album remove file command 2022-04-26 14:11:11 +08:00
xiaoyaofenfen c8404260cc add album list file command 2022-04-26 12:01:03 +08:00
xiaoyaofenfen 24083e190f add album reanme command 2022-04-26 11:41:46 +08:00
xiaoyaofenfen 311f1fdd39 add album rm command 2022-04-26 11:35:20 +08:00
xiaoyaofenfen ab15ddf54e add album new command 2022-04-26 11:18:56 +08:00
xiaoyaofenfen b0a2ee41ae add album command 2022-04-26 10:39:19 +08:00
xiaoyaofenfen 6c89a01900 add JAVASCRIPT label for console log 2022-04-22 15:23:07 +08:00
tickstep 5a5ab464be add plugin sample file for build 2022-04-21 17:02:20 +08:00
tickstep 8750761aee add download file finish plugin hook 2022-04-21 16:53:08 +08:00
tickstep a318796f3b add download file plugin hook 2022-04-21 16:30:12 +08:00
tickstep 5572f844ed add upload file finish plugin hook 2022-04-20 23:16:04 +08:00
tickstep 0887b9f160 fix js handler func not existed bug 2022-04-20 22:05:50 +08:00
tickstep 182c28fdb3 update js sample file 2022-04-20 21:57:46 +08:00
tickstep 56a06d495e change plugin folder 2022-04-20 21:56:47 +08:00
xiaoyaofenfen c1115dfcc7 add upload prepare hook 2022-04-20 14:14:18 +08:00
xiaoyaofenfen 70443455de fix javascript log bug 2022-04-20 09:49:16 +08:00
xiaoyaofenfen e8c370cc86 add javascript plugin 2022-04-19 20:23:17 +08:00
xiaoyaofenfen e2fd95c557 restrict the upload/download parallel number to fix issue #67 2022-03-21 19:51:11 +08:00
tickstep 85eb4ce2c4 add illegal download file check to fix issue #61 2022-03-02 19:52:12 +08:00
tickstep 37e7bd07cf update version to v0.1.3 2022-02-13 22:48:00 +08:00
tickstep cc1fc526ef add file order & file direction option for file list 2022-02-13 22:32:21 +08:00
xiaoyaofenfen 9bffdc1cff support album drive for webdav service 2022-02-08 19:34:22 +08:00
tickstep b910776209 fix tokenId empty bug 2022-02-07 20:48:20 +08:00
tickstep ebbba140e3 update token service config 2022-02-01 19:05:27 +08:00
tickstep b6b71b45e3 add relogin action when token expired 2022-01-31 12:09:28 +08:00
tickstep ea82ec8320 fix qrcode login api issue 2022-01-31 10:52:55 +08:00
tickstep 04e414b65d support QR Code login way 2022-01-30 22:10:46 +08:00
tickstep 7e0d2155a8 add comments 2022-01-30 10:39:26 +08:00
tickstep 35ca753ad6 add qrcode login api 2022-01-30 10:36:01 +08:00
tickstep 7f53b9642c update version to v0.1.2 2022-01-24 22:17:33 +08:00
tickstep d36a764525 update build.sh script 2022-01-24 21:57:27 +08:00
tickstep studio team 159b403879 Merge pull request #45 from stamhe/main
build.sh 默认新增创建 out 目录的指令
2022-01-24 21:50:05 +08:00
xiaoyaofenfen 93eb1797c5 add time delay to fix issue #26 2022-01-24 16:34:33 +08:00
xiaoyaofenfen 1bbb6e2860 optimize download task to fix issue #47 2022-01-24 15:59:55 +08:00
xiaoyaofenfen 1af752ab98 note login command usage 2022-01-19 14:26:25 +08:00
xiaoyaofenfen b29f017bb3 add RefreshTokenUrl flag for login command 2022-01-19 10:35:22 +08:00
xiaoyaofenfen bed142f490 modify config note 2022-01-19 09:01:44 +08:00
Stam He 5e052308cc Merge remote-tracking branch 'tickstep/main' into main 2022-01-17 20:36:37 +08:00
xiaoyaofenfen 11c4c632d7 make update warning more readable 2022-01-17 20:02:18 +08:00
Stam He 2660ded630 build.sh 新增默认创建 out 目录的 功能 2022-01-16 21:54:46 +08:00
Stam He 645ef7f538 Merge branch 'tickstep:main' into main 2022-01-16 21:23:55 +08:00
tickstep a85cfe20ab update readme 2022-01-16 17:29:16 +08:00
tickstep ce69a563a1 update version to v0.1.1 2022-01-16 16:21:48 +08:00
tickstep 69189a8305 fix forbidden file download issue #35 2022-01-16 16:00:01 +08:00
tickstep d4873b94f7 fix query file info error issue for downlaoding file 2022-01-16 14:51:20 +08:00
tickstep dc248af2eb fix token expired bug for download file 2022-01-16 14:21:20 +08:00
Stam He d7e7aeb59c Merge remote-tracking branch 'tickstep/main' into main 2022-01-16 10:28:52 +08:00
tickstep 3011402bb1 remove useless flag for download command 2022-01-15 23:24:03 +08:00
tickstep c8f8e13407 add global speed indicator for download file 2022-01-15 22:17:23 +08:00
tickstep 3a4582a68d optimize file download speed 2022-01-15 21:50:28 +08:00
Stam He 7be3b16b25 Merge remote-tracking branch 'tickstep/main' into main 2022-01-15 20:21:33 +08:00
Stam He 416e167a8a add log 2022-01-15 20:03:47 +08:00
tickstep 9b5fe1da16 fix upload progress bar showing bug 2022-01-15 19:51:51 +08:00
tickstep studio team f41f70ed6b Merge pull request #43 from stamhe/main
优化上传速度
2022-01-15 19:36:35 +08:00
Stam He 8f1a32aa88 调整创建文件夹的方式 2022-01-15 17:17:03 +08:00
Stam He 45de57b30b add log3 2022-01-15 16:14:23 +08:00
Stam He 7327da6795 add log 2022-01-15 15:45:01 +08:00
Stam He dbe0efacbd add log 2022-01-15 15:39:54 +08:00
Stam He 4ae36af2b7 优化上传速度 2022-01-15 15:33:35 +08:00
tickstep 261b9d350c add some util method 2022-01-12 20:12:03 +08:00
xiaoyaofenfen b5e5fd7924 add history command 2022-01-12 15:55:09 +08:00
xiaoyaofenfen 5d10cb48ed skip sha1 compute when set norapid flag for upload 2022-01-12 11:34:07 +08:00
xiaoyaofenfen b055dc368c update readme file 2022-01-10 10:19:34 +08:00
tickstep 81a4c1f765 update version to v0.1.0 2022-01-08 23:00:07 +08:00
tickstep 4c89bbc652 user can specify share password for private share 2022-01-08 22:16:24 +08:00
tickstep 4cf51f24fe optimize upload file speed for webdav 2022-01-08 22:08:04 +08:00
tickstep 4b5a7e9f07 fix upload url renew bug 2022-01-08 21:57:28 +08:00
xiaoyaofenfen b63320ec54 update readme file 2022-01-08 09:51:27 +08:00
tickstep 45acb61b68 add global upload speed statistic 2022-01-07 23:41:22 +08:00
tickstep 583b6fe48d optimize uplaod file speed 2022-01-07 21:44:22 +08:00
tickstep 7c44f4e4a3 fix config dir detect error 2022-01-07 19:01:14 +08:00
xiaoyaofenfen 779ebd532f set default upload file parallel num as 20 2022-01-07 17:13:02 +08:00
xiaoyaofenfen 797ac7bbe6 fix upload url expired issue 2022-01-07 17:12:31 +08:00
xiaoyaofenfen b770e272e8 add more config dir automatically check 2022-01-06 11:29:07 +08:00
tickstep 094ad8f6a8 remove latest docker tag for docker build script 2022-01-05 22:23:12 +08:00
tickstep ddc8de2c17 update version to v0.0.9 2022-01-05 22:07:58 +08:00
xiaoyaofenfen d6f130a1b6 fix upload url expired bug for webdav 2022-01-05 17:02:05 +08:00
xiaoyaofenfen 7516ab8fff use cache token first for docker 2022-01-05 15:26:43 +08:00
tickstep 6590d34273 update version to v0.0.8 2022-01-04 23:01:23 +08:00
xiaoyaofenfen 36bb42bb3e use alternative time parse method to fix issue #39 2022-01-04 17:05:15 +08:00
xiaoyaofenfen d7fac821ed fix webdav upload zero file bug 2022-01-04 16:58:15 +08:00
xiaoyaofenfen 45f527f66d format log style 2022-01-04 16:33:44 +08:00
xiaoyaofenfen 9ccbb4e9a8 fix webdav list issue for windows 2022-01-04 16:23:09 +08:00
tickstep e8d998197f update readme file 2022-01-03 14:37:45 +08:00
tickstep aceee66709 update readme file 2022-01-03 14:34:39 +08:00
tickstep e48e95cd79 update readme file 2022-01-03 13:33:34 +08:00
tickstep b0c8b14edb update docker build script 2022-01-03 12:02:40 +08:00
tickstep 7f919569ad update version to v0.0.7 2022-01-03 11:20:44 +08:00
tickstep bc8f9358c0 update go mod file 2022-01-03 10:07:09 +08:00
tickstep 39c75a13ed add docker build script 2022-01-03 09:55:55 +08:00
tickstep 91b760670e update webdav usages 2022-01-03 07:55:03 +08:00
tickstep cfddc3db30 add webdav command 2022-01-02 18:44:45 +08:00
tickstep b9cbf78da4 fix webdav issue for root folder 2022-01-02 18:02:02 +08:00
tickstep 0c15220190 support pulic and private aliyun ECS url for webdav file download and upload 2022-01-02 16:05:39 +08:00
tickstep 4a421326b6 format code 2022-01-02 15:43:55 +08:00
tickstep a39fe17b93 add webdav upload file action 2022-01-02 15:32:10 +08:00
tickstep 999e5d7f39 add remove file for webdav 2022-01-01 11:14:21 +08:00
tickstep 32e552c6ff format code and add comments 2022-01-01 09:20:46 +08:00
tickstep b7b157947a fix webdav download file issue 2021-12-31 19:55:00 +08:00
tickstep f70a5b2fa0 add webdav download file action 2021-12-31 00:21:53 +08:00
tickstep a4a74f062e add webdav move file action 2021-12-30 23:15:42 +08:00
tickstep d867267756 support webdav rename action 2021-12-30 23:06:02 +08:00
feny 5a88effab9 add webdav file list function 2021-12-30 20:24:33 +08:00
tickstep 43686488a6 add url escape method 2021-12-29 19:33:46 +08:00
feny 2cdbf64b7a add webdav framework 2021-12-29 15:41:30 +08:00
feny 2a8b813e31 update readme 2021-11-20 14:31:21 +08:00
tickstep 445a3ac3f5 update readme 2021-11-13 17:48:04 +08:00
tickstep 37ec0437b1 update readme 2021-11-13 17:46:39 +08:00
171 changed files with 19111 additions and 3779 deletions
+31
View File
@@ -0,0 +1,31 @@
---
name: 问题反馈
about: 如果你在使用过程中发现问题,请使用该模板进行反馈。
labels: Bug
---
### Ⅰ. 使用环境
1. 操作系统:(Linux? macOS? Windows?)
2. aliyunpan版本号:(v0.3.6? v0.3.7?...)
### Ⅱ. 问题描述
### Ⅲ. 期望的结果
### Ⅳ. 如何复现问题
<!--
如果该问题是必现的,请帮忙整理复现步骤。
如果不是必现的,请尽可能说明问题出现之前你的操作步骤,越详细越好。
-->
1. xxx
2. xxx
3. xxx
### Ⅴ. 请提供相关的错误日志
<!-- 最好能提供debug日志。如何开启debug日志请参考README文档。 -->
```log
(日志请贴在这里。如果日志很多,可以以附件的形式上传)
```
@@ -0,0 +1,15 @@
---
name: 提新需求
about: 如果你想提出一个新需求,新功能建议,请使用此模板。
labels: Feature Request
---
### Ⅰ. 你当前使用环境
1. 操作系统:(Linux? macOS? Windows?)
2. aliyunpan版本号:(v0.3.6? v0.3.7?...)
### Ⅱ. 请描述你想要的新功能
<!-- 请简单描述你希望的新功能,例如:"upload上传命令添加一个xxx参数,用于实现xxx功能。" -->
### Ⅲ. 请描述新功能的使用场景
<!-- 可以讲讲你对新功能的看法,为什么需要增加这个功能,这个新功能在哪些场景下会使用。 -->
+4 -1
View File
@@ -41,4 +41,7 @@ main_test.go
license_header.sh
account.txt
git_push.sh
git_pull.sh
git_pull.sh
docker/sync/data
clear_docker.sh
upload.sh
+331 -544
View File
@@ -1,574 +1,351 @@
# 关于
阿里云盘CLI。仿 Linux shell 文件处理命令的阿里云盘命令行客户端。
# 重要提示
***由于阿里上传接口更改,目前上传文件需要原始文件的片段信息,故之前的"手动秒传","导入","导出","秒传链接分享"功能全部无法使用。新版本暂时屏蔽了该功能,请知悉。***
# 相关说明
1. 本项目还处于开发阶段,未经过充分的测试,如有bug或者好的建议欢迎提交issue
2. 由于阿里云盘还在内测中,后面功能和接口随时会被修改,相对应的,本工具相关功能也会被影响
3. 目前阶段优先处理Bug,功能增强的开发会相对往后安排
# 版本标签说明
1. arm / armv5 / armv7 : 适用32位ARM系统
2. arm64 : 适用64位ARM系统
3. 386 / x86 : 适用32系统,包括Intel和AMD的CPU系统
4. amd64 / x64 : 适用64位系统,包括Intel和AMD的CPU系统
5. mips : 适用MIPS指令集的CPU,例如国产龙芯CPU
6. macOS amd64适用Intel CPU的机器,macOS arm64目前主要是适用苹果M1芯片的机器
7. iOS arm64适用iPhone手机,并且必须是越狱的手机才能正常运行
# 目录
- [特色](#特色)
- [下载/运行 说明](#下载运行说明)
* [Windows](#windows)
* [Linux / macOS](#linux--macos)
- [命令列表及说明](#命令列表及说明)
* [注意](#注意)
* [检测程序更新](#检测程序更新)
* [查看帮助](#查看帮助)
* [登录阿里云盘帐号](#登录阿里云盘帐号)
* [列出帐号列表](#列出帐号列表)
* [获取当前帐号](#获取当前帐号)
* [切换阿里云盘帐号](#切换阿里云盘帐号)
* [退出阿里云盘帐号](#退出阿里云盘帐号)
* [刷新Token](#刷新Token)
* [切换网盘(文件/相册)](#切换网盘)
* [获取网盘配额](#获取网盘配额)
* [切换工作目录](#切换工作目录)
* [输出工作目录](#输出工作目录)
* [列出目录](#列出目录)
* [下载文件/目录](#下载文件目录)
* [上传文件/目录](#上传文件目录)
* [备份本地文件/目录](#备份本地文件目录)
* [手动秒传文件](#手动秒传文件)
* [创建目录](#创建目录)
* [删除文件/目录](#删除文件目录)
* [移动文件/目录](#移动文件目录)
* [重命名文件/目录](#重命名文件目录)
* [导出文件](#导出文件)
* [导入文件](#导入文件)
* [分享文件/目录](#分享文件目录)
+ [设置分享文件/目录](#设置分享文件目录)
+ [列出已分享文件/目录](#列出已分享文件目录)
+ [取消分享文件/目录](#取消分享文件目录)
+ [分享秒传链接](#分享秒传链接)
* [显示和修改程序配置项](#显示和修改程序配置项)
- [常见问题Q&A](#常见问题Q&A)
* [1. 如何获取RefreshToken](#1-如何获取RefreshToken)
* [2. 如何开启Debug调试日志](#1-如何开启Debug调试日志)
- [交流反馈](#交流反馈)
- [鸣谢](#鸣谢)
阿里云盘CLI。仿 Linux shell 文件处理命令的阿里云盘命令行客户端,支持JavaScript插件,支持同步备份功能,支持相册批量下载。
# 特色
1. 多平台支持, 支持 Windows, macOS, linux(x86/x64/arm), android, iOS 等
2. 阿里云盘多用户支持
3. 支持文件网盘,相册网盘无缝切换
4. 支持导入/导出功能,快速备份(导出)和恢复(导入)网盘的文件
5. [下载](#下载文件目录)网盘内文件, 支持多个文件或目录下载, 支持断点续传和单文件并行下载
6. [上传](#上传文件目录)本地文件, 支持多个文件或目录上传,支持排除指定文件夹/文件(正则表达式)功能
7. [备份本地文件](#备份本地文件目录)支持备份本地文件夹到网盘中,保持本地文件和网盘文件同步。常用于嵌入式或者NAS等设备。
8. 命令和文件路径输入支持Tab键自动补全
3. 支持备份盘,资源库无缝切换
4. [下载](docs/manual.md#下载文件目录)网盘内文件, 支持多个文件或目录下载, 支持断点续传和单文件并行下载。支持软链接(符号链接)文件。
5. [上传](docs/manual.md#上传文件目录)本地文件, 支持多个文件或目录上传,支持排除指定文件夹/文件(正则表达式)功能。支持软链接(符号链接)文件。
6. [同步备份功能](docs/manual.md#同步备份功能)支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份保持本地文件和网盘文件同步。常用于嵌入式或者NAS等设备,支持docker镜像部署。
7. 命令和文件路径输入支持Tab键自动补全,路径支持通配符匹配模式
8. 支持[JavaScript插件](docs/plugin_manual.md#简介),你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求
9. 支持共享相册的相关操作,支持批量下载相册所有普通照片、实况照片文件到本地
10. 支持多用户联合下载功能,对下载速度有极致追求的用户可以尝试使用该选项。详情请查看文档[多用户联合下载](docs/manual.md#多用户联合下载)
# 下载/运行说明
# 阿里云盘VIP会员推荐码
如果大家有打算开通阿里云盘VIP会员,可以使用阿里云盘APP扫描下面的优惠推荐码进行开通。
注意:您需要开通【三方应用权益包】,这样使用本程序下载才能加速,否则下载无法提速。
![](./assets/images/aliyunpan_cps_qrcode.png)
可以直接在[发布页](https://github.com/tickstep/aliyunpan/releases)下载使用.
# 目录
- [关于](#关于)
- [特色](#特色)
- [目录](#目录)
- [如何安装](#如何安装)
- [直接下载安装](#直接下载安装)
- [apt安装](#apt安装)
- [yum安装](#yum安装)
- [brew安装](#brew安装)
- [winget安装](#winget安装)
- [docker安装](#docker安装)
- [sync同步盘](#sync同步盘)
- [如何使用](#如何使用)
- [基本使用](#基本使用)
- [修改配置目录](#修改配置目录)
- [启动程序](#启动程序)
- [查看帮助](#查看帮助)
- [登录](#登录)
- [切换网盘](#切换网盘)
- [查看文件列表](#查看文件列表)
- [下载文件](#下载文件)
- [上传文件](#上传文件)
- [同步备份文件](#同步备份文件)
- [更多命令](#更多命令)
- [常见问题](#常见问题)
- [如何开启Debug调试日志](#如何开启debug调试日志)
- [第一步](#第一步)
- [第二步](#第二步)
- [如何登出和下线客户端](#如何登出和下线客户端)
- [交流反馈](#交流反馈)
- [鸣谢](#鸣谢)
也可以使用这个天翼云盘分享链接(阿里云盘还不支持zip压缩包分享)进行下载:[下载链接](https://cloud.189.cn/t/z2auuiFfYb2i)(访问码:5o9b)
# 如何安装
## 直接下载安装
可以直接在本仓库 [发布页](https://github.com/tickstep/aliyunpan/releases) 下载安装包,解压后使用。
要特别注意安装包的标签,不同的标签对应不同架构的系统,相关版本文件的标签说明如下:
1. arm / armv5 / armv7 : 适用32位ARM系统
2. arm64 : 适用64位ARM系统
3. 386 / x86 : 适用32系统,包括Intel和AMD的CPU系统
4. amd64 / x64 : 适用64位系统,包括Intel和AMD的CPU系统
5. mips : 适用MIPS指令集的CPU,例如中标麒麟的系统。MIPS要注意大小端字序的不同对应的版本也不同。
6. macOS amd64适用Intel CPU的机器,macOS arm64目前主要是适用苹果M系列芯片的机器
7. iOS arm64适用iPhone手机,并且必须是越狱的手机才能正常运行
8. loong64 : 适用于LoongArch64架构的机器,例如国产龙芯CPU
如果程序运行时输出乱码, 请检查下终端的编码方式是否为 `UTF-8`.
使用本程序之前, 非常建议先学习一些 linux 基础命令知识.
如果没有带任何参数运行程序, 程序将会进入仿Linux shell系统用户界面的cli交互模式, 可直接运行相关命令.
cli交互模式下, 光标所在行的前缀应为 `aliyunpan >`, 如果登录了帐号则格式为 `aliyunpan:<工作目录> <用户昵称>$ `
程序会提供相关命令的使用说明.
## Windows
程序应在 命令提示符 (Command Prompt) 或 PowerShell 中运行.
也可直接双击程序运行, 具体使用方法请参见 [命令列表及说明](#命令列表及说明).
## Linux / macOS
程序应在 终端 (Terminal) 运行.
具体使用方法请参见 [命令列表及说明](#命令列表及说明) .
# 命令列表及说明
## 注意
命令的前缀 `aliyunpan` 为指向程序运行的全路径名 (ARGv 的第一个参数)
直接运行程序时, 未带任何其他参数, 则程序进入cli交互模式, 进入cli模式运行以下命令时要把命令的前缀 `aliyunpan` 去掉! 即不需要输入`aliyunpan`。
cli交互模式支持按tab键自动补全命令.
## 检测程序更新
```
aliyunpan update
参考例子:
```shell
wget https://github.com/tickstep/aliyunpan/releases/download/v0.4.0/aliyunpan-v0.4.0-linux-amd64.zip
unzip aliyunpan-v0.4.0-linux-amd64.zip
cd aliyunpan-v0.4.0-linux-amd64
./aliyunpan
```
## 查看帮助
```
aliyunpan help
```
### 例子
```
列出程序支持的命令
aliyunpan help
查看login命令的帮助手册
aliyunpan help login
## apt安装
适用于apt包管理器的系统,例如Ubuntu,国产deepin深度操作系统等。目前只支持amd64和arm64架构的机器。
```shell
sudo curl -fsSL http://file.tickstep.com/apt/pgp | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/tickstep-packages-archive-keyring.gpg > /dev/null && echo "deb [signed-by=/etc/apt/trusted.gpg.d/tickstep-packages-archive-keyring.gpg arch=amd64,arm64] http://file.tickstep.com/apt aliyunpan main" | sudo tee /etc/apt/sources.list.d/tickstep-aliyunpan.list > /dev/null && sudo apt-get update && sudo apt-get install -y aliyunpan
```
## 登录阿里云盘帐号
## yum安装
适用于yum包管理器的系统,例如CentOS、RockyLinux等。目前只支持amd64和arm64架构的机器。
```shell
sudo curl -fsSL http://file.tickstep.com/rpm/aliyunpan/aliyunpan.repo | sudo tee /etc/yum.repos.d/tickstep-aliyunpan.repo > /dev/null && sudo yum install aliyunpan -y
```
## brew安装
适用于brew包管理器的系统,主要是苹果macOS系统。目前只支持amd64和arm64架构(Apple Silicon)的机器。
```shell
brew install aliyunpan
```
由于brew默认安装在系统目录下面,这样配置文件也默认存放在系统目录里了,建议设置系统变量进行配置文件的单独存储,例如
```shell
export ALIYUNPAN_CONFIG_DIR=/Users/tickstep/Applications/adrive/config
```
## winget安装
适用于Windows系统的winget包管理器。目前只支持x86和x64架构的机器。
更新源(可选)
```powershell
winget source update
```
安装
```powershell
winget install tickstep.aliyunpan --source winget --silent
```
## docker安装
### sync同步盘
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地两种模式。支持JavaScript插件对备份文件进行过滤。
备份功能支持以下模式:
1. 备份本地文件,即上传本地文件到网盘,始终保持本地文件有一个完整的备份在网盘
2. 备份云盘文件,即下载网盘文件到本地,始终保持网盘的文件有一个完整的备份在本地
备份功能支持指定备份策略:
1. exclusive,排他备份文件(一比一镜像备份),目标目录多余的文件会被删除。保证备份的源目录,和目标目录文件一比一备份。源目录文件如果文件被删除,则对应的目标目录的文件也会被删除。
2. increment,增量备份文件,目标目录多余的文件不会被删除。只会把源目录修改的文件,新增的文件备份到目标目录。如果源目录有文件删除,或者目标目录有其他文件新增是不会被删除。
同步的基本逻辑如下所示,一次循环包括:扫描-对比-执行,一共三个环节。
![](./assets/images/sync_command-basic_logic.jpg)
```
docker run -d --name=aliyunpan-sync --restart=always -v "<your aliyunpan_config.json>:/home/app/config/aliyunpan_config.json" -v "<your local dir>:/home/app/data" -e ALIYUNPAN_PAN_DIR="<your drive pan dir>" -e ALIYUNPAN_SYNC_MODE="upload" -e ALIYUNPAN_SYNC_POLICY="increment" -e ALIYUNPAN_SYNC_DRIVE="backup" -e ALIYUNPAN_SYNC_LOG="true" tickstep/aliyunpan-sync:v0.4.0
<your aliyunpan_config.json>: 用户已经登录成功并保存好的aliyunpan_config.json凭据文件
<your local dir>:本地目标目录,绝对路径,例如:/tickstep/Documents/设计文档
ALIYUNPAN_PAN_DIR:云盘目标目录,绝对路径
ALIYUNPAN_SYNC_MODE:备份模式,支持: upload(备份本地文件到云盘),download(备份云盘文件到本地)
ALIYUNPAN_SYNC_POLICY:备份策略,支持:exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)
ALIYUNPAN_SYNC_DRIVE: 网盘,支持:backup(备份盘), resource(资源盘)
ALIYUNPAN_SYNC_LOG: 同步日志,true-开启同步日志显示,false-关闭同步日志
```
docker-compose启动方式请查看:[docker-compose.yml](https://github.com/tickstep/aliyunpan/blob/main/docs/manual.md#Docker%E8%BF%90%E8%A1%8C)
更详情文档请参考dockerhub网址:[tickstep/aliyunpan-sync](https://hub.docker.com/r/tickstep/aliyunpan-sync)
# 如何使用
完整和详细的命令说明请查看手册:[命令手册](docs/manual.md)
1. Windows
程序应在 命令提示符 (Command Prompt) 或 PowerShell 中运行.
也可直接双击程序运行, 具体使用方法请参见 [命令列表及说明](docs/manual.md#命令列表及说明)
2. Linux / macOS
程序应在 终端 (Terminal) 运行.
具体使用方法请参见 [命令列表及说明](docs/manual.md#命令列表及说明)
如果程序运行时输出乱码, 请检查下终端的编码方式是否为 `UTF-8`.
如果没有带任何参数运行程序, 程序将会进入仿Linux shell系统用户界面的CLI交互模式, 可直接运行相关命令.
在交互模式下, 光标所在行的前缀应为 `aliyunpan >`, 如果登录了帐号则格式为 `aliyunpan:<工作目录> <用户昵称>$ `
程序内置了相关命令的使用说明,你可以通过运行`命令 -h`的方式获取命令的使用说明,例如:`upload -h`获取上传命令的使用说明。
## 基本使用
本程序支持阿里云盘大多数命令操作,这里只介绍基本的使用,更多更详细的命令请查看手册:[命令手册](docs/manual.md)。
### 修改配置目录
你可以指定程序配置文件的存储路径,如果没有指定,程序会使用默认的目录。
方法为设置环境变量`ALIYUNPAN_CONFIG_DIR`并指定一个存在的目录,例如linux下面可以这样指定
```shell
export ALIYUNPAN_CONFIG_DIR=/home/tickstep/tools/aliyunpan/config
```
### 启动程序
直接启动进入交互命令行
```shell
[tickstep@MacPro ~]$ aliyunpan
提示: 方向键上下可切换历史命令.
提示: Ctrl + A / E 跳转命令 首 / 尾.
提示: 输入 help 获取帮助.
aliyunpan >
```
### 查看帮助
```shell
aliyunpan > help
...
阿里云盘:
album, abm 相簿(Beta)
cd 切换工作目录
download, d 下载文件/目录
ls, l, ll 列出目录
mkdir 创建目录
mv 移动文件/目录
pwd 输出工作目录
recycle 回收站
rename 重命名文件
rm 删除文件/目录
share 分享文件/目录
sync 同步备份功能
upload, u 上传文件/目录
...
```
### 登录
当前支持使用RefreshToken进行登录。RefreshToken请参考 [1. 如何获取RefreshToken](#1-如何获取RefreshToken) 获取
```
aliyunpan login
```
### 例子
```
按照引导步骤登录
aliyunpan login
请输入RefreshToken, 回车键提交 > 626a27b6193f4c5ca6ef0.......
命令行指定RefreshToken登录
aliyunpan login -RefreshToken=626a27b6193f4c5ca6ef0.......
```
## 列出帐号列表
```
aliyunpan loglist
```
列出所有已登录的帐号
## 获取当前帐号
```
aliyunpan who
```
## 切换阿里云盘帐号
切换已登录的帐号
```
aliyunpan su <uid>
```
```
aliyunpan su
请输入要切换帐号的 # 值 >
```
## 退出阿里云盘帐号
退出当前登录的帐号
```
aliyunpan logout
```
程序会进一步确认退出帐号, 防止误操作.
## 刷新Token
由于阿里云盘的RefreshToken是会过期的,为了延长最大过期时间,需要定期刷新Token,建议每小时刷新一次。
调用该命令可以自动刷新RefreshToken并保存到配置文件中,但是有一个前提,即Token必须还没有过期,如果Token已经过期是无法刷新的则只能重新登录。
```
刷新当前登录用户
aliyunpan token update
刷新所有登录的用户
aliyunpan token update -mode 2
```
如果你的aliyunpan工具是在Linux中运行,则建议你使用crontab定时任务进行Token自动刷新,例如
```
每小时执行一次Token刷新任务
*/60 * * * * /<your path>/aliyunpan token update -mode 2
```
## 切换网盘
程序默认工作在文件网盘下,如需切换到相册网盘,可以使用本命令进行切换。
```
aliyunpan drive <driveId>
```
```
aliyunpan drive
输入要切换的网盘 # 值 >
```
## 获取网盘配额
```
aliyunpan quota
```
获取网盘的总储存空间, 和已使用的储存空间
## 切换工作目录
```
aliyunpan cd <目录>
```
### 例子
```
# 切换 /我的文档 工作目录
aliyunpan cd /我的文档
# 切换 上级目录
aliyunpan cd ..
# 切换 根目录
aliyunpan cd /
```
## 输出工作目录
```
aliyunpan pwd
```
## 列出目录
列出当前工作目录的文件和目录或指定目录
```
aliyunpan ls
```
```
aliyunpan ls <目录>
```
### 可选参数
```
-driveId value 网盘ID
```
### 例子
```
# 列出 我的文档 内的文件和目录
aliyunpan ls 我的文档
# 绝对路径
aliyunpan ls /我的文档
# 详细列出 我的文档 内的文件和目录
aliyunpan ll /我的文档
```
## 下载文件/目录
```
aliyunpan download <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
aliyunpan d <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
```
### 可选参数
```
--ow overwrite, 覆盖已存在的文件
--status 输出所有线程的工作状态
--save 将下载的文件直接保存到当前工作目录
--saveto value 将下载的文件直接保存到指定的目录
-x 为文件加上执行权限, (windows系统无效)
-p value 指定下载线程数 (default: 0)
-l value 指定同时进行下载文件的数量 (default: 0)
--retry value 下载失败最大重试次数 (default: 3)
--nocheck 下载文件完成后不校验文件
--exn value 指定排除的文件夹或者文件的名称,只支持正则表达式。支持排除多个名称,每一个名称就是一个exn参数
```
### 例子
```
# 设置保存目录, 保存到 D:\Downloads
# 注意区别反斜杠 "\" 和 斜杠 "/" !!!
aliyunpan config set -savedir D:/Downloads
# 下载 /我的文档/1.mp4
aliyunpan d /我的文档/1.mp4
# 下载 /我的文档 整个目录!!
aliyunpan d /我的文档
```
下载的文件默认保存到 **程序所在目录** 的 download/ 目录, 支持设置指定目录, 重名的文件会自动跳过!
通过 `aliyunpan config set -savedir <savedir>` 可以自定义保存的目录.
支持多个文件或目录下载.
自动跳过下载重名的文件!
## 上传文件/目录
```
aliyunpan upload <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>
aliyunpan u <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>
```
### 例子:
```
# 将本地的 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录
# 注意区别反斜杠 "\" 和 斜杠 "/" !!!
aliyunpan upload C:/Users/Administrator/Desktop/1.mp4 /视频
# 将本地的 C:\Users\Administrator\Desktop\1.mp4 和 C:\Users\Administrator\Desktop\2.mp4 上传到网盘 /视频 目录
aliyunpan upload C:/Users/Administrator/Desktop/1.mp4 C:/Users/Administrator/Desktop/2.mp4 /视频
# 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录
aliyunpan upload C:/Users/Administrator/Desktop /视频
## 下面演示文件或者文件夹排除功能
# 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
aliyunpan upload -exn "\.jpg$" C:/Users/Administrator/Video /视频
# 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
aliyunpan upload -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视频
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式
1)排除@eadir文件或者文件夹:-exn "^@eadir$"
2)排除.jpg文件:-exn "\.jpg$"
3)排除.号开头的文件:-exn "^\."
4)排除~号开头的文件:-exn "^~"
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
```
## 备份本地文件/目录
本地备份功能一般用于NAS等系统,日常只进行增量备份操作,默认情况下本地删除不影响网盘文件。
比如在手机上备分照片目录,就可以使用这个功能定时备份,备份完成后本地文件可安全删除。
基本用法和`upload`命令一样,额外增加两个参数
>1. delete 用于同步删除操作(只通过本地数据库记录对比)。
>2. sync 使网盘上的文件和本地文件同步(本地为主),会删除网盘中不存在于本地的文件或目录(速度比较慢)。
>sync 和 delete 的区别在于sync是通过网盘文件列表和本地文件进行判断。
>delete 只通过本地数据库进行判断,正常情况下使用 delete 就够用了。
和 `upload` 相比由于增加了本地数据库,可以快速判断文件是否有更新等,大大减少了 API 的调用,操作速度更快。
```
aliyunpan backup <本地目录1> <目录2> <目录3> ... <目标目录>
注:
1. 默认不删除网盘文件,可用 delete 或 sync 参数进行同步删除。
```
### 例子:
```
# 将本地的 C:\Users\Administrator\Desktop 备份到网盘 /test 目录
# 注意区别反斜杠 "\" 和 斜杠 "/" !!!
aliyunpan backup C:/Users/Administrator/Desktop /test
# 将本地的 C:\Users\Administrator\Desktop 备份到网盘 /test 目录,但排除所有 @eadir 目录
aliyunpan backup -exn "^@eadir$" C:/Users/Administrator/Desktop /test
```
## 手动秒传上传文件
通过秒传链接上传文件到网盘,秒传链接可以通过share命令获取
```
aliyunpan rapidupload <秒传链接1> <秒传链接2> <秒传链接3> ...
```
### 例子:
```
# 如果秒传成功, 则保存到网盘路径 /file.dmg
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|"
# 如果秒传成功, 则保存到网盘路径 /pan_folder/file.dmg
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder"
```
## 创建目录
```
aliyunpan mkdir <目录>
```
### 例子
```
aliyunpan mkdir test123
```
## 删除文件/目录
```
aliyunpan rm <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
```
注意: 删除多个文件和目录时, 请确保每一个文件和目录都存在, 否则删除操作会失败.
被删除的文件或目录可在网盘文件回收站找回.
### 例子
```
# 删除 /我的文档/1.mp4
aliyunpan rm /我的文档/1.mp4
# 删除 /我的文档/1.mp4 和 /我的文档/2.mp4
aliyunpan rm /我的文档/1.mp4 /我的文档/2.mp4
# 删除 /我的文档 整个目录 !!
aliyunpan rm /我的文档
```
## 移动文件/目录
```
aliyunpan mv <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>
```
注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败.
### 例子
```
# 将 /我的文档/1.mp4 移动到 根目录 /
aliyunpan mv /我的文档/1.mp4 /
```
## 重命名文件/目录
```
aliyunpan rename <旧文件/目录名> <新文件/目录名>
```
注意: 重命名的文件/目录,如果指定的是绝对路径,则必须保证新旧的绝对路径在同一个文件夹内,否则重命名失败!
### 例子
```
# 将 /我的文档/1.mp4 重命名为 /我的文档/2.mp4
aliyunpan rename /我的文档/1.mp4 /我的文档/2.mp4
```
## 导出文件
导出文件主要是用于备份网盘的文件。通过该命令将网盘的文件元数据信息导出并保存到本地文件中,等到以后需要的时候再通过import命令导入到网盘中。
```
aliyunpan export <网盘文件/目录的路径1> <文件/目录2> <文件/目录3> ... <本地保存文件路径>
```
导出指定文件/目录下面的所有文件的元数据信息,并保存到指定的本地文件里面。导出的文件元信息可以使用 import 命令(秒传文件功能)导入到网盘中。
### 例子
```
导出 /我的资源 整个目录 元数据到文件 /Users/tickstep/Downloads/export_files.txt
aliyunpan export /我的资源 /Users/tickstep/Downloads/export_files.txt
导出 网盘 整个目录 元数据到文件 /Users/tickstep/Downloads/export_files.txt
aliyunpan export / /Users/tickstep/Downloads/export_files.txt
```
## 导入文件
```
aliyunpan export <本地元数据文件路径>
```
导入文件中记录的元数据文件到网盘。保存到网盘的文件会使用文件元数据记录的路径位置,如果没有指定云盘目录(saveto)则默认导入到目录 aliyunpan 中。
导入的文件可以使用 export 命令获得。
导入文件每一行是一个文件元数据,样例如下:
```
aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|
```
需要先登录,已经登录过的可以跳过此步。登录是在浏览器上进行,你需要进行两次登录。
```shell
aliyunpan > login
请在浏览器打开以下链接进行登录,链接有效时间为5分钟。
注意:你需要进行一次授权一次扫码的两次登录。
https://openapi.alipan.com/oauth/authorize?client_id=cf9f70e8fc61430f8ec5ab5cadf31375&redirect_uri=https%3A%2F%2Fapi.tickstep.com%2Fauth%2Ftickstep%2Faliyunpan%2Ftoken%2Fopenapi%2F8206f0.....fb5db6b40336%2Fauth&scope=user:base,file:all:read,file:all:write
阿里云盘登录成功: tickstep
aliyunpan:/ tickstep$
```
### 切换网盘
支持切换备份盘、资源库,默认工作在备份盘下。
```shell
aliyunpan:/ tickstep(备份盘)$ drive
# DRIVE ID 网盘名称
1 11519221 备份盘
2 1311893110 资源库
### 例子
```
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项
aliyunpan import /Users/tickstep/Downloads/export_files.txt
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项并保存到目录 /my2020 中
aliyunpan import -saveto=/my2020 /Users/tickstep/Downloads/export_files.txt
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项并保存到网盘根目录 / 中
aliyunpan import -saveto=/ /Users/tickstep/Downloads/export_files.txt
输入要切换的网盘 # 值 > 2
切换到网盘:资源库
aliyunpan:/ tickstep(资源库)$
```
## 分享文件/目录
```
aliyunpan share
### 查看文件列表
```shell
aliyunpan:/ tickstep$ ls
# 文件大小 修改日期 文件(目录)
0 - 2021-11-03 13:32:22 临时/
1 - 2021-07-10 07:44:34 好友的分享/
2 - 2021-07-09 22:11:22 我的项目/
3 - 2021-07-09 22:10:37 我的游戏/
4 - 2021-07-09 22:10:10 我的文档/
5 349.86KB 2021-06-06 11:46:02 使用统计.xls
6 503.57KB 2021-06-06 11:46:02 IMG_0098.JPG
7 72.20KB 2021-06-06 11:46:02 IMG_0103.PNG
总: 3.20MB 文件总数: 3, 目录总数: 7
----
```
### 设置分享文件/目录
阿里目前之支持少数文件类型的分享,不支持的文件分享会提示分享失败
### 下载文件
如果需要更改默认下载目录路径,可以通过 `aliyunpan config set -savedir <savedir>` 自定义下载文件保存的目录。
```shell
aliyunpan:/ tickstep$ download IMG_0106.JPG
[0] 当前文件下载最大并发量为: 5, 下载缓存为: 64.00KB
[1] 加入下载队列: /IMG_0106.JPG
[1] ----
文件ID: 60bc44f855814e19692a4958b4a8823a1a06e5de
文件名: IMG_0106.JPG
文件类型: 文件
文件路径: /IMG_0106.JPG
[1] 准备下载: /IMG_0106.JPG
[1] 将会下载到路径: /root/Downloads/4d001d48564f43b..62874f04bbe6/IMG_0106.JPG
[1] 下载开始
[1] ↓ 704.00KB/1.48MB 0B/s(1.69MB/s) in 1.88s, left - ............
[1] 下载完成, 保存位置: /root/Downloads/4d001d48564f43b..62874f04bbe6/IMG_0106.JPG
[1] 检验文件有效性成功: /root/Downloads/4d001d48564f43b..62874f04bbe6/IMG_0106.JPG
下载结束, 时间: 4秒, 数据总量: 1.48MB
```
aliyunpan share set <文件/目录1> <文件/目录2> ...
aliyunpan share s <文件/目录1> <文件/目录2> ...
或者使用UI面板展示下载进度(使用-ui选项),更加直观和友好。
```shell
aliyunpan:/ tickstep$ download /photo -ui
┌──────────────────────────────────────────────────────────────────────────────────────────┐
│总速度: 341.59 KB/s | 文件: 0/7 | 失败: 0 | 状态: 下载中 | 已用时间: 9s | 剩余时间: 51s │
│总进度: [===> ] 7.4% (896.00KB/11.83MB) │
│──────────────────────────────────────────────────────────────────────────────────────────│
│ 1. IMG_0005.JPG [============> ] 66.9% 258.42KB/s 3s │
│ 2. IMG_0007.JPG [============> ] 67.3% 258.54KB/s 3s │
│ 3. IMG_0009.JPG [ ] 0.0% 等待中 - │
│──────────────────────────────────────────────────────────────────────────────────────────│
│[10:39:24] [0] 当前文件下载最大并发量为: 2, 单文件下载分片线程数为: 1, 下载缓存为: 64.00KB │
│[10:39:27] [2] 加入下载队列: /photo/p/IMG_0007.JPG │
│[10:39:27] [2] ---- 文件ID: 67dfe476a7573e76a2034626bd013f36581296ed 文件名: IMG_0007.... │
│[10:39:27] [3] 准备下载: /photo/p/IMG_0007.JPG │
│[10:39:27] [2] 将会下载到路径: /Users/tickstep/Downloads/4d001d48564f43b3bc5662874f04bb... │
│[10:39:28] [2] 下载开始 │
└──────────────────────────────────────────────────────────────────────────────────────────┘
```
### 列出已分享文件/目录
### 上传文件
```shell
aliyunpan:/ tickstep$ upload /Users/tickstep/Downloads/apt.zip /tmp
[0] 当前文件上传最大并发量为: 10, 上传分片大小为: 10.00MB
[1] 加入上传队列: /Users/tickstep/Downloads/apt.zip
[1] 2022-08-13 13:41:22 准备上传: /Users/tickstep/Downloads/apt.zip => /tmp/apt.zip
[1] 2022-08-13 13:41:22 正在检测和创建云盘文件夹: /tmp
[1] 2022-08-13 13:41:22 正在计算文件SHA1: /Users/tickstep/Downloads/apt.zip
[1] 2022-08-13 13:41:22 检测秒传中, 请稍候...
[1] 2022-08-13 13:41:22 秒传失败,开始正常上传文件
[1] ↑ 21.00MB/21.00MB 702.53KB/s(702.70KB/s) in 15s ............
[1] 2022-08-13 13:41:22 上传文件成功, 保存到网盘路径: /tmp/apt.zip
[1] 2022-08-13 13:41:22 文件上传结果: 成功! 耗时 18秒
上传结束, 时间: 18秒, 数据总量: 21.00MB
```
aliyunpan share list
aliyunpan share l
或者使用UI面板展示上传进度(使用-ui选项),更加直观和友好。
```shell
aliyunpan:/ tickstep$ upload /Users/tickstep/Downloads /photo -ui
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│总速度: 3.62 MB/s | 文件: 9/14 | 失败: 0 | 状态: 上传中 | 已用时间: 12s | 剩余时间: 17s │
│总进度: [==========================> ] 51.7% (68.56MB/132.73MB) │
│─────────────────────────────────────────────────────────────────────────────────────────│
│ 1. System Design Interview 2.zip [====> ] 26.2% 3.25MB/s 18s │
│ 2. ips_20260228.txt [ ] 0.0% 0 B/s - │
│ 3. neko-agent [ ] 0.0% 0 B/s - │
│─────────────────────────────────────────────────────────────────────────────────────────│
│[11:14:49] [9] 2026-03-02 11:14:26 正在计算文件SHA1: /Users/tickstep/Downloads/test/da... │
│[11:14:49] [13] 2026-03-02 11:14:26 正在检测和创建云盘文件夹: /photo/p2/test/output │
│[11:14:49] [9] 2026-03-02 11:14:26 检测秒传中, 请稍候... │
│[11:14:49] [9] 2026-03-02 11:14:26 秒传成功, 保存到网盘路径: /photo/p2/test/data/ips_2... │
│[11:14:49] [9] 2026-03-02 11:14:26 文件上传结果: 成功! 耗时 12秒 │
│[11:14:49] 完成: /Users/tickstep/Downloads/test/data/ips_20260226.txt │
└─────────────────────────────────────────────────────────────────────────────────────────┘
```
### 取消分享文件/目录
```
aliyunpan share cancel <shareid_1> <shareid_2> ...
aliyunpan share c <shareid_1> <shareid_2> ...
```
目前只支持通过分享id (shareid) 来取消分享.
### 同步备份文件
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地两种模式。支持JavaScript插件对备份文件进行过滤。
例如:将本地目录 `/tickstep/Documents/设计文档` 中的文件备份上传到云盘目录 `/备份盘/我的文档`
```shell
aliyunpan:/ tickstep$ sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload" -drive "backup"
启动同步备份进程
备份配置文件:(使用命令行配置)
链接类型:默认链接
下载并发:2
上传并发:2
下载分片大小:1.00MB
上传分片大小:10.00MB
### 分享秒传链接
秒传链接支持所有类型的文件分享,可以突破阿里的分享限制。得到的链接可以使用import或者rapidupload命令保存到自己网盘中。
秒传链接只支持文件分享,不支持文件夹。如果指定文件夹会创建文件夹下所有文件的秒传链接。
```
aliyunpan share mc <文件/目录1> <文件/目录2> ...
例子
# 创建文件 1.mp4 的秒传链接
aliyunpan share mc 1.mp4
# 创建文件 1.mp4 的秒传链接,但链接隐藏相对路径
aliyunpan share mc -hp 1.mp4
# 创建文件夹 share_folder 下面所有文件的秒传链接
aliyunpan share mc share_folder/
启动同步任务
任务: 设计文档(de3d6b69a607497b73624bcca0845f19)
同步模式: 备份本地文件(只上传)
本地目录: /tickstep/Documents/设计文档
云盘目录: /备份盘/我的文档
```
## 更多命令
更多更详细的命令请查看手册:[命令手册](docs/manual.md)。
## 显示和修改程序配置项
```
# 显示配置
aliyunpan config
# 设置配置
aliyunpan config set
```
### 例子
```
# 显示所有可以设置的值
aliyunpan config -h
aliyunpan config set -h
# 设置下载文件的储存目录
aliyunpan config set -savedir D:/Downloads
# 设置下载最大并发量为 15
aliyunpan config set -max_download_parallel 15
# 组合设置
aliyunpan config set -max_download_parallel 15 -savedir D:/Downloads
# 设置使用阿里云内部URL链接,专供阿里云ECS环境使用
# 开启内部URL链接可以使用阿里云ECS私网带宽流量,而不用使用宝贵的公网带宽流量,如果你在阿里ECS环境中使用本工具,建议开启
aliyunpan config set -transfer_url_type 2
```
# 常见问题Q&A
## 1 如何获取RefreshToken
需要通过浏览器获取refresh_token。这里以Chrome浏览器为例,其他浏览器类似。
打开 [阿里云盘网页](https://www.aliyundrive.com/drive) 并进行登录,然后F12按键打开浏览器调试菜单,按照下面步骤进行
![](./assets/images/how-to-get-refresh-token.png)
## 2 如何开启Debug调试日志
# 常见问题
## 如何开启Debug调试日志
当需要定位问题,或者提交issue的时候抓取log,则需要开启debug日志。步骤如下:
### 第一步
@@ -580,19 +357,29 @@ export ALIYUNPAN_VERBOSE=1
Windows
不同版本会有些许不一样,请自行查询具体方法
设置示意图如下:
设置示意图如下:
![](./assets/images/win10-env-debug-config.png)
### 第二步
打开aliyunpan命令行程序,任何云盘命令都有类似如下日志输出
打开aliyunpan命令行程序,任何云盘命令都有类似如下日志输出
![](./assets/images/debug-log-screenshot.png)
## 如何登出和下线客户端
阿里云盘单账户最多只允许同时登录 10 台设备
当出现这个提示:你账号已超出最大登录设备数量,请先下线一台设备,然后重启本应用,才可以继续使用
说明你的账号登录客户端已经超过数量,你需要先登出其他客户端才能继续使用,如下所示
APP手机端
![](./assets/images/app-deregister-device.png)
Web网页端
![](./assets/images/web-deregister-device.png)
# 交流反馈
提交issue: [issues页面](https://github.com/tickstep/aliyunpan/issues)
联系邮箱: tickstep@outlook.com
# 鸣谢
本项目大量借鉴了以下相关项目的功能&成果
> [tickstep/cloudpan189-go](https://github.com/tickstep/cloudpan189-go)
> [tickstep/aliyunpan-api](https://github.com/tickstep/aliyunpan-api)
> [tickstep/cloudpan189-go](https://github.com/tickstep/cloudpan189-go)
> [kazutoiris/ali_ecc](https://github.com/kazutoiris/ali_ecc)
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity version="0.0.6.0" processorArchitecture="*" name="com.tickstep.aliyunpan" type="win32"/>
<assemblyIdentity version="0.4.0.0" processorArchitecture="*" name="com.tickstep.aliyunpan" type="win32"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
BIN
View File
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 92 KiB

+146
View File
@@ -0,0 +1,146 @@
// ==========================================================================================
// aliyunpan JS插件回调处理函数
// 支持 JavaScript ECMAScript 5.1 语言规范
//
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
// ==========================================================================================
// ------------------------------------------------------------------------------------------
// 函数说明:下载文件前的回调函数
//
// 参数说明
// context - 当前调用的上下文信息
// {
// "appName": "aliyunpan",
// "version": "v0.1.3",
// "userId": "11001d48564f43b3bc5662874f04bb11",
// "nickname": "tickstep",
// "fileDriveId": "19519111",
// "resourceDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户备份网盘ID
// resourceDriveId - 用户资源网盘ID
//
// params - 文件下载前参数
// {
// "downloadActionId": "20976308-a026-450d-92b8-16a969fed0c6",
// "driveId": "19519221",
// "driveFileName": "token.bat",
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
// "driveFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
// "driveFileSize": 125330,
// "driveFileType": "file",
// "driveFileUpdatedAt": "2022-04-14 07:05:12",
// "localFilePath": "aliyunpan\\Downloads\\token.bat"
// }
// downloadActionId - 下载动作ID。同一次下载动作(download命令)下载的文件,这个ID都是同一个值。注意这个ID不是永久的,每执行一次download命令会生成一个对应ID。
// driveId - 网盘ID
// driveFileName - 网盘文件名
// driveFilePath - 网盘文件绝对完整路径
// driveFileSize - 网盘文件大小,单位B
// driveFileSha1 - 网盘文件SHA1
// driveFileType - 网盘文件类型,file-文件,folder-文件夹
// driveFileUpdatedAt - 网盘文件修改时间
// localFilePath - 下载文件到本地保存的路径,这个是相对路径,相对指定下载的目标文件夹
//
// 返回值说明
// {
// "downloadApproved": "yes",
// "localFilePath": "newfolder\\token.bat"
// }
// downloadApproved - 该文件是否下载,yes-确认下载,no-取消下载
// localFilePath - 文件保存的本地路径,这个是相对路径,如果为空""代表保持原本的目标路径。
// 这个改动要小心,会导致重名文件下载只会下载一个
// ------------------------------------------------------------------------------------------
function downloadFilePrepareCallback(context, params) {
console.log(params)
var result = {
"downloadApproved": "yes",
"localFilePath": ""
};
// 所有的.dmg.exe文件,下载保存的文件后缀名去掉.exe,网盘文件不改动
if (params["driveFileName"].lastIndexOf(".dmg.exe") > 0) {
result["localFilePath"] = params["localFilePath"].substr(0, (params["localFilePath"].length - ".exe".length));
}
// 禁止.txt文件下载
if (params["driveFileName"].lastIndexOf(".txt") > 0) {
result["downloadApproved"] = "no";
}
return result;
}
// ------------------------------------------------------------------------------------------
// 函数说明:下载文件结束的回调函数
//
// 参数说明
// context - 当前调用的上下文信息
// {
// "appName": "aliyunpan",
// "version": "v0.1.3",
// "userId": "11001d48564f43b3bc5662874f04bb11",
// "nickname": "tickstep",
// "fileDriveId": "19519111",
// "resourceDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户备份网盘ID
// resourceDriveId - 用户资源网盘ID
//
// params - 文件下载结束参数
// {
// "downloadActionId": "20976308-a026-450d-92b8-16a969fed0c6",
// "driveId": "19519221",
// "driveFileId": "65ebb9396d66dfcd9f25414bbdd81594ecb9fcd6",
// "driveFileName": "token.bat",
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
// "driveFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
// "driveFileSize": 125330,
// "driveFileType": "file",
// "driveFileUpdatedAt": "2022-04-14 07:05:12",
// "downloadResult": "success",
// "localFilePath": "aliyunpan\\Downloads\\token.bat"
// }
// downloadActionId - 下载动作ID。同一次下载动作(download命令)下载的文件,这个ID都是同一个值。注意这个ID不是永久的,每执行一次download命令会生成一个对应ID。
// driveId - 网盘ID
// driveFileId - 网盘文件ID
// driveFileName - 网盘文件名
// driveFilePath - 网盘文件绝对完整路径
// driveFileSize - 网盘文件大小,单位B
// driveFileSha1 - 网盘文件SHA1
// driveFileType - 网盘文件类型,file-文件,folder-文件夹
// driveFileUpdatedAt - 网盘文件修改时间
// downloadResult - 下载结果,success-成功,fail-失败
// localFilePath - 下载文件到本地保存的路径,这个是相对路径,相对指定下载的目标文件夹
//
// 返回值说明
// (没有返回值)
// ------------------------------------------------------------------------------------------
function downloadFileFinishCallback(context, params) {
console.log(params)
// // 云盘文件成功下载到本地后,删除云盘的文件
// if (params["downloadResult"] == "success") {
// if (params["driveFileType"] == "file") {
// // 文件下载成功,删除该云盘文件
// var userId = context["userId"]
// var driveId = params["driveId"]
// var driveFileId = params["driveFileId"]
// if (PluginUtil.PanFS.deleteFile(userId, driveId, driveFileId)) {
// console.log("插件删除云盘文件成功:" + params["driveFilePath"])
// }
// }
// }
}
+105
View File
@@ -0,0 +1,105 @@
// ==========================================================================================
// aliyunpan JS插件回调处理函数
// 支持 JavaScript ECMAScript 5.1 语言规范
//
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
// ==========================================================================================
// ------------------------------------------------------------------------------------------
// 函数说明:删除文件前的回调函数
//
// 参数说明
// context - 当前调用的上下文信息
// {
// "appName": "aliyunpan",
// "version": "v0.1.3",
// "userId": "11001d48564f43b3bc5662874f04bb11",
// "nickname": "tickstep",
// "fileDriveId": "19519111",
// "resourceDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户备份网盘ID
// resourceDriveId - 用户资源网盘ID
//
// params - 删除文件前的调用参数
// {
// "count": 10,
// "items": [{}, {}, ...],
// }
// count - 文件总数
// items - 文件列表,文件详情请查看下面 RemoveFilePrepareItem 定义
//
// RemoveFilePrepareItem - 文件详情参数
// {
// "driveId": "19519221",
// "driveFileId": "65f6c5161ee4a1b9a02b41399282f281c6e6df21",
// "driveFileName": "token.bat",
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
// "driveFileSize": 125330,
// "driveFileType": "file",
// "driveFileUpdatedAt": "2025-03-02 10:39:14",
// "driveFileCreatedAt": "2025-03-02 10:39:14"
// }
// driveId - 网盘ID
// driveFileId - 网盘文件的ID
// driveFileName - 网盘文件名
// driveFilePath - 网盘文件绝对完整路径
// driveFileSize - 网盘文件大小,单位B
// driveFileType - 网盘文件类型,file-文件,folder-文件夹
// driveFileUpdatedAt - 网盘文件修改时间
// driveFileCreatedAt - 网盘文件创建时间
//
// 返回值说明
// {
// "result": [{}, {}, ...],
// }
// result - 结果列表,每个文件的确认结果,文件详细定义请查看 RemoveFilePrepareResultItem 定义
//
// RemoveFilePrepareResultItem - 每项文件确认结果
// {
// "driveId": "19519221",
// "driveFileId": "65f6c5161ee4a1b9a02b41399282f281c6e6df21",
// "removeApproved": "yes"
// }
// driveId - 网盘ID
// driveFileId - 网盘文件的ID
// removeApproved - 该文件是否能删除,yes-确认删除,no-不能删除
// ------------------------------------------------------------------------------------------
function removeFilePrepareCallback(context, params) {
console.log(params)
var result = {"result":[]};
for(var i = 0; i < params.items.length; i++) {
var fileItem = params.items[i]
console.log(fileItem);
var fileItemApprovedResult = {
"driveId": fileItem["driveId"],
"driveFileId": fileItem["driveFileId"],
"removeApproved": "yes"
}
// 可以通过名称过滤
if (fileItem["driveFileType"] == "file") { // 文件
if (fileItem["driveFileName"] == "password.key") {
fileItemApprovedResult["removeApproved"] = "no"; // 禁止删除password.key文件
}
} else if (params["driveFileType"] == "folder") { // 文件夹
// 对文件夹进行处理
}
// 可以通过时间过滤,例如:保留最近10天的文件
// ...
// 可以通过数量过滤,例如:保留最新5个文件
// ...
// 放入结果列表
result["result"].push(fileItemApprovedResult)
}
// 返回
return result;
}
+266
View File
@@ -0,0 +1,266 @@
// ==========================================================================================
// aliyunpan JS插件回调处理函数
// 支持 JavaScript ECMAScript 5.1 语言规范
//
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
// ==========================================================================================
// ------------------------------------------------------------------------------------------
// 函数说明:同步备份-扫描本地文件前的回调函数
//
// 参数说明
// context - 当前调用的上下文信息
// {
// "appName": "aliyunpan",
// "version": "v0.1.3",
// "userId": "11001d48564f43b3bc5662874f04bb11",
// "nickname": "tickstep",
// "fileDriveId": "19519111",
// "resourceDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户备份网盘ID
// resourceDriveId - 用户资源网盘ID
//
// params - 扫描本地文件前参数
// {
// "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
// "localFileName": "token.bat",
// "localFileSize": 125330,
// "localFileType": "file",
// "localFileUpdatedAt": "2022-04-14 07:05:12",
// "driveId": "19519221"
// }
// localFilePath - 本地文件绝对完整路径
// localFileName - 本地文件名
// localFileSize - 本地文件大小,单位B
// localFileType - 本地文件类型,file-文件,folder-文件夹
// localFileUpdatedAt - 文件修改时间
// driveId - 备份的目标网盘ID
//
// 返回值说明
// {
// "syncScanLocalApproved": "yes"
// }
// syncScanLocalApproved - 该文件是否确认扫描,yes-允许扫描,no-禁止扫描。
// 禁止扫描的文件不会执行后续的动作,例如上传,下载。
// ------------------------------------------------------------------------------------------
function syncScanLocalFilePrepareCallback(context, params) {
console.log(params);
var result = {
"syncScanLocalApproved": "yes"
};
// 禁止.开头文件上传
if (params["localFileName"].indexOf(".") == 0) {
result["syncScanLocalApproved"] = "no";
}
// 禁止~$开头文件上传(office暂存临时文件)
if (params["localFileName"].indexOf("~$") == 0) {
result["syncScanLocalApproved"] = "no";
}
// 禁止.txt文件上传(正则表达式方式)
if (params["localFileName"].search(/.txt$/i) >= 0) {
result["syncScanLocalApproved"] = "no";
}
// 禁止password.key文件上传
if (params["localFileName"] == "password.key") {
result["syncScanLocalApproved"] = "no";
}
// 禁止@eadir文件上传
if (params["localFileName"] == "@eadir") {
result["syncScanLocalApproved"] = "no";
}
return result;
}
// ------------------------------------------------------------------------------------------
// 函数说明:同步备份-扫描云盘文件前的回调函数
//
// 参数说明
// context - 当前调用的上下文信息
// {
// "appName": "aliyunpan",
// "version": "v0.1.3",
// "userId": "11001d48564f43b3bc5662874f04bb11",
// "nickname": "tickstep",
// "fileDriveId": "19519111",
// "resourceDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户备份网盘ID
// resourceDriveId - 用户资源网盘ID
//
// params - 扫描云盘文件前参数
// {
// "driveId": "19519221",
// "driveFileName": "token.bat",
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
// "driveFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
// "driveFileSize": 125330,
// "driveFileType": "file",
// "driveFileUpdatedAt": "2022-04-14 07:05:12"
// }
// driveId - 网盘ID
// driveFileName - 网盘文件名
// driveFilePath - 网盘文件绝对完整路径
// driveFileSize - 网盘文件大小,单位B
// driveFileSha1 - 网盘文件SHA1
// driveFileType - 网盘文件类型,file-文件,folder-文件夹
// driveFileUpdatedAt - 网盘文件修改时间
//
// 返回值说明
// {
// "syncScanPanApproved": "yes"
// }
// syncScanPanApproved - 该文件是否确认扫描,yes-允许扫描,no-禁止扫描。
// 禁止扫描的文件不会执行后续的动作,例如上传,下载。
// ------------------------------------------------------------------------------------------
function syncScanPanFilePrepareCallback(context, params) {
console.log(params);
var result = {
"syncScanPanApproved": "yes"
};
// 禁止.开头文件下载
if (params["driveFileName"].indexOf(".") == 0) {
result["syncScanPanApproved"] = "no";
}
// 禁止~$开头文件下载(office暂存临时文件)
if (params["driveFileName"].indexOf("~$") == 0) {
result["syncScanPanApproved"] = "no";
}
// 禁止.txt文件下载(正则表达式方式)
// if (params["driveFileName"].search(/.txt$/i) >= 0) {
// result["syncScanPanApproved"] = "no";
// }
return result;
}
// ------------------------------------------------------------------------------------------
// 函数说明:同步备份-同步文件后的回调函数
//
// 参数说明
// context - 当前调用的上下文信息
// {
// "appName": "aliyunpan",
// "version": "v0.1.3",
// "userId": "11001d48564f43b3bc5662874f04bb11",
// "nickname": "tickstep",
// "fileDriveId": "19519111",
// "resourceDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户备份网盘ID
// resourceDriveId - 用户资源网盘ID
//
// params - 同步文件后的参数
// {
// "action": "upload",
// "actionResult": "success",
// "driveId": "19519221",
// "driveFileId": "65ebb9396d66dfcd9f25414bbdd81594ecb9fcd6",
// "fileName": "1.txt",
// "filePath": "D:/goprojects/dev/upload/1.txt",
// "fileSha1": "294C8F24C56042710813E95C55A0B018299BA9A7",
// "fileSize": "28077",
// "fileType": "file",
// "fileUpdatedAt": "2022-03-04 15:19:47"
// }
// action - 同步动作,download-下载文件,upload-上传文件
// actionResult - 同步结果,success-成功,fail-失败
// driveId - 网盘ID
// driveFileId - 网盘文件ID,只有download模式才会有这个字段
// fileName - 文件名称
// filePath - 文件完整路径
// fileSha1 - 文件SHA1
// fileSize - 文件大小,单位B
// fileType - 文件类型,file-文件,folder-文件夹
// fileUpdatedAt - 文件修改时间
//
// 返回值说明
// (无)
// ------------------------------------------------------------------------------------------
function syncFileFinishCallback(context, params) {
console.log(params)
// 云盘文件成功下载到本地后,删除云盘的文件
// if (params["action"] == "download") {
// if (params["actionResult"] == "success" && params["fileType"] == "file") {
// // 文件下载成功,删除该云盘文件
// var userId = context["userId"]
// var driveId = params["driveId"]
// var driveFileId = params["driveFileId"]
// if (PluginUtil.PanFS.deleteFile(userId, driveId, driveFileId)) {
// console.println("插件删除云盘文件成功:" + params["filePath"])
// }
// }
// }
}
// ------------------------------------------------------------------------------------------
// 函数说明:同步备份-同步任务同步全部文件完成时的回调函数(目前只有"只运行一次备份"的任务模式才有该回调)
//
// 参数说明
// context - 当前调用的上下文信息
// {
// "appName": "aliyunpan",
// "version": "v0.1.3",
// "userId": "11001d48564f43b3bc5662874f04bb11",
// "nickname": "tickstep",
// "fileDriveId": "19519111",
// "resourceDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户备份网盘ID
// resourceDriveId - 用户资源网盘ID
//
// params - 任务完成的回调参数
// {
// "name": "备份相册",
// "id": "2e959a34-8435-4773-9a3e-687f2692ddc1",
// "userId": "4d001d48564f43b3bc5662874114bbe6",
// "driveName": "backup",
// "driveId": "19519221",
// "localFolderPath": "/Volumes/DataDisk3T/Games/Switch/图库备份",
// "panFolderPath": "/备份盘/Switch游戏/Album",
// "mode": "upload",
// "policy": "upload",
// }
// name - 任务名称
// id - 任务ID
// userId - 登录的用户ID
// driveName - 网盘名称,backup-备份盘,resource-资源盘
// driveId - 目标网盘ID
// localFolderPath - 本地目录
// panFolderPath - 云盘目录
// mode - 备份模式,upload-备份本地文件到云盘,download-备份云盘文件到本地
// policy - 备份策略,increment-增量备份,exclusive-排他备份
//
// 返回值说明
// 无
// ------------------------------------------------------------------------------------------
function syncAllFileFinishCallback(context, params) {
console.log(params)
}
+48
View File
@@ -0,0 +1,48 @@
// ==========================================================================================
// aliyunpan JS插件回调处理函数
// 支持 JavaScript ECMAScript 5.1 语言规范
//
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
// ==========================================================================================
// ------------------------------------------------------------------------------------------
// 函数说明:用户Token刷新完成后回调函数
//
// 参数说明
// context - 当前调用的上下文信息
// {
// "appName": "aliyunpan",
// "version": "v0.1.3",
// "userId": "11001d48564f43b3bc5662874f04bb11",
// "nickname": "tickstep",
// "fileDriveId": "19519111",
// "resourceDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户备份网盘ID
// resourceDriveId - 用户资源网盘ID
//
// params - Token刷新参数
// {
// "result": "success",
// "message": "ok",
// "oldToken": "aa31fcc229c54d5ab6d8bfb17aff3711",
// "newToken": "bb31fcc229c54d5ab6d8bfb17aff3722",
// "updatedAt": "2022-04-14 07:05:12"
// }
// result - Token刷新的结果,success-成功,fail-失败
// message - 消息说明,如果失败这里会有原因说明
// oldToken - 刷新前的Token
// newToken - 刷新后的Token,只有result为success的才有该值
// updatedAt - Token刷新的时间
//
// 返回值说明
// (没有返回值)
// ------------------------------------------------------------------------------------------
function userTokenRefreshFinishCallback(context, params) {
console.log(params)
}
+128
View File
@@ -0,0 +1,128 @@
// ==========================================================================================
// aliyunpan JS插件回调处理函数
// 支持 JavaScript ECMAScript 5.1 语言规范
//
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
// ==========================================================================================
// ------------------------------------------------------------------------------------------
// 函数说明:上传文件前的回调函数
//
// 参数说明
// context - 当前调用的上下文信息
// {
// "appName": "aliyunpan",
// "version": "v0.1.3",
// "userId": "11001d48564f43b3bc5662874f04bb11",
// "nickname": "tickstep",
// "fileDriveId": "19519111",
// "resourceDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户备份网盘ID
// resourceDriveId - 用户资源网盘ID
//
// params - 文件上传前参数
// {
// "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
// "localFileName": "token.bat",
// "localFileSize": 125330,
// "localFileType": "file",
// "localFileUpdatedAt": "2022-04-14 07:05:12",
// "driveId": "19519221",
// "driveFilePath": "aliyunpan/Downloads/token.bat"
// }
// localFilePath - 本地文件绝对完整路径
// localFileName - 本地文件名
// localFileSize - 本地文件大小,单位B
// localFileType - 本地文件类型,file-文件,folder-文件夹
// localFileUpdatedAt - 文件修改时间
// driveId - 准备上传的目标网盘ID
// driveFilePath - 准备上传的目标网盘保存的路径,这个是相对路径,相对指定上传的目标文件夹
//
// 返回值说明
// {
// "uploadApproved": "yes",
// "driveFilePath": "newfolder/token.bat"
// }
// uploadApproved - 该文件是否确认上传,yes-允许上传,no-禁止上传
// driveFilePath - 文件保存的网盘路径,这个是相对路径,如果为空""代表保持原本的目标路径。
// 这个改动要小心,会导致重名文件只会上传一个
// ------------------------------------------------------------------------------------------
function uploadFilePrepareCallback(context, params) {
var result = {
"uploadApproved": "yes",
"driveFilePath": ""
};
// 所有的.dmg文件,网盘保存的文件增加后缀名为.exe,本地文件不改动
if (params["localFilePath"].lastIndexOf(".dmg") > 0) {
result["driveFilePath"] = params["driveFilePath"] + ".exe";
}
// 禁止.txt文件上传
if (params["localFilePath"].lastIndexOf(".txt") > 0) {
result["uploadApproved"] = "no";
}
// 禁止password.key文件上传
if (params["localFileName"] == "password.key") {
result["uploadApproved"] = "no";
}
return result;
}
// ------------------------------------------------------------------------------------------
// 函数说明:上传文件结束的回调函数
//
// 参数说明
// context - 当前调用的上下文信息
// {
// "appName": "aliyunpan",
// "version": "v0.1.3",
// "userId": "11001d48564f43b3bc5662874f04bb11",
// "nickname": "tickstep",
// "fileDriveId": "19519111",
// "resourceDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户备份网盘ID
// resourceDriveId - 用户资源网盘ID
//
// params - 文件上传结束参数
// {
// "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
// "localFileName": "token.bat",
// "localFileSize": 125330,
// "localFileType": "file",
// "localFileUpdatedAt": "2022-04-14 07:05:12",
// "localFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
// "uploadResult": "success",
// "driveId": "19519221",
// "driveFilePath": "/tmp/test/aliyunpan/Downloads/token.bat"
// }
// localFilePath - 本地文件绝对完整路径
// localFileName - 本地文件名
// localFileSize - 本地文件大小,单位B
// localFileType - 本地文件类型,file-文件,folder-文件夹
// localFileUpdatedAt - 文件修改时间
// localFileSha1 - 本地文件的SHA1。这个值不一定会有
// uploadResult - 上传结果,success-成功,fail-失败
// driveId - 目标网盘ID
// driveFilePath - 文件网盘保存的绝对路径
//
// 返回值说明
// (没有返回值)
// ------------------------------------------------------------------------------------------
function uploadFileFinishCallback(context, params) {
console.log(params)
}
+14
View File
@@ -0,0 +1,14 @@
@echo off
REM 配置环境变量
REM set ALIYUNPAN_CONFIG_DIR=d:\path\to\your\aliyunpan\config
REM 指定refresh token用于登录
aliyunpan login -RefreshToken=9078907....adg9087
REM 上传下载链接类型:1-默认 2-阿里ECS环境
aliyunpan config set -transfer_url_type 1
REM 指定配置参数并进行启动
REM 支持的模式:upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/备份盘/我的文档" -mode "upload"
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
# 请更改成你自己电脑上aliyunpan执行文件所在的目录
#cd /path/to/aliyunpan/folder
# 配置环境变量
#export ALIYUNPAN_CONFIG_DIR=/path/to/your/aliyunpan/config
chmod +x ./aliyunpan
# 指定refresh token用于登录
./aliyunpan login -RefreshToken=9078907....adg9087
# 上传下载链接类型:1-默认 2-阿里ECS环境
./aliyunpan config set -transfer_url_type 1
# 指定配置参数并进行启动
# 支持的模式:upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)
./aliyunpan sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload"
+13
View File
@@ -0,0 +1,13 @@
@echo off
REM 配置环境变量
REM set ALIYUNPAN_CONFIG_DIR=d:\path\to\your\aliyunpan\config
REM 指定refresh token用于登录
aliyunpan login -RefreshToken=9078907....adg9087
REM 上传下载链接类型:1-默认 2-阿里ECS环境
aliyunpan config set -transfer_url_type 1
REM 指定webdav启动参数并进行启动。你可以按照自己的需求更改以下参数,例如用户名和密码
aliyunpan webdav start -ip "0.0.0.0" -port 23077 -webdav_user "admin" -webdav_password "admin" -pan_dir_path "/" -bs 1024
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
# 请更改成你自己电脑上aliyunpan执行文件所在的目录
#cd /path/to/aliyunpan/folder
# 配置环境变量
#export ALIYUNPAN_CONFIG_DIR=/path/to/your/aliyunpan/config
chmod +x ./aliyunpan
# 指定refresh token用于登录
./aliyunpan login -RefreshToken=9078907....adg9087
# 上传下载链接类型:1-默认 2-阿里ECS环境
./aliyunpan config set -transfer_url_type 1
# 指定webdav启动参数并进行启动。你可以按照自己的需求更改以下参数,例如用户名、密码和网盘目录
./aliyunpan webdav start -ip "0.0.0.0" -port 23077 -webdav_user "admin" -webdav_password "admin" -pan_dir_path "/" -bs 1024
@@ -0,0 +1,18 @@
# linux service 配置文件模版
# centos系统默认存放路径为:/lib/systemd/system/aliyunpansync.service
# 启动服务:systemctl start aliyunpansync
# 停止服务:systemctl stop aliyunpansync
[Unit]
Description=aliyunpansync
After=network.target
[Service]
Type=forking
ExecStart=/path/to/start.sh
ExecStop=/path/to/stop.sh
PrivateTmp=true
Restart=always
[Install]
WantedBy=multi-user.target
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
# 更改路径为你电脑sync.sh文件所在文件夹路径
cd /path/to/your/sync.sh/folder
nohup ./sync.sh >/dev/null 2>&1 &
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
PROCESS=`ps -ef|grep aliyunpan|grep -v grep|grep -v PPID|awk '{ print $2}'`
for i in $PROCESS
do
# kill进程
kill -9 $i
break
done
+12
View File
@@ -0,0 +1,12 @@
{
"configVer": "1.0",
"syncTaskList": [
{
"name": "设计文档备份",
"localFolderPath": "D:\\tickstep\\Documents\\设计文档",
"panFolderPath": "/sync_drive/我的文档",
"mode": "upload",
"priority": "local"
}
]
}
+40 -16
View File
@@ -13,6 +13,11 @@ fi
output="out"
build_dir=`dirname $0`
if [ ! -d ${build_dir}/${output} ];then
mkdir -p ${build_dir}/${output}
fi
default_golang() {
export GOROOT=/usr/local/go
go=$GOROOT/bin/go
@@ -57,7 +62,7 @@ IOSBuild() {
cd "$output/$1"
export CC=/usr/local/go/misc/ios/clangwrap.sh GOOS=ios GOARCH=arm64 GOARM=7 CGO_ENABLED=1
$go build -ldflags "-X main.Version=$version -s -w" -o $name github.com/tickstep/aliyunpan
jtool --sign --inplace --ent ../../entitlements.xml $name
jtool2 --sign --inplace --ent ../../entitlements.xml $name
cd ../..
RicePack $1 $name
Pack $1 "ios"
@@ -70,6 +75,11 @@ Pack() {
fi
cp README.md "$output/$1"
cp docs/manual.md "$output/$1"
cp docs/plugin_manual.md "$output/$1"
cp -rf assets/scripts/* "$output/$1"
cp -rf assets/plugin "$output/$1"
cp -rf assets/sync_drive "$output/$1"
cd $output
zip -q -r "$1.zip" "$1"
@@ -85,45 +95,59 @@ RicePack() {
return # 已取消web功能
}
# Android
############### Android ###############
export ANDROID_NDK_ROOT=/Users/tickstep/Applications/android_ndk/android-ndk-r23-darwin
CC=$ANDROID_NDK_ROOT/bin/arm-linux-androideabi/bin/clang AndroidBuild $name-$version"-android-api16-armv7" android arm 7
CC=$ANDROID_NDK_ROOT/bin/aarch64-linux-android/bin/clang AndroidBuild $name-$version"-android-api21-arm64" android arm64 7
CC=$ANDROID_NDK_ROOT/bin/i686-linux-android/bin/clang AndroidBuild $name-$version"-android-api16-386" android 386 7
CC=$ANDROID_NDK_ROOT/bin/x86_64-linux-android/bin/clang AndroidBuild $name-$version"-android-api21-amd64" android amd64 7
# iOS
############### iOS ###############
IOSBuild $name-$version"-ios-arm64"
# OS X / macOS
############### macOS ###############
Build $name-$version"-darwin-macos-amd64" darwin amd64
# Build $name-$version"-darwin-macos-386" darwin 386
Build $name-$version"-darwin-macos-arm64" darwin arm64
# Windows
############### Windows ###############
Build $name-$version"-windows-x86" windows 386
Build $name-$version"-windows-x64" windows amd64
Build $name-$version"-windows-arm" windows arm
Build $name-$version"-windows-arm64" windows arm64
# Linux
############### Linux ###############
# x64/x86
Build $name-$version"-linux-386" linux 386
Build $name-$version"-linux-amd64" linux amd64
# 龙芯 LoongArch
Build $name-$version"-linux-loong64" linux loong64
# arm
Build $name-$version"-linux-armv5" linux arm 5
Build $name-$version"-linux-armv7" linux arm 7
Build $name-$version"-linux-arm64" linux arm64
GOMIPS=softfloat Build $name-$version"-linux-mips" linux mips
Build $name-$version"-linux-mips64" linux mips64
GOMIPS=softfloat Build $name-$version"-linux-mipsle" linux mipsle
Build $name-$version"-linux-mips64le" linux mips64le
# Build $name-$version"-linux-ppc64" linux ppc64
# Build $name-$version"-linux-ppc64le" linux ppc64le
# Build $name-$version"-linux-s390x" linux s390x
# Others
# Build $name-$version"-solaris-amd64" solaris amd64
# mips
GOMIPS=softfloat Build $name-$version"-linux-mips64" linux mips64
GOMIPS=softfloat Build $name-$version"-linux-mips64le" linux mips64le
GOMIPS=hardfloat Build $name-$version"-linux-mips64hf" linux mips64
GOMIPS=hardfloat Build $name-$version"-linux-mips64lehf" linux mips64le
GOMIPS=softfloat Build $name-$version"-linux-mips" linux mips
GOMIPS=softfloat Build $name-$version"-linux-mipsle" linux mipsle
GOMIPS=hardfloat Build $name-$version"-linux-mipshf" linux mips
GOMIPS=hardfloat Build $name-$version"-linux-mipslehf" linux mipsle
# freebsd
Build $name-$version"-freebsd-386" freebsd 386
Build $name-$version"-freebsd-amd64" freebsd amd64
# Build $name-$version"-freebsd-arm" freebsd arm
# Others
# Build $name-$version"-linux-ppc64" linux ppc64
# Build $name-$version"-linux-ppc64le" linux ppc64le
# Build $name-$version"-linux-s390x" linux s390x
# Build $name-$version"-solaris-amd64" solaris amd64
# Build $name-$version"-netbsd-386" netbsd 386
# Build $name-$version"-netbsd-amd64" netbsd amd64
# Build $name-$version"-netbsd-arm" netbsd arm
-75
View File
@@ -1,38 +1,11 @@
package cmder
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder/cmdliner"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"sync"
)
var (
appInstance *cli.App
saveConfigMutex *sync.Mutex = new(sync.Mutex)
ReloadConfigFunc = func(c *cli.Context) error {
err := config.Config.Reload()
if err != nil {
fmt.Printf("重载配置错误: %s\n", err)
}
return nil
}
SaveConfigFunc = func(c *cli.Context) error {
saveConfigMutex.Lock()
defer saveConfigMutex.Unlock()
err := config.Config.Save()
if err != nil {
fmt.Printf("保存配置错误: %s\n", err)
}
return nil
}
)
func SetApp(app *cli.App) {
@@ -42,51 +15,3 @@ func SetApp(app *cli.App) {
func App() *cli.App {
return appInstance
}
func DoLoginHelper(refreshToken string) (refreshTokenStr string, webToken aliyunpan.WebLoginToken, error error) {
line := cmdliner.NewLiner()
defer line.Close()
if refreshToken == "" {
refreshToken, error = line.State.Prompt("请输入RefreshToken, 回车键提交 > ")
if error != nil {
return
}
}
// app login
atoken, apperr := aliyunpan.GetAccessTokenFromRefreshToken(refreshToken)
if apperr != nil {
if apperr.Code == apierror.ApiCodeTokenExpiredCode || apperr.Code == apierror.ApiCodeRefreshTokenExpiredCode {
fmt.Println("Token过期,需要重新登录")
} else {
fmt.Println("Token登录失败:", apperr)
}
return "", webToken, fmt.Errorf("登录失败")
}
refreshTokenStr = refreshToken
return refreshTokenStr, *atoken, nil
}
func TryLogin() *config.PanUser {
// can do automatically login?
for _, u := range config.Config.UserList {
if u.UserId == config.Config.ActiveUID {
// login
_, webToken, err := DoLoginHelper(u.RefreshToken)
if err != nil {
logger.Verboseln("automatically login error")
break
}
// success
u.WebToken = webToken
// save
SaveConfigFunc(nil)
// reload
ReloadConfigFunc(nil)
return config.Config.ActiveUser()
}
}
return nil
}
+5
View File
@@ -123,3 +123,8 @@ func WalkDir(dirPth, suffix string) (files []string, err error) {
func ConvertToUnixPathSeparator(p string) string {
return strings.Replace(p, "\\", "/", -1)
}
// ConvertToWindowsPathSeparator 将路径中的所有分隔符转换成windows格式
func ConvertToWindowsPathSeparator(p string) string {
return strings.Replace(p, "/", "\\", -1)
}
+75
View File
@@ -0,0 +1,75 @@
# alpine:3.21
# 参数
ARG DOCKER_IMAGE_HASH
#FROM alpine@sha256:$DOCKER_IMAGE_HASH
#FROM alpine
FROM alpine:$DOCKER_IMAGE_HASH
LABEL author="tickstep"
LABEL email="tickstep@outlook.com"
LABEL version="1.0"
LABEL description="sync & backup service for aliyun cloud drive"
# 时区
ENV TZ=Asia/Shanghai
# 手动下载tzdata安装包,注意要下载对应架构的: https://dl-cdn.alpinelinux.org/alpine/v3.15/community/
RUN apk add -U tzdata
# 更新Alpine的软件源为国内镜像站点,提高下载速度。安装tzdata包
# RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
# RUN apk add --no-cache tzdata
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 创建运行目录
RUN mkdir -p /home/app
VOLUME /home/app
WORKDIR /home/app
RUN cd /home/app
# 创建配置文件目录
RUN mkdir -p /home/app/config
# 创建数据文件夹
RUN mkdir -p /home/app/data
RUN chmod 777 /home/app/data
# 复制文件
COPY ./docker/sync/app.sh /home/app/app.sh
RUN chmod +x /home/app/app.sh
COPY ./docker/sync/health_check.sh /home/app/health_check.sh
RUN chmod +x /home/app/health_check.sh
COPY ./out/binary_files/aliyunpan /home/app
RUN mkdir -p /home/app/config/plugin
COPY ./out/binary_files/plugin /home/app/config/plugin
RUN mkdir -p /home/app/config/sync_drive
COPY ./out/binary_files/sync_drive /home/app/config/sync_drive
#RUN chmod +x /home/app/aliyunpan
# 健康检查
HEALTHCHECK --start-period=5s --interval=10s --timeout=5s --retries=3 CMD /bin/sh /home/app/health_check.sh
# 环境变量
ENV ALIYUNPAN_DOCKER=1
ENV ALIYUNPAN_CONFIG_DIR=/home/app/config
ENV ALIYUNPAN_DOWNLOAD_PARALLEL=2
ENV ALIYUNPAN_UPLOAD_PARALLEL=2
ENV ALIYUNPAN_DOWNLOAD_BLOCK_SIZE=1024
ENV ALIYUNPAN_UPLOAD_BLOCK_SIZE=10240
ENV ALIYUNPAN_LOCAL_DIR=/home/app/data
ENV ALIYUNPAN_PAN_DIR=/sync_drive
ENV ALIYUNPAN_SYNC_MODE=upload
ENV ALIYUNPAN_SYNC_POLICY=increment
ENV ALIYUNPAN_SYNC_DRIVE=backup
ENV ALIYUNPAN_SYNC_CYCLE=infinity
ENV ALIYUNPAN_SYNC_PRIORITY=time
ENV ALIYUNPAN_SYNC_LOG=false
ENV ALIYUNPAN_LOCAL_DELAY_TIME=3
ENV ALIYUNPAN_SCAN_INTERVAL_TIME=1
ENV ALIYUNPAN_DEVICE_ID=""
ENV ALIYUNPAN_IP_TYPE="ipv4"
# 运行
ENTRYPOINT ./app.sh
+66
View File
@@ -0,0 +1,66 @@
#!/bin/sh
cd /home/app
chmod +x ./aliyunpan
# sync config file
readonly sync_drive_config_file="$ALIYUNPAN_CONFIG_DIR/sync_drive/sync_drive_config.json"
if test -s $sync_drive_config_file
then
echo "using existed sync_drive_config.json file"
else
echo "generate sync_drive_config.json file"
tee $sync_drive_config_file << EOF
{
"configVer": "1.0",
"syncTaskList": [
{
"name": "阿里云盘备份",
"id": "5b2d7c10-e927-4e72-8f9d-5abb3bb04815",
"localFolderPath": "$ALIYUNPAN_LOCAL_DIR",
"panFolderPath": "$ALIYUNPAN_PAN_DIR",
"mode": "$ALIYUNPAN_SYNC_MODE",
"policy": "$ALIYUNPAN_SYNC_POLICY",
"driveName": "$ALIYUNPAN_SYNC_DRIVE",
"lastSyncTime": "2022-06-12 19:28:20"
}
]
}
EOF
fi
sleep 2s
# device-id
#if [[ -z $ALIYUNPAN_DEVICE_ID ]];
#then
# echo "the program use random device id"
#else
# echo "set device id"
# ./aliyunpan config set -device_id ${ALIYUNPAN_DEVICE_ID}
#fi
# show docker IPs
./aliyunpan tool getip
# check login already or not
./aliyunpan who
if [ $? -eq 0 ]
then
echo "cache token is valid, not need to re-login"
else
echo "token is invalid, please use the valid aliyunpan_config.json file and retry"
# ./aliyunpan login -RefreshToken=${ALIYUNPAN_REFRESH_TOKEN}
fi
if [ "$ALIYUNPAN_SYNC_LOG" = "true" ]
then
./aliyunpan config set -file_record_config 1
else
./aliyunpan config set -file_record_config 2
fi
# IP type
./aliyunpan config set -ip_type ${ALIYUNPAN_IP_TYPE}
# run
./aliyunpan sync start -dp ${ALIYUNPAN_DOWNLOAD_PARALLEL} -up ${ALIYUNPAN_UPLOAD_PARALLEL} -dbs ${ALIYUNPAN_DOWNLOAD_BLOCK_SIZE} -ubs ${ALIYUNPAN_UPLOAD_BLOCK_SIZE} -log ${ALIYUNPAN_SYNC_LOG} -ldt ${ALIYUNPAN_LOCAL_DELAY_TIME} -cycle ${ALIYUNPAN_SYNC_CYCLE} -sit ${ALIYUNPAN_SCAN_INTERVAL_TIME}
+43
View File
@@ -0,0 +1,43 @@
version: '3'
services:
sync:
image: tickstep/aliyunpan-sync:v0.1.6
container_name: aliyunpan-sync
restart: always
volumes:
# (必须)映射的本地目录
- ./data:/home/app/data:rw
# (可选)可以指定JS插件sync_handler.js用于过滤文件,详见插件说明
#- ./plugin/js/sync_handler.js:/home/app/config/plugin/js/sync_handler.js
# (推荐)挂载sync_drive同步数据库到本地,这样即使容器销毁,同步数据库还可以用于以后使用
#- ./sync_drive:/home/app/config/sync_drive
# (必须)映射token凭据文件
- /your/file/path/for/aliyunpan_config.json:/home/app/config/aliyunpan_config.json
environment:
# 时区,东8区
- TZ=Asia/Shanghai
# 下载文件并发数
- ALIYUNPAN_DOWNLOAD_PARALLEL=2
# 上传文件并发数
- ALIYUNPAN_UPLOAD_PARALLEL=2
# 下载数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
- ALIYUNPAN_DOWNLOAD_BLOCK_SIZE=1024
# 上传数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
- ALIYUNPAN_UPLOAD_BLOCK_SIZE=10240
# 指定网盘文件夹作为备份目标目录,不要指定根目录
- ALIYUNPAN_PAN_DIR=/my_sync_dir
# 备份模式:upload(备份本地文件到云盘), download(备份云盘文件到本地)
- ALIYUNPAN_SYNC_MODE=upload
# 备份策略: exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)
- ALIYUNPAN_SYNC_POLICY=increment
# 备份周期, 支持两种: infinity(永久循环备份),onetime(只运行一次备份)
- ALIYUNPAN_SYNC_CYCLE=infinity
# 网盘:backup(备份盘), resource(资源盘)
- ALIYUNPAN_SYNC_DRIVE=backup
# 是否显示文件备份过程日志,true-显示,false-不显示
- ALIYUNPAN_SYNC_LOG=true
# 本地文件修改检测延迟间隔,单位秒。如果本地文件会被频繁修改,例如录制视频文件,配置好该时间可以避免上传未录制好的文件
- ALIYUNPAN_LOCAL_DELAY_TIME=3
# 扫描文件间隔时间,单位:分钟
- ALIYUNPAN_SCAN_INTERVAL_TIME=1
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# 检查aliyunpan进程是否存在
ps | awk '{print $4}' | grep aliyunpan
if [ $? == 0 ]
then
echo $?
exit 0
else
echo $?
exit 1
fi
+1
View File
@@ -4,6 +4,7 @@
工具安装,运行下面的命令即可生成工具。也可以直接用 bin/ 文件夹下面的编译好的
```
go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo
go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo
```
versioninfo.json - 里面有exe程序信息以及ico的配置
+793
View File
@@ -0,0 +1,793 @@
# 目录
- [命令列表及说明](#命令列表及说明)
* [注意](#注意)
* [修改配置文件存储路径](#修改配置文件存储路径)
* [检测程序更新](#检测程序更新)
* [查看帮助](#查看帮助)
* [登录阿里云盘帐号](#登录阿里云盘帐号)
* [列出帐号列表](#列出帐号列表)
* [获取当前帐号](#获取当前帐号)
* [切换阿里云盘帐号](#切换阿里云盘帐号)
* [退出阿里云盘帐号](#退出阿里云盘帐号)
* [切换网盘(备份盘/资源库)](#切换网盘)
* [获取网盘配额](#获取网盘配额)
* [切换工作目录](#切换工作目录)
* [输出工作目录](#输出工作目录)
* [列出目录](#列出目录)
* [下载文件/目录](#下载文件目录)
* [多用户联合下载](#多用户联合下载)
* [上传文件/目录](#上传文件目录)
* [创建目录](#创建目录)
* [删除文件/目录](#删除文件目录)
* [移动文件/目录](#移动文件目录)
* [重命名文件/目录](#重命名文件目录)
* [分享文件/目录](#分享文件目录)
+ [设置分享文件/目录](#设置分享文件目录)
+ [创建快传链接](#创建快传链接)
+ [列出已分享文件/目录](#列出已分享文件目录)
+ [取消分享文件/目录](#取消分享文件目录)
* [共享相册](#共享相册)
+ [展示共享相簿列表](#展示共享相簿列表)
+ [展示指定相簿中的文件](#展示指定相簿中的文件)
+ [下载相簿中的所有文件](#下载相簿中的所有文件)
* [同步备份功能](#同步备份功能)
+ [常用命令说明](#常用命令说明)
+ [备份配置文件说明](#备份配置文件说明)
+ [命令行启动](#命令行启动)
+ [Linux后台启动](#Linux后台启动)
+ [Windows后台启动](#Windows后台启动)
+ [Docker运行](#Docker运行)
* [JavaScript插件](#JavaScript插件)
* [显示和修改程序配置项](#显示和修改程序配置项)
- [常见问题Q&A](#常见问题QA)
* [1. 如何开启Debug调试日志](#1-如何开启Debug调试日志)
# 命令列表及说明
## 注意
命令的前缀 `aliyunpan` 为指向程序运行的全路径名 (ARGv 的第一个参数)
直接运行程序时, 未带任何其他参数, 则程序进入cli交互模式, 进入cli模式运行以下命令时要把命令的前缀 `aliyunpan` 去掉! 即不需要输入`aliyunpan`。
cli交互模式支持按tab键自动补全命令.
## 修改配置文件存储路径
设置环境变量ALIYUNPAN_CONFIG_DIR并指定一个存在目录即可,注意目录需要是绝对路径
```
例如linux下面可以这样指定
export ALIYUNPAN_CONFIG_DIR=/home/tickstep/tools/aliyunpan/config
```
## 检测程序更新
```
aliyunpan update
```
## 查看帮助
```
aliyunpan help
```
### 例子
```
列出程序支持的命令
aliyunpan help
查看login命令的帮助手册
aliyunpan help login
```
## 登录阿里云盘帐号
### 登录
当前支持在浏览器进行登录,请输入以下命令按照步骤进行即可。
```
aliyunpan login
```
### 例子
```
aliyunpan > login
请在浏览器打开以下链接进行登录,链接有效时间为5分钟。
https://openapi.alipan.com/oauth/authorize?client_id=cf9f70e8fc61430f8ec5ab5cadf31375&redirect_uri=https%3A%2F%2Fapi.tickstep.com%2Fauth%2Ftickstep%2Faliyunpan%2Ftoken%2Fopenapi%2F8206f0.....fb5db6b40336%2Fauth&scope=user:base,file:all:read,file:all:write
阿里云盘登录成功: tickstep
aliyunpan:/ tickstep$
```
目前登录全部采用网页登录,由于 aliyunpan 融合了阿里官方Open接口+网页Web端接口,所以你需要进行两次登录。输入 login 命令
![](../assets/images/login-screenshot-1.png)
复制该链接到浏览器打开,会自动跳转到阿里官方授权登录页面,点击允许完成第一次登录
![](../assets/images/login-screenshot-2.png)
接着页面会自动跳转到网页接口登录页面,使用阿里APP扫码完成第二次登录
![](../assets/images/login-screenshot-3.png)
然后切换回 aliyunpan 程序,按下 Enter 按键完成登录即可
![](../assets/images/login-screenshot-4.png)
## 列出帐号列表
```
aliyunpan loglist
```
列出所有已登录的帐号
## 获取当前帐号
```
aliyunpan who
```
## 切换阿里云盘帐号
切换已登录的帐号
```
aliyunpan su <uid>
```
```
aliyunpan su
请输入要切换帐号的 # 值 >
```
## 退出阿里云盘帐号
退出当前登录的帐号
```
aliyunpan logout
```
程序会进一步确认退出帐号, 防止误操作.
## 切换网盘
程序默认工作在文件网盘下,如需切换到资源库网盘,可以使用本命令进行切换。
```
aliyunpan drive <driveId>
```
```
aliyunpan drive
输入要切换的网盘 # 值 >
```
## 获取网盘配额
```
aliyunpan quota
```
获取网盘的总储存空间, 和已使用的储存空间
## 切换工作目录
```
aliyunpan cd <目录>
```
### 例子
```
# 切换 /我的文档 工作目录
aliyunpan cd /我的文档
# 切换 上级目录
aliyunpan cd ..
# 切换 根目录
aliyunpan cd /
```
## 输出工作目录
```
aliyunpan pwd
```
## 列出目录
列出当前工作目录的文件和目录或指定目录
```
aliyunpan ls
```
```
aliyunpan ls <目录>
```
### 可选参数
```
-driveId value 网盘ID
```
### 例子
```
# 列出 我的文档 内的文件和目录
aliyunpan ls 我的文档
# 绝对路径
aliyunpan ls /我的文档
# 详细列出 我的文档 内的文件和目录
aliyunpan ll /我的文档
```
## 下载文件/目录
```
aliyunpan download <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
aliyunpan d <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
```
### 可选参数
```
--ow overwrite, 覆盖已存在的文件
--status 输出所有线程的工作状态
--save 将下载的文件直接保存到当前工作目录
--saveto value 将下载的文件直接保存到指定的目录
-x 为文件加上执行权限, (windows系统无效)
-p value parallel,指定同时进行下载文件的数量(取值范围:1 ~ 3) (default: 1)
--sp value slice parallel,指定单个文件下载的最大线程(分片)数(取值范围:1 ~ 3) (default: 0)
--retry value 下载失败最大重试次数 (default: 3)
--nocheck 下载文件完成后不校验文件
--np no progress 不展示下载进度条
--driveId value 网盘ID
--exn value exclude name,指定排除的文件夹或者文件的名称,被排除的文件不会进行下载,只支持正则表达式。支持同时排除多个名称,每一个名称就是一个exn参数
--md (BETA) Multi-User Download,使用多用户联合下载,可以对单一文件叠加所有登录用户的下载速度
--ui (BETA) 使用UI面板显示下载详情和进度,更加直观和友好
```
### 例子
```
# 设置保存目录, 保存到 D:\Downloads
# 注意区别反斜杠 "\" 和 斜杠 "/" !!!
aliyunpan config set -savedir D:/Downloads
# 下载 /我的文档/1.mp4
aliyunpan d /我的文档/1.mp4
# 下载 /我的文档 整个目录!!
aliyunpan d /我的文档
# 下载 /photo 整个目录,并使用UI面板展示下载详情
aliyunpan d /photo -ui
```
下载的文件默认保存到 **程序所在目录** 的 download/ 目录, 支持设置指定目录, 重名的文件会自动跳过!
通过 `aliyunpan config set -savedir <savedir>` 可以自定义保存的目录。
支持多个文件或目录下载,支持自动跳过下载重名的文件!
### Linux后台下载
需要结合nohup进行启动。
新建 download.sh 脚本,内容如下
```
#!/bin/bash
# 是否开启调试日志
export ALIYUNPAN_VERBOSE=0
# (可选)配置目录的绝对路径(请更改成你自己的目录)
export ALIYUNPAN_CONFIG_DIR=/Users/tickstep/Applications/adrive/config
# aliyunpan程序所在的绝对路径(请更改成你自己的目录)
export ALIYUNPAN_BIN=/Users/tickstep/Applications/adrive/aliyunpan
# 本地目录(请更改成你自己的目录)
LOCAL_DIR="/tickstep/Documents/我的文档"
# 网盘目录(请更改成你自己的目录)
PAN_DIR="/我的文档"
# 执行下载
"$ALIYUNPAN_BIN" download --saveto "$LOCAL_DIR" "$PAN_DIR"
```
增加脚本执行权限
```
$ chmod +x download.sh
```
然后启动该脚本进行后台运行
```
$ nohup ./download.sh > aliyunpan.log 2>&1 &
```
## 多用户联合下载
前提:程序必须登录多个帐号,并且登录授权都有效。
```
# 使用多用户联合下载 /我的资源/1.mp4 文件。必须保证所有登录的用户在相同的网盘(备份盘/资源盘)下,相同的路径下,有相同的文件
aliyunpan download /我的资源/1.mp4 -md
```
什么是多用户联合下载?
由于阿里云盘的限制,一个用户下载的时候只能有3个线程并发数,未开通三方权益包的账号一个下载线程速度大概是500KB/s,3个并发即为1.5MB/s。
多用户联合下载,即是多个用户同时下载同一个文件,每个用户并发3个线程,N个用户即能开启 Nx3 个线程,对应的下载速度叠加为 Nx1.5MB/s。
对下载速度有极致追求的用户可以尝试使用该选项。
如果你的账号都开通了三方权益包,则一个用户下载速度为50MB/s,两个用户联合下载可以轻松突破100MB/s。
![](../assets/images/multi_user_download.png)
## 上传文件/目录
```
aliyunpan upload <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>
aliyunpan u <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>
```
### 例子:
```
# 将本地的 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录
# 注意区别反斜杠 "\" 和 斜杠 "/" !!!
aliyunpan upload C:/Users/Administrator/Desktop/1.mp4 /视频
# 将本地的 C:\Users\Administrator\Desktop\1.mp4 和 C:\Users\Administrator\Desktop\2.mp4 上传到网盘 /视频 目录
aliyunpan upload C:/Users/Administrator/Desktop/1.mp4 C:/Users/Administrator/Desktop/2.mp4 /视频
# 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录
aliyunpan upload C:/Users/Administrator/Desktop /视频
# 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录,并使用UI面板展示下载详情
aliyunpan upload C:/Users/Administrator/Desktop /视频 -ui
## 下面演示文件或者文件夹排除功能
# 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
aliyunpan upload -exn "\.jpg$" C:/Users/Administrator/Video /视频
# 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
aliyunpan upload -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视频
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式
1)排除@eadir文件或者文件夹:-exn "^@eadir$"
2)排除.jpg文件:-exn "\.jpg$"
3)排除.号开头的文件:-exn "^\."
4)排除~号开头的文件:-exn "^~"
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
```
### Linux后台上传
需要结合nohup进行启动。
新建 upload.sh 脚本,内容如下
```
#!/bin/bash
# 是否开启调试日志 (1=开启, 0=关闭)
export ALIYUNPAN_VERBOSE=0
# (可选)配置目录的绝对路径
export ALIYUNPAN_CONFIG_DIR="/Users/tickstep/Applications/adrive/config"
# aliyunpan程序所在的绝对路径
export ALIYUNPAN_BIN="/Users/tickstep/Applications/adrive/aliyunpan"
# 本地目录
LOCAL_DIR="/tickstep/Documents/我的文档"
# 网盘目录
PAN_DIR="/我的文档"
# 检查本地目录是否存在
if [ ! -d "$LOCAL_DIR" ]; then
echo "[ERROR] 本地目录不存在: $LOCAL_DIR"
exit 1
fi
# 执行上传(已修复引号、添加安全引用和覆盖策略)
"$ALIYUNPAN_BIN" upload \
-exn "^\." \
-exn "^@eadir$" \
-ow \
"$LOCAL_DIR" \
"$PAN_DIR"
echo "[INFO] 上传任务执行完毕"
```
增加脚本执行权限
```
$ chmod +x upload.sh
```
然后启动该脚本进行后台运行
```
$ nohup ./upload.sh > aliyunpan.log 2>&1 &
```
## 创建目录
```
aliyunpan mkdir <目录>
```
### 例子
```
aliyunpan mkdir test123
```
## 删除文件/目录
```
aliyunpan rm <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
```
注意: 删除多个文件和目录时, 请确保每一个文件和目录都存在, 否则删除操作会失败.
被删除的文件或目录可在网盘文件回收站找回.
### 例子
```
# 删除 /我的文档/1.mp4
aliyunpan rm /我的文档/1.mp4
# 删除 /我的文档/1.mp4 和 /我的文档/2.mp4
aliyunpan rm /我的文档/1.mp4 /我的文档/2.mp4
# 删除 /我的文档 整个目录 !!
aliyunpan rm /我的文档
```
## 移动文件/目录
```
aliyunpan mv <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>
```
注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败.
### 例子
```
# 将 /我的文档/1.mp4 移动到 根目录 /
aliyunpan mv /我的文档/1.mp4 /
```
## 重命名文件/目录
```
aliyunpan rename <旧文件/目录名> <新文件/目录名>
```
注意: 重命名的文件/目录,如果指定的是绝对路径,则必须保证新旧的绝对路径在同一个文件夹内,否则重命名失败!
### 例子
```
# 将 /我的文档/1.mp4 重命名为 /我的文档/2.mp4
aliyunpan rename /我的文档/1.mp4 /我的文档/2.mp4
```
## 分享文件/目录
```
aliyunpan share
```
### 设置分享文件/目录
阿里目前只支持少数文件类型的分享,不支持的文件分享会提示分享失败
```
aliyunpan share set -mode 1 <文件/目录1> <文件/目录2> ...
```
### 创建快传链接
阿里的快传支持大部分文件的共享,例如zip压缩包,按照如下方式可以创建快传链接
```
aliyunpan share set -mode 3 <文件/目录1> <文件/目录2> ...
```
### 列出已分享文件/目录
```
aliyunpan share list
```
### 取消分享文件/目录
```
aliyunpan share cancel <shareid_1> <shareid_2> ...
```
目前只支持通过分享id (shareid) 来取消分享.
## 共享相册
```
aliyunpan album
```
### 展示共享相簿列表
```
aliyunpan album list
```
### 展示指定相簿中的文件
展示相簿中文件,同名的相簿只会展示第一个符合条件的
```
例如:展示相簿中文件"我的相簿2025"
aliyunpan album list-file "我的相簿2025"
```
### 下载相簿中的所有文件
下载相簿中的所有文件到本地,支持普通照片、实况照片。
```
下载相簿 "我的相簿2025" 里面的所有文件
aliyunpan album download-file 我的相簿2025
```
## 同步备份功能
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地两种模式。支持JavaScript插件对备份文件进行过滤。
指定本地目录和对应的一个网盘目录,以备份文件。网盘目录必须和本地目录独占使用,不要用作其他用途,不然备份可能会有问题。
备份功能支持以下模式:
1. 备份本地文件,即上传本地文件到网盘,始终保持本地文件有一个完整的备份在网盘
2. 备份云盘文件,即下载网盘文件到本地,始终保持网盘的文件有一个完整的备份在本地
备份功能支持指定备份策略:
1. exclusive,排他备份文件,目标目录多余的文件会被删除。保证备份的源目录,和目标目录文件一比一备份。源目录文件如果文件被删除,则对应的目标目录的文件也会被删除。
2. increment,增量备份文件,目标目录多余的文件不会被删除。只会把源目录修改的文件,新增的文件备份到目标目录。如果源目录有文件删除,或者目标目录有其他文件新增是不会被删除。
备份功能一般用于NAS等系统,进行文件备份。比如备份照片,就可以使用这个功能定期备份照片到云盘。
同步的基本逻辑如下所示,一次循环包括:扫描-对比-执行,一共三个环节。
![](../assets/images/sync_command-basic_logic.jpg)
### 常用命令说明
```
查看同步备份功能说明
aliyunpan sync
查看如何配置和启动同步备份功能
aliyunpan sync start -h
使用命令行配置启动同步备份服务,将本地目录 D:\tickstep\Documents\设计文档 中的文件备份上传到云盘目录 /sync_drive/我的文档
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "upload"
使用命令行配置启动同步备份服务,将云盘目录 /sync_drive/我的文档 中的文件备份下载到本地目录 D:\tickstep\Documents\设计文档
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "download"
使用命令行配置启动同步备份服务,将本地目录 D:\tickstep\Documents\设计文档 中的文件备份到云盘目录 /sync_drive/我的文档
同时配置下载并发为2,上传并发为1,下载分片大小为256KB,上传分片大小为1MB
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "upload" -dp 2 -up 1 -dbs 256 -ubs 1024
使用配置文件启动同步备份服务,使用配置文件可以支持同时启动多个备份任务。配置文件必须存在,否则启动失败。
aliyunpan sync start
使用配置文件启动同步备份服务,并配置下载并发为2,上传并发为1,下载分片大小为256KB,上传分片大小为1MB
aliyunpan sync start -dp 2 -up 1 -dbs 256 -ubs 1024
```
### 备份配置文件说明
如果你只有一个文件夹进行备份建议直接使用命令行配置启动即可。如果需要同时启动多个备份任务,则可以使用备份配置文件启动同步备份任务。
配置文件如下所示,如果你有通过环境变量```ALIYUNPAN_CONFIG_DIR```设置配置目录,则需要将```sync_drive```文件夹拷贝到配置的目录中才可以生效。
```
配置文件需要保存在:(配置目录)/sync_drive/sync_drive_config.json,样例如下:
{
"configVer": "1.0",
"syncTaskList": [
{
"name": "设计文档备份",
"localFolderPath": "D:/tickstep/Documents/设计文档",
"panFolderPath": "/备份盘/我的文档",
"mode": "upload",
"driveName": "backup"
},
{
"name": "手机图片备份",
"localFolderPath": "D:/tickstep/Photos/手机图片",
"panFolderPath": "/备份盘/手机图片",
"mode": "upload",
"driveName": "resource"
}
]
}
相关字段说明如下:
name - 任务名称
localFolderPath - 本地目录
panFolderPath - 网盘目录
mode - 模式,支持: upload(备份本地文件到云盘),download(备份云盘文件到本地)
driveName - 网盘,支持:backup(备份盘), resource(资源盘)
```
### 命令行启动
需要先进行登录。然后使用以下命令运行即可,该命令是阻塞的不会退出。
```
使用命令行配置启动同步备份服务,将本地目录 /tickstep/Documents/设计文档 中的文件备份上传到云盘目录 /备份盘/我的文档
./aliyunpan sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload" -drive "backup"
参数说明
ldir:本地目录
pdir:云盘目录
mode:备份模式,支持:upload(备份本地文件到云盘),download(备份云盘文件到本地)
drive - 网盘,支持:backup(备份盘), resource(资源盘)
--------------------------------------------------------------
正常会有以下的输出:
启动同步备份进程
备份配置文件:(使用命令行配置)
链接类型:默认链接
下载并发:2
上传并发:2
下载分片大小:1.00MB
上传分片大小:10.00MB
启动同步任务
任务: 设计文档(de3d6b69a607497b73624bcca0845f19)
同步模式: 备份本地文件(只上传)
本地目录: /tickstep/Documents/设计文档
云盘目录: /备份盘/我的文档
```
### Linux后台启动
建议结合nohup进行启动。
sync.sh脚本,内容如下
```
#!/bin/bash
# 是否开启调试日志
export ALIYUNPAN_VERBOSE=0
# (可选)配置目录的绝对路径(请更改成你自己的目录)
export ALIYUNPAN_CONFIG_DIR=/Users/tickstep/Applications/adrive/config
# aliyunpan程序所在的绝对路径(请更改成你自己的目录)
export ALIYUNPAN_BIN=/Users/tickstep/Applications/adrive/aliyunpan
# 本地目录(请更改成你自己的目录)
LOCAL_DIR="/tickstep/Documents/设计文档"
# 网盘目录(请更改成你自己的目录)
PAN_DIR="/备份盘/我的文档"
# 指定配置参数并进行启动
# 支持的模式:upload(备份本地文件到云盘),download(备份云盘文件到本地)
"$ALIYUNPAN_BIN" sync start -ldir "$LOCAL_DIR" -pdir "$PAN_DIR" -mode "upload" -drive "backup"
```
增加脚本执行权限
```
$ chmod +x sync.sh
```
然后启动该脚本进行后台运行
```
$ nohup ./sync.sh >/dev/null 2>&1 &
```
### Windows后台启动
需要结合 [WinSW](https://github.com/winsw/winsw) 进行后台启动,请前往官网自行下载: https://github.com/winsw/winsw
步骤如下:
1. 配置好备份任务:(配置目录)\sync_drive\sync_drive_config.json
2. 下载winsw.exe并更名为alisync.exe
3. 新增一个alisync.xml文件,内容如下:
```xml
<service>
<id>alisync</id>
<name>alisync</name>
<description>aliyunpan-sync 后台备份服务。</description>
<env name="ALIYUNPAN_CONFIG_DIR" value="(更改成你PC上aliyunpan.exe工具所在目录)"/>
<executable>aliyunpan.exe</executable>
<arguments>sync start</arguments>
<log mode="roll"></log>
</service>
```
4. 将alisync.exe和alisync.xml存放在你PC上aliyunpan.exe工具所在目录,例如:
![](../assets/images/win10-alisync-service.png)
5. CMD命令行启动程序
```
# 安装服务(只需要第一次安装,后面不用再安装)
D:\Program Files\aliyunpan>alisync install
# 启动服务
D:\Program Files\aliyunpan>alisync start
2022-06-21 13:39:05,795 INFO - Starting service 'alisync (alisync)'...
2022-06-21 13:39:06,361 INFO - Service 'alisync (alisync)' started successfully.
# 查看服务状态
D:\Program Files\aliyunpan>alisync status
Started
# 停止服务
D:\Program Files\aliyunpan>alisync stop
2022-06-21 13:42:32,201 INFO - Stopping service 'alisync (alisync)'...
2022-06-21 13:42:32,211 INFO - Service 'alisync (alisync)' stopped successfully.
```
效果截图如下
![](../assets/images/win10-alisync-service-bg.png)
### Docker运行
详情文档请参考dockerhub网址:[tickstep/aliyunpan-sync](https://hub.docker.com/r/tickstep/aliyunpan-sync)
1. 直接运行
```
docker run -d --name=aliyunpan-sync --restart=always -v "<your aliyunpan_config.json>:/home/app/config/aliyunpan_config.json" -v "<your local dir>:/home/app/data" -e ALIYUNPAN_PAN_DIR="<your drive pan dir>" -e ALIYUNPAN_SYNC_MODE="upload" -e ALIYUNPAN_SYNC_POLICY="increment" -e ALIYUNPAN_SYNC_DRIVE="backup" -e ALIYUNPAN_SYNC_LOG="true" tickstep/aliyunpan-sync:<tag>
<your aliyunpan_config.json>: 用户已经登录成功并保存好的aliyunpan_config.json凭据文件
<your local dir>:本地目标目录,绝对路径,例如:/tickstep/Documents/设计文档
ALIYUNPAN_PAN_DIR:云盘目标目录,绝对路径
ALIYUNPAN_SYNC_MODE:备份模式,支持: upload(备份本地文件到云盘),download(备份云盘文件到本地)
ALIYUNPAN_SYNC_POLICY:备份策略,支持:exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)
ALIYUNPAN_SYNC_DRIVE: 网盘,支持:backup(备份盘), resource(资源盘)
ALIYUNPAN_SYNC_LOG: 同步日志,true-开启同步日志显示,false-关闭同步日志
```
2. docker-compose运行
docker-compose.yml 文件如下所示,为了方便说明增加了相关的注释,部署的时候可以去掉注释。
```
version: '3'
services:
sync:
image: tickstep/aliyunpan-sync:<tag>
container_name: aliyunpan-sync
restart: always
volumes:
# (必须)映射的本地目录
- ./data:/home/app/data:rw
# (可选)可以指定JS插件sync_handler.js用于过滤文件,详见插件说明
#- ./plugin/js/sync_handler.js:/home/app/config/plugin/js/sync_handler.js
# (推荐)挂载sync_drive同步数据库到本地,这样即使容器销毁,同步数据库还可以用于以后使用
#- ./sync_drive:/home/app/config/sync_drive
# (必须)映射token凭据文件
- /your/file/path/for/aliyunpan_config.json:/home/app/config/aliyunpan_config.json
environment:
# 时区,东8区
- TZ=Asia/Shanghai
# 下载文件并发数
- ALIYUNPAN_DOWNLOAD_PARALLEL=2
# 上传文件并发数
- ALIYUNPAN_UPLOAD_PARALLEL=2
# 下载数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
- ALIYUNPAN_DOWNLOAD_BLOCK_SIZE=1024
# 上传数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
- ALIYUNPAN_UPLOAD_BLOCK_SIZE=10240
# 指定网盘文件夹作为备份目标目录,不要指定根目录
- ALIYUNPAN_PAN_DIR=/my_sync_dir
# 备份模式:upload(备份本地文件到云盘), download(备份云盘文件到本地)
- ALIYUNPAN_SYNC_MODE=upload
# 备份策略: exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)
- ALIYUNPAN_SYNC_POLICY=increment
# 备份周期, 支持两种: infinity(永久循环备份),onetime(只运行一次备份)
- ALIYUNPAN_SYNC_CYCLE=infinity
# 网盘:backup(备份盘), resource(资源盘)
- ALIYUNPAN_SYNC_DRIVE=backup
# 是否显示文件备份过程日志,true-显示,false-不显示
- ALIYUNPAN_SYNC_LOG=true
# 本地文件修改检测延迟间隔,单位秒。如果本地文件会被频繁修改,例如录制视频文件,配置好该时间可以避免上传未录制好的文件
- ALIYUNPAN_LOCAL_DELAY_TIME=3
```
3. sync_handler.js插件说明
可以使用JS插件过滤备份的文件。更多细节请查看文档:[JavaScript插件手册](https://github.com/tickstep/aliyunpan/blob/main/docs/plugin_manual.md#如何使用)
## JavaScript插件
本程序支持javascript插件,更多细节请查看文档:[JavaScript插件手册](https://github.com/tickstep/aliyunpan/blob/main/docs/plugin_manual.md)
## 显示和修改程序配置项
```
# 显示配置
aliyunpan config
# 设置配置
aliyunpan config set
```
### 例子
```
# 显示所有可以设置的值
aliyunpan config -h
aliyunpan config set -h
# 设置下载文件的储存目录
aliyunpan config set -savedir D:/Downloads
# 设置下载最大并发量为 15
aliyunpan config set -max_download_parallel 15
# 组合设置
aliyunpan config set -max_download_parallel 15 -savedir D:/Downloads
```
# 常见问题Q&A
## 1 如何开启Debug调试日志
当需要定位问题,或者提交issue的时候抓取log,则需要开启debug日志。步骤如下:
### 第一步
Linux&MacOS
命令行运行
```
export ALIYUNPAN_VERBOSE=1
```
Windows
不同版本会有些许不一样,请自行查询具体方法
设置示意图如下:
![](../assets/images/win10-env-debug-config.png)
### 第二步
打开aliyunpan命令行程序,任何云盘命令都有类似如下日志输出
![](../assets/images/debug-log-screenshot.png)
+553
View File
@@ -0,0 +1,553 @@
# 目录
- [简介](#简介)
- [如何使用](#如何使用)
- [JS中内置的函数](#JS中内置的函数)
+ [console.log()](#consolelog)
+ [console.println()](#consoleprintln)
+ [PluginUtil.Http.get()](#PluginUtilHttpget)
+ [PluginUtil.Http.post()](#PluginUtilHttppost)
+ [PluginUtil.LocalFS.deleteFile()](#PluginUtilLocalFSdeleteFile)
+ [PluginUtil.PanFS.deleteFile()](#PluginUtilPanFSdeleteFile)
+ [PluginUtil.Email.sendTextMail()](#PluginUtilEmailsendTextMail)
+ [PluginUtil.Email.sendHtmlMail()](#PluginUtilEmailsendHtmlMail)
+ [PluginUtil.KV.putString()](#PluginUtilKVputString)
+ [PluginUtil.KV.getString()](#PluginUtilKVgetString)
+ [PluginUtil.HashTool.md5Hex()](#PluginUtilHashToolmd5Hex)
- [常见场景样例](#常见场景样例)
+ [1.禁止特定文件上传](#1禁止特定文件上传)
+ [2.上传文件后删除本地文件](#2上传文件后删除本地文件)
+ [3.下载文件并截断过长的文件名](#3下载文件并截断过长的文件名)
+ [4.上传文件去掉文件名包含的部分字符](#4上传文件去掉文件名包含的部分字符)
+ [5.上传文件时过滤指定目录或者文件路径](#5上传文件时过滤指定目录或者文件路径)
+ [6.下载云盘文件到本地后删除云盘对应的文件](#6下载云盘文件到本地后删除云盘对应的文件)
+ [7.Token刷新失败发送外部通知](#7Token刷新失败发送外部通知)
+ [8.每次只下载指定数量的文件](#8每次只下载指定数量的文件)
# 简介
本程序支持javascript插件。通过JS插件,你可以按照自己的需要定制上传、下载、同步、删除过程中关键步骤的行为,最大程度满足自己的个性化需求。
例如:
1. 排除某个特定敏感文件的上传
2. 上传文件进行改名,但是本地文件不做更改
3. 上传完成文件,删除本地文件
4. 上传的文件路径进行更改,但是本地的文件保持不变
5. 上传文件成功后,通过HTTP通知其他服务
6. 排除某些网盘文件的下载
7. 下载的文件进行改名,但是网盘的文件保持不变
8. 下载的文件路径进行更改,但是网盘的文件保持不变
9. 下载文件完成后,通过HTTP通知其他服务
10. 同步备份功能,支持过滤本地文件,或者过滤云盘文件。定制上传或者下载需要同步的文件
# 如何使用
JS插件的样本文件默认存放在程序所在的```plugin/js```文件夹下,分别为:
1. 下载插件(download_handler.js.sample)
2. 上传插件(upload_handler.js.sample)
3. 删除插件(remove_handler.js.sample)
4. 同步备份插件(sync_handler.js.sample)
5. 用户Token插件(token_handler.js.sample)
建议拷贝一份并将后缀名更改为.js,例如:upload_handler.js,不然插件不会生效。
你必须具备一定的JS语言基础,然后按照里面的样例根据自己所需进行改动即可。如果你不会JS那也没关系,你可以提issue需求,然后我们开发成员或者网友会给你提供JS脚本代码。
注意:如果你有通过环境变量```ALIYUNPAN_CONFIG_DIR```设置配置目录,则需要将plugin文件夹拷贝到配置的目录中才可以生效。
# JS中内置的函数
目前开放了如下函数,你可以在你的js脚本中直接调用,以用于增强JS脚本的扩展性、可玩性以及可适用性。
## console.log()
打印日志,这个日志需要开启debug日志才会在控制台窗口显示
```js
console.log("hello world");
```
## console.println()
打印日志,这个日志会直接在控制台窗口显示,无需开启debug日志
```js
console.println("hello world");
```
## PluginUtil.Http.get()
发起HTTP的get请求
```js
var header = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36",
"Content-Type": "application/json",
"Accept": "application/json"
};
try {
var r = PluginUtil.Http.get(header, "https://625f528c53a42eaa07f37e13.mockapi.io/files/1");
console.log(r);
} catch (e) {
if (e !== "Error") {
throw e;
}
}
```
## PluginUtil.Http.post()
发起HTTP的post请求
```js
var header = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36",
"Content-Type": "application/json",
"Accept": "application/json"
};
try {
var reqDataStr = JSON.stringify({
"id": "1",
"localFilePath": "/usr/local/src/borders_burundi_producer.gram.aab",
"localFileSize": 1111,
"uploadApproved": false
});
var r = PluginUtil.Http.post(header, "https://625f528c53a42eaa07f37e13.mockapi.io/files", reqDataStr);
console.log(r);
} catch (e) {
if (e !== "Error") {
throw e;
}
}
```
## PluginUtil.LocalFS.deleteFile()
删除本地指定文件,不支持文件夹
```
PluginUtil.LocalFS.deleteFile(localFilePath);
其中:
localFilePath - 本地文件的绝对完整路径
```
样例
```js
PluginUtil.LocalFS.deleteFile("/Users/tickstep/Downloads/IMG_0884.HEIC");
```
## PluginUtil.PanFS.deleteFile()
删除云盘指定文件,支持文件、文件夹
```
PluginUtil.PanFS.deleteFile(userId, driveId, panFileId);
其中:
userId - 登录的用户ID
driveId - 网盘ID
panFileId - 网盘文件ID
```
样例
```js
var userId = context["userId"]
var driveId = params["driveId"]
var driveFileId = params["driveFileId"]
if (PluginUtil.PanFS.deleteFile(userId, driveId, driveFileId)) {
console.println("插件删除云盘文件成功")
}
```
## PluginUtil.Email.sendTextMail()
发送文本邮件,定义如下
```
PluginUtil.Email.sendTextMail(mailServer, userName, password, to, subject, body)
其中:
mailServer - smtp服务器+端口
userName - 发件人邮箱地址
password - 发件人邮箱密码
to - 收件人邮箱地址
subject - 邮件标题
body - 邮件内容,纯文本
```
样例
```js
PluginUtil.Email.sendTextMail("smtp.qq.com:465", "123xxx@qq.com", "pwdxxxxxx", "12545xxx@qq.com", "文件上传通知", "该文件已经上传完毕");
```
## PluginUtil.Email.sendHtmlMail()
发送HTML富文本邮件,定义如下
```
PluginUtil.Email.sendHtmlMail(mailServer, userName, password, to, subject, body)
其中:
mailServer - smtp服务器+端口
userName - 发件人邮箱地址
password - 发件人邮箱密码
to - 收件人邮箱地址
subject - 邮件标题
body - 邮件内容,HTML富文本
```
样例
```js
var html = "<html>"
+"<body>"
+"<h1>文件上传通知</h1>"
+"<h2>该文件已经上传完毕</h2>"
+"</body>"
+"</html>";
PluginUtil.Email.sendHtmlMail("smtp.qq.com:465", "123xxx@qq.com", "pwdxxxxxx", "12545xxx@qq.com", "文件上传通知", html);
```
## PluginUtil.KV.putString()
存储键值对,定义如下
```
PluginUtil.KV.putString(key, value)
其中:
key - 键,字符串
value - 值,字符串
```
样例
```js
PluginUtil.KV.putString("mykey", "1670419352");
```
## PluginUtil.KV.getString()
获取存储的键值,指定对应的键,返回存储的值,如果没有则返回空字符串,定义如下
```
PluginUtil.KV.getString(key)
其中:
key - 键,字符串
```
样例
```js
var value = PluginUtil.KV.getString("mykey");
```
## PluginUtil.HashTool.md5Hex()
计算指定字符串text的MD5值,返回hex格式的字符串MD5,定义如下
```
PluginUtil.HashTool.md5Hex(text)
其中:
text - 字符串
```
样例
```js
var md5 = PluginUtil.HashTool.md5Hex("123456");
```
# 常见场景样例
这里收集了一些常见的需求样例,可以作为插件定制的样例模板。
## 1.禁止特定文件上传
使用JavaScript上传插件中的`uploadFilePrepareCallback`函数。如下所示:
```js
function uploadFilePrepareCallback(context, params) {
console.log(params)
var result = {
"uploadApproved": "yes",
"driveFilePath": ""
};
if (params["localFileType"] != "file") {
// do nothing
return result;
}
// 禁止点号.开头的文件上传
if (params["localFileName"].indexOf(".") == 0) {
result["uploadApproved"] = "no";
}
// 禁止.txt文件上传
if (params["localFileName"].search(/.txt$/i) >= 0) {
result["uploadApproved"] = "no";
}
// 禁止password.key文件上传
if (params["localFileName"] == "password.key") {
result["uploadApproved"] = "no";
}
return result;
}
```
## 2.上传文件后删除本地文件
使用JavaScript上传插件中的`uploadFileFinishCallback`函数。如下所示:
```js
function uploadFileFinishCallback(context, params) {
console.log(params);
if (params["localFileType"] != "file") {
// do nothing
return;
}
if (params["uploadResult"] == "success") {
PluginUtil.LocalFS.deleteFile(params["localFilePath"]);
}
}
```
## 3.下载文件并截断过长的文件名
有些文件的路径或者名称太长,下载的时候可能会由于路径名称过程导致无法下载,这时候可以使用JavaScript下载插件中的`downloadFilePrepareCallback`函数定制下载保存的文件名。
如下所示:
```js
function downloadFilePrepareCallback(context, params) {
console.log(params)
var result = {
"downloadApproved": "yes",
"localFilePath": ""
};
if (params["driveFileType"] != "file") {
return result;
}
// 下面的代码都是分隔路径,方便后面修改路径时使用
var filePath = params["localFilePath"];
filePath = filePath.replace(/\\/g, "/");
// 目录完整路径
var dirPath = "";
// 文件名,不包括后缀名
var fileName = "";
// 文件后缀名
var fileExt = "";
var idx = filePath.lastIndexOf('/');
if (idx > 0) {
dirPath = filePath.substring(0,idx);
fileName = filePath.substring(idx+1,filePath.length);
} else {
fileName = filePath;
}
idx = fileName.lastIndexOf(".")
if (idx > 0) {
fileExt = fileName.substring(idx,fileName.length);
fileName = fileName.substring(0, fileName.length-fileExt.length)
}
// 开始按照需要截断太长的文件路径,例如下面的这个例子:
//
// dirPath + "/" ==> 这个的意思是保留前面的文件夹路径,如果不需要就去掉
// fileName.substr(0,10) ==> 文件名只取前面10个字符,其他的不要了
// + fileExt ==> 把文件的后缀名补上
var saveFilePath = dirPath + "/" + fileName.substr(0,10) + fileExt;
// 返回
result["localFilePath"] = saveFilePath;
return result;
}
```
效果如下:
1)假如网盘源路径是:`/亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi`
2)下载后保存本地的路径是:`D:\test\亚马逊书籍合集\亚马逊 kindle ebook 大合集5289册\经济金融 (2)\解密Instagra.mobi`
```
[1] ----
文件ID: 630f0623e67c0a1d2e554b1994a29108d089f0d5
文件名: 解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi
文件类型: 文件
文件路径: /亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi
插件修改文件下载保存路径为: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi
[1] 准备下载: /亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi
[1] 将会下载到路径: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi
[1] 下载开始
[1] 下载完成, 保存位置: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi
[1] 检验文件有效性成功: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi
```
## 4.上传文件去掉文件名包含的部分字符
例如本地文件夹名称为```[周杰伦]范特西[mp3]```,上传到网盘希望能更改成名称```[周杰伦]范特西```但保持本地文件夹名称不变,因为文件夹有很多不希望每个手动更改,可以这样实现:
```js
function uploadFilePrepareCallback(context, params) {
var result = {
"uploadApproved": "yes",
"driveFilePath": ""
};
// 去掉网盘保存路径中包含的[mp3]字段
var filePath = params["driveFilePath"];
filePath = filePath.replace(/\[mp3\]/g, "");
result["driveFilePath"] = filePath;
return result;
}
```
## 5.上传文件时过滤指定目录或者文件路径
upload命令本身支持exn参数排除上传文件或目录,但是只能指定文件名称,不能指定文件的路径。如果需要排除指定路径则可以通过插件脚本实现,样例如下:
```js
function uploadFilePrepareCallback(context, params) {
//(自行配置)禁止上传的本地【目录列表】,使用绝对路径,可以配置多个路径用逗号分隔
var forbiddenUploadFolders = ["/Users/tickstep/Downloads/up/target","/Users/tickstep/Downloads/up/.idea"]
//(自行配置)禁止上传的本地【文件列表】,使用绝对路径,可以配置多个路径用逗号分隔
var forbiddenUploadFiles = ["/Users/tickstep/Downloads/up/pom.xml"]
// -------------------- 以下代码不要修改 --------------------
var result = {
"uploadApproved": "yes",
"driveFilePath": ""
};
// 下面的代码都是分隔路径,方便后面使用
var filePath = params["localFilePath"];
filePath = filePath.replace(/\\/g, "/");
// 目录完整路径
var dirPath = "";
// 文件名,不包括后缀名
var fileName = "";
// 文件后缀名
var fileExt = "";
var idx = filePath.lastIndexOf('/');
if (idx > 0) {
dirPath = filePath.substring(0,idx);
fileName = filePath.substring(idx+1,filePath.length);
} else {
fileName = filePath;
}
idx = fileName.lastIndexOf(".")
if (idx > 0) {
fileExt = fileName.substring(idx,fileName.length);
fileName = fileName.substring(0, fileName.length-fileExt.length)
}
if (params["localFileType"] == "file") {
for (var i = 0; i < forbiddenUploadFiles.length; i++) {
if (forbiddenUploadFiles[i].replace(/\\/g, "/") == params["localFilePath"]) {
result["uploadApproved"] = "no"; // 禁止文件上传
break
}
}
for (var i = 0; i < forbiddenUploadFolders.length; i++) {
if (forbiddenUploadFolders[i].replace(/\\/g, "/") == dirPath) {
result["uploadApproved"] = "no"; // 禁止文件上传
break
}
}
} else if (params["localFileType"] == "folder") {
for (var i = 0; i < forbiddenUploadFolders.length; i++) {
if (forbiddenUploadFolders[i].replace(/\\/g, "/") == params["localFilePath"]) {
result["uploadApproved"] = "no"; // 禁止文件上传
break
}
}
}
return result;
}
```
## 6.下载云盘文件到本地后删除云盘对应的文件
```js
function downloadFileFinishCallback(context, params) {
console.log(params)
// 云盘文件成功下载到本地后,删除云盘的文件
if (params["downloadResult"] == "success") {
if (params["driveFileType"] == "file") {
// 文件下载成功,删除该云盘文件
var userId = context["userId"]
var driveId = params["driveId"]
var driveFileId = params["driveFileId"]
if (PluginUtil.PanFS.deleteFile(userId, driveId, driveFileId)) {
console.println("插件删除云盘文件成功:" + params["driveFilePath"])
}
}
}
}
```
## 7.Token刷新失败发送外部通知
Token刷新失败发送外部通知,例如Server酱
```js
function userTokenRefreshFinishCallback(context, params) {
var header = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36",
"Content-Type": "application/json",
"Accept": "application/json"
};
try {
if (params["result"] === "fail") {
// 避免频繁发送
var ONE_MINUTE = 60 * 1000;
var lastSendEmailTime = PluginUtil.KV.getString("email_last_send_time");
if (lastSendEmailTime != "") {
if ((Date.now() - lastSendEmailTime) < (10 * ONE_MINUTE)) {
console.log("距离上次发送邮件小于10分钟,先不发送了");
return
}
}
PluginUtil.KV.putString("email_last_send_time", Date.now());
var reqData = {
"text": "Token刷新失败",
"desp": params["message"]
};
var r = PluginUtil.Http.post(header, "https://sctapi.ftqq.com/xxxxxkeyxxxxxx.send", JSON.stringify(reqData));
}
} catch (e) {
if (e !== "Error") {
throw e;
}
}
}
```
或者发送邮件通知
```js
function userTokenRefreshFinishCallback(context, params) {
try {
if (params["result"] === "fail") {
// 避免频繁发送
var ONE_MINUTE = 60 * 1000;
var lastSendEmailTime = PluginUtil.KV.getString("email_last_send_time");
if (lastSendEmailTime != "") {
if ((Date.now() - lastSendEmailTime) < (10 * ONE_MINUTE)) {
console.log("距离上次发送邮件小于10分钟,先不发送了");
return
}
}
PluginUtil.KV.putString("email_last_send_time", Date.now());
// 发送通知邮件,请确保你的发送邮箱具备smtp发送权限,没有的需要找邮箱提供商配置开启
console.log("发送通知邮件");
PluginUtil.Email.sendTextMail("smtp.qq.com:465", "123xxx@qq.com", "pwdxxxxxx", "12545xxx@qq.com", "Token刷新失败了", "Token过期了,骚年快去手动恢复吧");
}
} catch (e) {
if (e !== "Error") {
throw e;
}
}
}
```
## 8.每次只下载指定数量的文件
每次运行download下载命令,只下载指定数量的文件,只要数量够了其他文件就跳过不再下载,其他文件等下一次运行download命令的时候再下载。
```js
function downloadFilePrepareCallback(context, params) {
var result = {
"downloadApproved": "yes",
"localFilePath": ""
};
// 这次下载限制的最大文件总数量
const maxCountOfDownloadAction = 3;
// 只处理文件
if (params["driveFileType"] != "file") {
return
}
// 获取这次下载动作,已经下载的文件数量
var keyOfThisDownloadAction = "download:" + params["downloadActionId"];
var valueOfThisDownloadAction = PluginUtil.KV.getString(keyOfThisDownloadAction);
if (valueOfThisDownloadAction == "") {
valueOfThisDownloadAction = "0";
}
var countOfThisDownloadAction = parseInt(valueOfThisDownloadAction);
if (countOfThisDownloadAction >= maxCountOfDownloadAction) {
// 下载数量已达到最大,其他文件不再下载
result["downloadApproved"] = "no";
return result;
}
// 该文件需要下载,记录相关信息
var keyOfThisDownloadFile = "file:" + PluginUtil.HashTool.md5Hex(params["driveFilePath"]);
var valueOfThisDownloadFile = PluginUtil.KV.getString(keyOfThisDownloadFile);
if (valueOfThisDownloadFile == "finish") {
// 已经在下载的文件不做处理
return result;
}
PluginUtil.KV.putString(keyOfThisDownloadFile, "downloading");
// 更新下载总数量
countOfThisDownloadAction += 1;
PluginUtil.KV.putString(keyOfThisDownloadAction, String(countOfThisDownloadAction));
return result;
}
function downloadFileFinishCallback(context, params) {
var keyOfThisDownloadFile = "file:" + PluginUtil.HashTool.md5Hex(params["driveFilePath"]);
console.log(keyOfThisDownloadFile)
PluginUtil.KV.putString(keyOfThisDownloadFile, "finish");
}
```
+27 -7
View File
@@ -1,22 +1,42 @@
module github.com/tickstep/aliyunpan
go 1.16
go 1.22
require (
github.com/GeertJohan/go.incremental v1.0.0
github.com/json-iterator/go v1.1.10
github.com/adrg/xdg v0.4.0
github.com/deckarep/golang-set v1.8.0
github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e
github.com/json-iterator/go v1.1.12
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce
github.com/peterh/liner v1.2.1
github.com/tickstep/bolt v1.3.3
github.com/tickstep/aliyunpan-api v0.0.3
github.com/tickstep/library-go v0.0.5
github.com/satori/go.uuid v1.2.0
github.com/tickstep/aliyunpan-api v0.2.9
github.com/tickstep/bolt v1.3.4
github.com/tickstep/library-go v0.1.3
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 // indirect
golang.org/x/sys v0.15.0
)
require (
github.com/boltdb/bolt v1.3.1 // indirect
github.com/btcsuite/btcd v0.22.1 // indirect
github.com/cpuguy83/go-md2man v1.0.10 // indirect
github.com/denisbrodbeck/machineid v1.0.1 // indirect
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/russross/blackfriday v1.5.2 // indirect
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
golang.org/x/text v0.3.7 // indirect
)
//replace github.com/boltdb/bolt => github.com/tickstep/bolt v1.3.4
//replace github.com/tickstep/bolt => /Users/tickstep/Documents/Workspace/go/projects/bolt
//replace github.com/tickstep/library-go => /Users/tickstep/Documents/Workspace/go/projects/library-go
//replace github.com/tickstep/aliyunpan-api => /Users/tickstep/Documents/Workspace/go/projects/aliyunpan-api
+92 -49
View File
@@ -1,46 +1,95 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.1 h1:CnwP9LM/M9xuRrGSCGeMVs9iv09uMqwsVX7EeIpgV2c=
github.com/btcsuite/btcd v0.22.1/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4=
github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1 h1:9/4Hyp+A98nqssD7qceXwoLeAn75MZr25IN76dxlfbg=
github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1/go.mod h1:TQJQ+ZNyFVvUtUEtCZxBhfWiH7RJqR3EivNmvD6Waik=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e h1:OGunVjqY7y4U4laftpEHv+mvZBlr7UGimJXKEGQtg48=
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb h1:x0yCvYsspui5SAxSRvLd2zFg7PfFijzKdCo7QAtN92I=
github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb/go.mod h1:ym0w0flrmBtGvApLDgFLa0sfGJkWxDQqnm0/0ok5w3Y=
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce h1:RLmZmfx/K62HKpbwPqtW3tg+V2GgugN/XNNx+uiMH/Y=
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
github.com/peterh/liner v1.1.1-0.20190305032635-6f820f8f90ce h1:Lz+a/i+oS4A7tb6J6IyH4ZFiWgqvNv2yslv0Qn79wok=
github.com/peterh/liner v1.1.1-0.20190305032635-6f820f8f90ce/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/peterh/liner v1.2.1 h1:O4BlKaq/LWu6VRWmol4ByWfzx6MfXc5Op5HETyIy5yg=
github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
@@ -48,56 +97,50 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tickstep/aliyunpan-api v0.0.2 h1:aXChXeb1vmpINqV2qf60Bi3SGKgZPdmV7D8Eymb29Yw=
github.com/tickstep/aliyunpan-api v0.0.2/go.mod h1:EdciagdVeAHfqvzStVEVp1UVtySaq1DFd5RSucDfKWY=
github.com/tickstep/aliyunpan-api v0.0.3 h1:DBxRIjRUuebIjAgAPtS7rYZ4tafzOEF6J2949V2FeDY=
github.com/tickstep/aliyunpan-api v0.0.3/go.mod h1:EdciagdVeAHfqvzStVEVp1UVtySaq1DFd5RSucDfKWY=
github.com/tickstep/bolt v1.3.3 h1:3abb88T5JJocnBlYTJE3EqKvwWV6+D/JZD2Fsdt1QHI=
github.com/tickstep/bolt v1.3.3/go.mod h1:Y24RwCywbOsKAyHiVcJ4K2QQfED+q67x7PEOH9OG7Q4=
github.com/tickstep/cloudpan189-api v0.0.3 h1:L8woe6XSUjQOsa7XRC7zH8VZj8SNKo/1qQb1k2ttLkU=
github.com/tickstep/cloudpan189-api v0.0.3/go.mod h1:1H1r6h5fOu8sEhPY6h09YTuSt4OKl5uFJchFscBFRwo=
github.com/tickstep/cloudpan189-api v0.0.4 h1:qsEhqvL2L5P6EHP+vfLhVqyjJbcHUkE9JEfUiyu1z0g=
github.com/tickstep/cloudpan189-api v0.0.4/go.mod h1:1H1r6h5fOu8sEhPY6h09YTuSt4OKl5uFJchFscBFRwo=
github.com/tickstep/cloudpan189-api v0.0.5 h1:R46M4op2aABKJgR2S8c/KUe/fIQYbUpQGxlJ9SUnsBo=
github.com/tickstep/cloudpan189-api v0.0.5/go.mod h1:HYu2wtfBDRldAnohj9UmNpEZipQDMm2wOYszNuLzZ9s=
github.com/tickstep/cloudpan189-api v0.0.6 h1:b+ctfvWfOVyq/Zot3rCrJxqf85b+toDSNLeGon4OWQo=
github.com/tickstep/cloudpan189-api v0.0.6/go.mod h1:qhNVXxF1UGOApXy9uG+UuKHReFI1GpwHn0pzL4tqSd8=
github.com/tickstep/library-go v0.0.1 h1:UbXeGE6ZxnxA6KTjMofEhG3h2aHJ4UGacfh7U2B4zgw=
github.com/tickstep/library-go v0.0.1/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
github.com/tickstep/library-go v0.0.2 h1:0JCxT2ZzRMrydUyqou3d9FI44ULrlxnkFcyioaGUbrE=
github.com/tickstep/library-go v0.0.2/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
github.com/tickstep/library-go v0.0.3 h1:j6C2qIYMC33Re/qa3cYS49DSMalgjtLR4KqGi6d1qBI=
github.com/tickstep/library-go v0.0.3/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
github.com/tickstep/library-go v0.0.4 h1:wNR48yDlaikxdJokL/NYX0J4ul8NN+TmBfXjxO2kfNw=
github.com/tickstep/library-go v0.0.4/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
github.com/tickstep/library-go v0.0.5 h1:MBb1tsvs4Wi67zy0E9eobVWLgsfPRLsqKAEdSEi3LBE=
github.com/tickstep/library-go v0.0.5/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tickstep/aliyunpan-api v0.2.9 h1:0n7BNjmKEi/JYqNLhotBnCQJmJE3OAqiDX28q0Gknxs=
github.com/tickstep/aliyunpan-api v0.2.9/go.mod h1:UvQgQYMIgA5k1QsfKIVnEJ2h/HqDXIkAb16oookNc8o=
github.com/tickstep/bolt v1.3.4 h1:UN1txRsauOfR8a5mUWm5So0ng233PsIMVR5/+3WowCE=
github.com/tickstep/bolt v1.3.4/go.mod h1:Y24RwCywbOsKAyHiVcJ4K2QQfED+q67x7PEOH9OG7Q4=
github.com/tickstep/library-go v0.1.3 h1:OUj6nkimTsqhHXh5s+PbmHq5hR0m739Y1tu9QpfA2ng=
github.com/tickstep/library-go v0.1.3/go.mod h1:uAHeNOIpoywCzlaeLrWmmRSupn03m9kJVZKOEmuarmA=
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f h1:xKDKjIsL76VUyHcA0G4Qe1cIAUB/nrq6Pt8D411bd1g=
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f/go.mod h1:qXyCeJubPqsgeiLd3kvHOGHHSrQcNdjZ2ScXIcVZK/I=
github.com/xujiajun/gorouter v1.2.0/go.mod h1:yJrIta+bTNpBM/2UT8hLOaEAFckO+m/qmR3luMIQygM=
github.com/xujiajun/mmap-go v1.0.1 h1:7Se7ss1fLPPRW+ePgqGpCkfGIZzJV6JPq9Wq9iv/WHc=
github.com/xujiajun/mmap-go v1.0.1/go.mod h1:CNN6Sw4SL69Sui00p0zEzcZKbt+5HtEnYUsc6BKKRMg=
github.com/xujiajun/nutsdb v0.5.1-0.20200830145825-432dd3d0c873 h1:pgTLmYRrfy9lGWgXrEqaIhn4WSJb9xA5Q+Uir2Dxeag=
github.com/xujiajun/nutsdb v0.5.1-0.20200830145825-432dd3d0c873/go.mod h1:Q8FXi2zeQRluPpUl/CKQ6J7u/9gcI02J6cZp3owFLyA=
github.com/xujiajun/utils v0.0.0-20190123093513-8bf096c4f53b h1:jKG9OiL4T4xQN3IUrhUpc1tG+HfDXppkgVcrAiiaI/0=
github.com/xujiajun/utils v0.0.0-20190123093513-8bf096c4f53b/go.mod h1:AZd87GYJlUzl82Yab2kTjx1EyXSQCAfZDhpTo1SQC4k=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/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-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+481
View File
@@ -0,0 +1,481 @@
// Copyright (c) 2020 tickstep.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"encoding/json"
"fmt"
"github.com/olekukonko/tablewriter"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/file/downloader"
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
"github.com/tickstep/aliyunpan/internal/global"
"github.com/tickstep/aliyunpan/internal/taskframework"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"github.com/tickstep/library-go/requester/rio/speeds"
"github.com/urfave/cli"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
func CmdAlbum() cli.Command {
return cli.Command{
Name: "album",
Aliases: []string{"abm"},
Usage: "共享相册",
UsageText: cmder.App().Name + " album",
Category: "阿里云盘",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
},
Subcommands: []cli.Command{
{
Name: "list",
Aliases: []string{"ls"},
Usage: "展示共享相簿列表",
UsageText: cmder.App().Name + " album list",
Description: `
示例:
展示共享相簿列表
aliyunpan album ls
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
RunShareAlbumList()
return nil
},
Flags: []cli.Flag{},
},
{
Name: "list-file",
Aliases: []string{"lf"},
Usage: "展示相簿中的文件",
UsageText: cmder.App().Name + " album list-file",
Description: `
展示相簿中文件,同名的相簿只会展示第一个符合条件的
示例:
展示相簿中文件"我的相簿2022"
aliyunpan album list-file "我的相簿2022"
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
RunShareAlbumListFile(c.Args().Get(0))
return nil
},
Flags: []cli.Flag{},
},
{
Name: "download-file",
Aliases: []string{"df"},
Usage: "下载相簿中的所有文件到本地",
UsageText: cmder.App().Name + " album download-file",
Description: `
下载相簿中的所有文件
示例:
下载相簿 "我的相簿2022" 里面的所有文件
aliyunpan album download-file 我的相簿2022
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
subArgs := c.Args()
if len(subArgs) == 0 {
fmt.Println("请指定下载的相簿名称")
return nil
}
// 处理saveTo
var (
saveTo string
)
if c.String("saveto") != "" {
saveTo = filepath.Clean(c.String("saveto"))
}
do := &DownloadOptions{
IsPrintStatus: false,
IsExecutedPermission: false,
IsOverwrite: c.Bool("ow"),
SaveTo: saveTo,
Parallel: 0,
SliceParallel: 0,
Load: 0,
MaxRetry: pandownload.DefaultDownloadMaxRetry,
NoCheck: false,
ShowProgress: !c.Bool("np"),
DriveId: parseDriveId(c),
ExcludeNames: []string{},
}
RunShareAlbumDownloadFile(c.Args(), do)
return nil
},
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ow",
Usage: "overwrite, 覆盖已存在的文件",
},
cli.StringFlag{
Name: "saveto",
Usage: "将下载的文件直接保存到指定的目录",
},
cli.BoolFlag{
Name: "np",
Usage: "no progress 不展示下载进度条",
},
},
},
},
}
}
func RunShareAlbumList() {
activeUser := GetActiveUser()
records, err := activeUser.PanClient().OpenapiPanClient().ShareAlbumListGetAll()
if err != nil {
fmt.Printf("获取相簿列表失败: %s\n", err)
return
}
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "ALBUM_ID", "名称", "更新日期", "创建日期"})
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT})
for k, record := range records {
tb.Append([]string{strconv.Itoa(k + 1), record.AlbumId, record.Name, record.UpdatedAtStr(), record.CreatedAtStr()})
}
tb.Render()
}
func getShareAlbumFromName(activeUser *config.PanUser, name string) *aliyunpan.AlbumEntity {
records, err := activeUser.PanClient().OpenapiPanClient().ShareAlbumListGetAll()
if err != nil {
fmt.Printf("获取相簿列表失败: %s\n", err)
return nil
}
for _, record := range records {
if name == record.Name {
return record
}
}
return nil
}
func RunShareAlbumListFile(name string) {
if len(name) == 0 {
fmt.Printf("相簿名称不能为空\n")
return
}
activeUser := GetActiveUser()
record := getShareAlbumFromName(activeUser, name)
if record == nil {
return
}
fileList, er := activeUser.PanClient().OpenapiPanClient().ShareAlbumListFileGetAll(&aliyunpan.ShareAlbumListFileParam{
AlbumId: record.AlbumId,
Limit: 100,
})
if er != nil {
fmt.Printf("获取相簿文件列表失败:%s\n", er)
return
}
renderTable(opLs, false, "", fileList)
}
func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
if len(albumNames) == 0 {
fmt.Printf("请指定相簿名称\n")
return
}
activeUser := GetActiveUser()
if options == nil {
options = &DownloadOptions{}
}
if options.MaxRetry < 0 {
options.MaxRetry = pandownload.DefaultDownloadMaxRetry
}
options.IsExecutedPermission = false
// 设置下载配置
cfg := &downloader.Config{
Mode: transfer.RangeGenMode_BlockSize,
CacheSize: config.Config.CacheSize,
BlockSize: MaxDownloadRangeSize,
MaxRate: config.Config.MaxDownloadRate,
InstanceStateStorageFormat: downloader.InstanceStateStorageFormatJSON,
ShowProgress: options.ShowProgress,
ExcludeNames: options.ExcludeNames,
}
if cfg.CacheSize == 0 {
cfg.CacheSize = int(DownloadCacheSize)
}
// 设置下载最大并发量
if options.Parallel < 1 {
options.Parallel = config.Config.MaxDownloadParallel
if options.Parallel == 0 {
options.Parallel = config.DefaultFileDownloadParallelNum
}
}
if options.Parallel > config.MaxFileDownloadParallelNum {
options.Parallel = config.MaxFileDownloadParallelNum
}
// 设置单个文件下载分片线程数
if options.SliceParallel < 1 { // 用户没有主动设置,则使用下面自动配置的策略
if options.Parallel > 1 {
// 如果是多文件下载,则一个文件只能同时使用1个线程
options.SliceParallel = 1
} else {
// 如果是单文件下载,则可以使用3个最大线程
options.SliceParallel = 3
}
}
// 保存文件的本地根文件夹
originSaveRootPath := ""
if options.SaveTo != "" {
originSaveRootPath = options.SaveTo
} else {
// 使用默认的保存路径
originSaveRootPath = GetActiveUser().GetSavePath("")
}
fi, err1 := os.Stat(originSaveRootPath)
if err1 != nil && !os.IsExist(err1) {
os.MkdirAll(originSaveRootPath, 0777) // 首先在本地创建目录
} else {
if !fi.IsDir() {
fmt.Println("本地保存路径不是文件夹,请删除或者创建对应的文件夹:", originSaveRootPath)
return
}
}
fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 下载缓存为: %s\n\n", options.Parallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
var (
panClient = activeUser.PanClient()
)
cfg.MaxParallel = options.Parallel
cfg.SliceParallel = options.SliceParallel
var (
executor = taskframework.TaskExecutor{
IsFailedDeque: true, // 统计失败的列表
}
statistic = &pandownload.DownloadStatistic{}
)
// 配置执行器任务并发数,即同时下载文件并发数
executor.SetParallel(cfg.MaxParallel)
// 全局速度统计
globalSpeedsStat := &speeds.Speeds{}
// 处理队列
allShareAlbumList, err := activeUser.PanClient().OpenapiPanClient().ShareAlbumListGetAll()
if err != nil {
fmt.Printf("获取相簿列表失败: %s\n", err)
return
}
for k := range albumNames {
var record *aliyunpan.AlbumEntity
for _, album := range allShareAlbumList {
if album.Name == albumNames[k] {
record = album
break
}
}
if record == nil {
continue
}
// 获取相簿下的所有文件
fileList, er := activeUser.PanClient().OpenapiPanClient().ShareAlbumListFileGetAll(&aliyunpan.ShareAlbumListFileParam{
AlbumId: record.AlbumId,
Limit: 100,
})
if er != nil {
fmt.Printf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k])
continue
}
if fileList == nil || len(fileList) == 0 {
fmt.Printf("相簿里面没有文件: %s\n", albumNames[k])
continue
}
// 相薄文件是没有子文件夹的,这个fileList就是该相册里面的全部文件了,所以不需要再遍历子文件夹了
idx := 0
for {
if idx >= len(fileList) {
break
}
f := fileList.Item(idx)
idx += 1
// 处理实况照片
if f.IsAlbumLivePhotoFile() {
// 如果是实况照片,则需要下载图片+视频两个文件
// 获取下载链接
durl, apierr := activeUser.PanClient().OpenapiPanClient().ShareAlbumGetFileDownloadUrl(&aliyunpan.ShareAlbumGetFileUrlParam{
AlbumId: f.AlbumId,
DriveId: f.DriveId,
FileId: f.FileId,
})
if apierr != nil {
logger.Verbosef("ERROR: get album file download url error: %s\n", f.FileId)
fmt.Printf("\n下载照片失败: %s\n", f.FileName)
continue
}
if durl.StreamsUrl != nil { // 实况图片(照片+视频)下载链接
// 照片文件
photoFile := cloneFileEntity(f)
photoFileSize := int64(0)
if durl.StreamsUrl.Heic != "" {
beforeStr, _ := strings.CutSuffix(f.FileName, ".livp")
photoFile.FileName = beforeStr + ".HEIC"
photoFile.FileExtension = "heic"
photoFileSize = getHttpDownloadFileSize(durl.StreamsUrl.Heic)
} else if durl.StreamsUrl.Jpeg != "" {
beforeStr, _ := strings.CutSuffix(f.FileName, ".livp")
photoFile.FileName = beforeStr + ".JPG"
photoFile.FileExtension = "jpg"
photoFileSize = getHttpDownloadFileSize(durl.StreamsUrl.Jpeg)
}
photoFile.FileSize = photoFileSize
fileList = append(fileList, photoFile)
// 视频文件
videoFile := cloneFileEntity(f)
videoFile.FileSize = f.FileSize - photoFileSize
if durl.StreamsUrl.Mov != "" {
beforeStr, _ := strings.CutSuffix(f.FileName, ".livp")
videoFile.FileName = beforeStr + ".MOV"
videoFile.FileExtension = "mov"
}
fileList = append(fileList, videoFile)
}
continue
}
// 补全虚拟网盘路径,规则:/共享相册/<相簿名称>/文件名称
f.Path = "/共享相册/" + albumNames[k] + "/" + f.FileName
// 生成下载项
newCfg := *cfg
unit := pandownload.DownloadTaskUnit{
Cfg: &newCfg, // 复制一份新的cfg
PanClient: panClient,
VerbosePrinter: panCommandVerbose,
ParentTaskExecutor: &executor,
DownloadStatistic: statistic,
IsPrintStatus: options.IsPrintStatus,
IsExecutedPermission: options.IsExecutedPermission,
IsOverwrite: options.IsOverwrite,
NoCheck: options.NoCheck,
FilePanSource: global.AlbumSource,
FilePanPath: f.Path,
DriveId: f.DriveId, // 一个相簿的文件会来自多个网盘(资源库/备份盘)
GlobalSpeedsStat: globalSpeedsStat,
FileRecorder: nil,
}
// 设置相簿文件信息
unit.SetFileInfo(global.AlbumSource, f)
// 设置储存的路径
if options.SaveTo != "" {
unit.OriginSaveRootPath = options.SaveTo
unit.SavePath = filepath.Join(options.SaveTo, f.Path)
} else {
// 使用默认的保存路径
unit.OriginSaveRootPath = GetActiveUser().GetSavePath("")
unit.SavePath = GetActiveUser().GetSavePath(f.Path)
}
info := executor.Append(&unit, options.MaxRetry)
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
}
}
// 开始计时
statistic.StartTimer()
// 开始执行
executor.Execute()
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
// 输出失败的文件列表
failedList := executor.FailedDeque()
if failedList.Size() != 0 {
fmt.Printf("以下文件下载失败: \n")
tb := cmdtable.NewTable(os.Stdout)
for e := failedList.Shift(); e != nil; e = failedList.Shift() {
item := e.(*taskframework.TaskInfoItem)
tb.Append([]string{item.Info.Id(), item.Unit.(*pandownload.DownloadTaskUnit).FilePanPath})
}
tb.Render()
}
}
func cloneFileEntity(entity *aliyunpan.FileEntity) *aliyunpan.FileEntity {
data, err := json.Marshal(entity) // 序列化原始对象
if err != nil {
panic(err)
}
newEntity := &aliyunpan.FileEntity{}
err = json.Unmarshal(data, newEntity) // 反序列化到新的对象中
if err != nil {
panic(err)
}
return newEntity
}
func getHttpDownloadFileSize(fileUrl string) int64 {
client := requester.NewHTTPClient()
client.SetKeepAlive(true)
client.SetTimeout(10 * time.Minute)
// header
headers := map[string]string{
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"referer": "https://www.aliyundrive.com/",
}
resp, err := client.Req("GET", fileUrl, nil, headers)
if err != nil {
return -1
}
fileSize := resp.ContentLength
return fileSize
}
+787
View File
@@ -0,0 +1,787 @@
// Copyright (c) 2020 tickstep.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"github.com/olekukonko/tablewriter"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/file/downloader"
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
"github.com/tickstep/aliyunpan/internal/taskframework"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester/rio/speeds"
"github.com/urfave/cli"
"os"
"path/filepath"
"strconv"
"sync/atomic"
"time"
)
type (
AlbumFileCategoryOption string
)
var (
ImageOnlyOption AlbumFileCategoryOption = "image"
VideoOnlyOption AlbumFileCategoryOption = "video"
ImageVideoOnlyOption AlbumFileCategoryOption = "image_video"
AllFileOption AlbumFileCategoryOption = "none"
)
func CmdAlbumWeb() cli.Command {
return cli.Command{
Name: "albumw",
Usage: "个人相册",
UsageText: cmder.App().Name + " albumw",
Category: "阿里云盘",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
},
Subcommands: []cli.Command{
{
Name: "list",
Aliases: []string{"ls"},
Usage: "展示相簿列表",
UsageText: cmder.App().Name + " album list",
Description: `
示例:
展示相簿列表
aliyunpan albumw ls
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
RunAlbumList()
return nil
},
Flags: []cli.Flag{},
},
{
Name: "new",
Aliases: []string{""},
Usage: "创建相簿",
UsageText: cmder.App().Name + " album new",
Description: `
示例:
新建相簿,名称为:我的相簿2022
aliyunpan albumw new "我的相簿2022"
新建相簿,名称为:我的相簿2022,描述为:存放2022所有文件
aliyunpan albumw new "我的相簿2022" "存放2022所有文件"
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
RunAlbumCreate(c.Args().Get(0), c.Args().Get(1))
return nil
},
Flags: []cli.Flag{},
},
{
Name: "rm",
Aliases: []string{""},
Usage: "删除相簿",
UsageText: cmder.App().Name + " album rm",
Description: `
删除相簿,同名的相簿只会删除第一个符合条件的
示例:
删除名称为"我的相簿2022"的相簿
aliyunpan albumw rm "我的相簿2022"
删除名称为"我的相簿2022-1" 和 "我的相簿2022-2"的相簿
aliyunpan albumw rm "我的相簿2022-1" "我的相簿2022-2"
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
RunAlbumDelete(c.Args())
return nil
},
Flags: []cli.Flag{},
},
{
Name: "rename",
Aliases: []string{""},
Usage: "重命名相簿",
UsageText: cmder.App().Name + " album rename",
Description: `
重命名相簿,同名的相簿只会修改第一个符合条件的
示例:
重命名相簿"我的相簿2022"为新的名称"我的相簿2022-new"
aliyunpan albumw rename "我的相簿2022" "我的相簿2022-new"
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
RunAlbumRename(c.Args().Get(0), c.Args().Get(1))
return nil
},
Flags: []cli.Flag{},
},
{
Name: "list-file",
Aliases: []string{"lf"},
Usage: "展示相簿中的文件",
UsageText: cmder.App().Name + " album list-file",
Description: `
展示相簿中文件,同名的相簿只会展示第一个符合条件的
示例:
展示相簿中文件"我的相簿2022"
aliyunpan albumw list-file "我的相簿2022"
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
RunAlbumListFile(c.Args().Get(0))
return nil
},
Flags: []cli.Flag{},
},
{
Name: "rm-file",
Aliases: []string{"rf"},
Usage: "移除相簿中的文件",
UsageText: cmder.App().Name + " album rm-file",
Description: `
移除相簿中的文件,同名的相簿只会移除第一个符合条件的
示例:
移除相簿 "我的相簿2022" 中的文件 1.png 2.png
aliyunpan albumw rm-file 我的相簿2022 1.png 2.png
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
subArgs := c.Args()
if len(subArgs) < 2 {
fmt.Println("请指定移除的文件")
return nil
}
RunAlbumRmFile(subArgs[0], subArgs[1:])
return nil
},
Flags: []cli.Flag{},
},
{
Name: "add-file",
Aliases: []string{"af"},
Usage: "增加(文件/相册)网盘文件到相簿中",
UsageText: cmder.App().Name + " album add-file",
Description: `
增加文件到相簿中
示例:
增加当前目录下的 1.png 2.png 文件到相簿 "我的相簿2022" 中
aliyunpan albumw add-file 我的相簿2022 1.png 2.png
增加当前目录下的 myFolder 文件夹下所有文件到相簿 "我的相簿2022" 中
aliyunpan albumw add-file 我的相簿2022 myFolder
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
subArgs := c.Args()
if len(subArgs) < 2 {
fmt.Println("请指定增加的文件")
return nil
}
RunAlbumAddFile(subArgs[0], subArgs[1:], ImageVideoOnlyOption)
return nil
},
Flags: []cli.Flag{},
},
{
Name: "download-file",
Aliases: []string{"df"},
Usage: "下载相簿中的所有文件到本地",
UsageText: cmder.App().Name + " album download-file",
Description: `
下载相簿中的所有文件
示例:
下载相簿 "我的相簿2022" 里面的所有文件
aliyunpan albumw download-file 我的相簿2022
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
subArgs := c.Args()
if len(subArgs) == 0 {
fmt.Println("请指定下载的相簿名称")
return nil
}
// 处理saveTo
var (
saveTo string
)
if c.String("saveto") != "" {
saveTo = filepath.Clean(c.String("saveto"))
}
do := &DownloadOptions{
IsPrintStatus: false,
IsExecutedPermission: false,
IsOverwrite: c.Bool("ow"),
SaveTo: saveTo,
Parallel: 0,
Load: 0,
MaxRetry: pandownload.DefaultDownloadMaxRetry,
NoCheck: false,
ShowProgress: !c.Bool("np"),
DriveId: parseDriveId(c),
ExcludeNames: []string{},
}
RunAlbumDownloadFile(c.Args(), do)
return nil
},
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ow",
Usage: "overwrite, 覆盖已存在的文件",
},
cli.StringFlag{
Name: "saveto",
Usage: "将下载的文件直接保存到指定的目录",
},
cli.BoolFlag{
Name: "np",
Usage: "no progress 不展示下载进度条",
},
},
},
},
}
}
func RunAlbumList() {
activeUser := GetActiveUser()
records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{})
if err != nil {
fmt.Printf("获取相簿列表失败: %s\n", err)
return
}
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "ALBUM_ID", "名称", "文件数量", "创建日期", "修改日期"})
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT})
for k, record := range records {
tb.Append([]string{strconv.Itoa(k + 1), record.AlbumId, record.Name, strconv.Itoa(record.FileCount),
record.CreatedAtStr(), record.UpdatedAtStr()})
}
tb.Render()
}
func RunAlbumCreate(name, description string) {
if name == "" {
fmt.Printf("相簿名称不能为空\n")
return
}
activeUser := GetActiveUser()
_, err := activeUser.PanClient().WebapiPanClient().AlbumCreate(&aliyunpan_web.AlbumCreateParam{
Name: name,
Description: description,
})
if err != nil {
fmt.Printf("创建相簿失败: %s\n", err)
return
}
fmt.Printf("创建相簿成功: %s\n", name)
}
func RunAlbumDelete(nameList []string) {
if len(nameList) == 0 {
fmt.Printf("相簿名称不能为空\n")
return
}
activeUser := GetActiveUser()
records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{})
if err != nil {
fmt.Printf("获取相簿列表失败: %s\n", err)
return
}
for _, record := range records {
for i, name := range nameList {
if name == record.Name {
nameList = append(nameList[:i], nameList[i+1:]...)
_, err := activeUser.PanClient().WebapiPanClient().AlbumDelete(&aliyunpan_web.AlbumDeleteParam{
AlbumId: record.AlbumId,
})
if err != nil {
fmt.Printf("删除相簿失败: %s\n", name)
return
} else {
fmt.Printf("删除相簿成功: %s\n", name)
}
break
}
}
}
}
func getAlbumFromName(activeUser *config.PanUser, name string) *aliyunpan.AlbumEntity {
records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{})
if err != nil {
fmt.Printf("获取相簿列表失败: %s\n", err)
return nil
}
for _, record := range records {
if name == record.Name {
return record
}
}
return nil
}
func RunAlbumRename(name, newName string) {
if len(name) == 0 {
fmt.Printf("相簿名称不能为空\n")
return
}
if len(newName) == 0 {
fmt.Printf("相簿名称不能为空\n")
return
}
activeUser := GetActiveUser()
record := getAlbumFromName(activeUser, name)
if record == nil {
return
}
_, err := activeUser.PanClient().WebapiPanClient().AlbumEdit(&aliyunpan_web.AlbumEditParam{
AlbumId: record.AlbumId,
Description: record.Description,
Name: newName,
})
if err != nil {
fmt.Printf("重命名相簿失败: %s\n", name)
return
} else {
fmt.Printf("重命名相簿成功: %s -> %s\n", name, newName)
}
}
func RunAlbumListFile(name string) {
if len(name) == 0 {
fmt.Printf("相簿名称不能为空\n")
return
}
activeUser := GetActiveUser()
record := getAlbumFromName(activeUser, name)
if record == nil {
return
}
fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{
AlbumId: record.AlbumId,
})
if er != nil {
fmt.Printf("获取相簿文件列表失败:%s\n", er)
return
}
renderTable(opLs, false, "", fileList)
}
func RunAlbumRmFile(name string, nameList []string) {
if len(name) == 0 {
fmt.Printf("相簿名称不能为空\n")
return
}
if len(nameList) == 0 {
fmt.Printf("指定文件不能为空\n")
return
}
activeUser := GetActiveUser()
album := getAlbumFromName(activeUser, name)
if album == nil {
return
}
fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{
AlbumId: album.AlbumId,
})
if er != nil {
fmt.Printf("获取相簿文件列表失败:%s\n", er)
return
}
param := &aliyunpan_web.AlbumDeleteFileParam{
AlbumId: album.AlbumId,
DriveFileList: []aliyunpan.FileBatchActionParam{},
}
for _, file := range fileList {
if len(nameList) == 0 {
break
}
for i, name := range nameList {
if name == file.FileName {
nameList = append(nameList[:i], nameList[i+1:]...)
param.AddFileItem(file.DriveId, file.FileId)
break
}
}
}
// 1-500 范围
if len(param.DriveFileList) == 0 {
fmt.Printf("没有符合的文件\n")
return
}
// delete file
_, e := activeUser.PanClient().WebapiPanClient().AlbumDeleteFile(param)
if e != nil {
fmt.Printf("删除相簿文件失败:%s\n", e)
return
}
fmt.Printf("删除相簿文件成功:%s\n", name)
}
// RunAlbumAddFile 增加网盘文件到相簿
func RunAlbumAddFile(albumName string, filePathList []string, filterOption AlbumFileCategoryOption) {
activeUser := GetActiveUser()
if albumName == "" {
fmt.Printf("必须指定相簿名称\n")
return
}
album := getAlbumFromName(activeUser, albumName)
if album == nil {
fmt.Printf("相簿不存在\n")
return
}
paths, err := makePathAbsolute(activeUser.ActiveDriveId, filePathList...)
if err != nil {
fmt.Println(err)
return
}
if len(paths) == 0 {
fmt.Printf("没有有效的文件\n")
return
}
fmt.Printf("正在获取增加的文件信息,该操作可能会非常耗费时间,请耐心等待...\n")
param := &aliyunpan_web.AlbumAddFileParam{
AlbumId: album.AlbumId,
DriveFileList: []aliyunpan.FileBatchActionParam{},
}
for k := range paths {
filePath := paths[k]
fileInfo, apierr := activeUser.PanClient().WebapiPanClient().FileInfoByPath(activeUser.ActiveDriveId, filePath)
if apierr != nil {
fmt.Printf("获取文件信息失败: %s\n", filePath)
continue
}
if fileInfo.IsFile() {
// file
if isFileMatchCondition(fileInfo, filterOption) {
param.AddFileItem(fileInfo.DriveId, fileInfo.FileId)
}
} else {
// folder
activeUser.PanClient().WebapiPanClient().FilesDirectoriesRecurseList(activeUser.ActiveDriveId, fileInfo.Path, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
if apiError != nil {
logger.Verbosef("%s\n", apiError)
return true
}
if !fd.IsFolder() {
if isFileMatchCondition(fd, filterOption) {
param.AddFileItem(fd.DriveId, fd.FileId)
}
}
time.Sleep(2 * time.Second)
return true
})
}
time.Sleep(2 * time.Second)
}
if len(param.DriveFileList) == 0 {
fmt.Printf("没有符合的文件\n")
return
}
// add file
_, e := activeUser.PanClient().WebapiPanClient().AlbumAddFile(param)
if e != nil {
fmt.Printf("增加相簿文件失败:%s\n", e)
return
}
fmt.Printf("增加相簿文件成功:%s\n", albumName)
}
func isFileMatchCondition(fileInfo *aliyunpan.FileEntity, filterOption AlbumFileCategoryOption) bool {
if fileInfo == nil {
return false
}
if filterOption == ImageOnlyOption {
return fileInfo.Category == "image"
} else if filterOption == VideoOnlyOption {
return fileInfo.Category == "video"
} else if filterOption == ImageVideoOnlyOption {
return fileInfo.Category == "image" || fileInfo.Category == "video"
} else if filterOption == AllFileOption {
return true
}
return false
}
func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
if len(albumNames) == 0 {
fmt.Printf("相簿名称不能为空\n")
return
}
activeUser := GetActiveUser()
activeUser.PanClient().WebapiPanClient().EnableCache()
activeUser.PanClient().WebapiPanClient().ClearCache()
defer activeUser.PanClient().WebapiPanClient().DisableCache()
// pan token expired checker
continueFlag := int32(0)
atomic.StoreInt32(&continueFlag, 0)
defer func() {
atomic.StoreInt32(&continueFlag, 1)
}()
go func(flag *int32) {
for atomic.LoadInt32(flag) == 0 {
time.Sleep(time.Duration(1) * time.Minute)
re := RefreshWebTokenInNeed(activeUser)
if re.Code == panlogin.SUCCESS {
logger.Verboseln("update access token for download task")
}
}
}(&continueFlag)
if options == nil {
options = &DownloadOptions{}
}
if options.MaxRetry < 0 {
options.MaxRetry = pandownload.DefaultDownloadMaxRetry
}
options.IsExecutedPermission = false
// 设置下载配置
cfg := &downloader.Config{
Mode: transfer.RangeGenMode_BlockSize,
CacheSize: config.Config.CacheSize,
BlockSize: MaxDownloadRangeSize,
MaxRate: config.Config.MaxDownloadRate,
InstanceStateStorageFormat: downloader.InstanceStateStorageFormatJSON,
ShowProgress: options.ShowProgress,
ExcludeNames: options.ExcludeNames,
}
if cfg.CacheSize == 0 {
cfg.CacheSize = int(DownloadCacheSize)
}
// 设置下载最大并发量
if options.Parallel < 1 {
options.Parallel = config.Config.MaxDownloadParallel
if options.Parallel == 0 {
options.Parallel = config.DefaultFileDownloadParallelNum
}
}
if options.Parallel > config.MaxFileDownloadParallelNum {
options.Parallel = config.MaxFileDownloadParallelNum
}
// 保存文件的本地根文件夹
originSaveRootPath := ""
if options.SaveTo != "" {
originSaveRootPath = options.SaveTo
} else {
// 使用默认的保存路径
originSaveRootPath = GetActiveUser().GetSavePath("")
}
fi, err1 := os.Stat(originSaveRootPath)
if err1 != nil && !os.IsExist(err1) {
os.MkdirAll(originSaveRootPath, 0777) // 首先在本地创建目录
} else {
if !fi.IsDir() {
fmt.Println("本地保存路径不是文件夹,请删除或者创建对应的文件夹:", originSaveRootPath)
return
}
}
fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 下载缓存为: %s\n\n", options.Parallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
var (
panClient = activeUser.PanClient()
)
cfg.MaxParallel = options.Parallel
var (
executor = taskframework.TaskExecutor{
IsFailedDeque: true, // 统计失败的列表
}
statistic = &pandownload.DownloadStatistic{}
)
// 配置执行器任务并发数,即同时下载文件并发数
executor.SetParallel(cfg.MaxParallel)
// 全局速度统计
globalSpeedsStat := &speeds.Speeds{}
// 处理队列
for k := range albumNames {
record := getAlbumFromName(activeUser, albumNames[k])
if record == nil {
continue
}
// 获取相簿下的所有文件
fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{
AlbumId: record.AlbumId,
})
if er != nil {
fmt.Printf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k])
continue
}
if fileList == nil || len(fileList) == 0 {
fmt.Printf("相簿里面没有文件: %s\n", albumNames[k])
continue
}
for _, f := range fileList {
// 补全虚拟网盘路径,规则:/<相簿名称>/文件名称
f.Path = "/" + albumNames[k] + "/" + f.FileName
// 生成下载项
newCfg := *cfg
unit := pandownload.DownloadTaskUnit{
Cfg: &newCfg, // 复制一份新的cfg
PanClient: panClient,
VerbosePrinter: panCommandVerbose,
ParentTaskExecutor: &executor,
DownloadStatistic: statistic,
IsPrintStatus: options.IsPrintStatus,
IsExecutedPermission: options.IsExecutedPermission,
IsOverwrite: options.IsOverwrite,
NoCheck: options.NoCheck,
FilePanPath: f.Path,
DriveId: f.DriveId, // 必须使用文件的DriveId,因为一个相簿的文件会来自多个网盘(资源库/备份盘)
GlobalSpeedsStat: globalSpeedsStat,
FileRecorder: nil,
}
// TODO: 相册下载需要重构
// 设置相簿文件信息
//unit.SetFileInfo(pandownload.AlbumFileSource, f)
// 设置储存的路径
if options.SaveTo != "" {
unit.OriginSaveRootPath = options.SaveTo
unit.SavePath = filepath.Join(options.SaveTo, f.Path)
} else {
// 使用默认的保存路径
unit.OriginSaveRootPath = GetActiveUser().GetSavePath("")
unit.SavePath = GetActiveUser().GetSavePath(f.Path)
}
info := executor.Append(&unit, options.MaxRetry)
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
}
}
// 开始计时
statistic.StartTimer()
// 开始执行
executor.Execute()
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
// 输出失败的文件列表
failedList := executor.FailedDeque()
if failedList.Size() != 0 {
fmt.Printf("以下文件下载失败: \n")
tb := cmdtable.NewTable(os.Stdout)
for e := failedList.Shift(); e != nil; e = failedList.Shift() {
item := e.(*taskframework.TaskInfoItem)
tb.Append([]string{item.Info.Id(), item.Unit.(*pandownload.DownloadTaskUnit).FilePanPath})
}
tb.Render()
}
}
-303
View File
@@ -1,303 +0,0 @@
// Copyright (c) 2020 tickstep
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/functions/panupload"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"os"
"path"
"path/filepath"
"strings"
"sync"
)
func CmdBackup() cli.Command {
return cli.Command{
Name: "backup",
Description: `备份指定 <文件/目录> 到云盘 <目标目录> 中
和上传的功能一样,只是备份多进行了如下操作
1. 增加了数据库,记录已经上传的文件信息。
目前只记录 文件位置、大小、修改时间、MD5 。
2. 上传前先根据数据库记录判断是否需要重新上传。
3. 强制同名覆盖。
注:只备份(上传)新的文件(同名覆盖),不处理删除操作。
示例:
1. 将本地的 C:\Users\Administrator\Video 整个目录备份到网盘 /视频 目录
注意区别反斜杠 "\" 和 斜杠 "/" !!!
aliyunpan-go backup C:/Users/Administrator/Video /视频
2. 将本地的 C:\Users\Administrator\Video 整个目录备份到网盘 /视频 目录,但是排除所有的.jpg文件
aliyunpan-go backup -exn "\.jpg$" C:/Users/Administrator/Video /视频
3. 将本地的 C:\Users\Administrator\Video 整个目录备份到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
aliyunpan-go backup -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视频
4. 将本地的 C:\Users\Administrator\Video 整个目录备份到网盘 /视频 目录,但是排除所有的 @eadir 文件夹
aliyunpan-go backup -exn "^@eadir$" C:/Users/Administrator/Video /视频
参考:
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式。在正则表达式中,^表示匹配开头,$表示匹配结尾。
1)排除@eadir文件或者文件夹:-exn "^@eadir$"
2)排除.jpg文件:-exn "\.jpg$"
3)排除.号开头的文件:-exn "^\."
4)排除~号开头的文件:-exn "^~"
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
`,
Usage: "备份文件或目录",
UsageText: "backup <文件/目录路径1> <文件/目录2> <文件/目录3> ... <目标目录>",
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Action: Backup,
Flags: append(UploadFlags, cli.BoolFlag{
Name: "delete",
Usage: "通过本地数据库记录同步删除网盘文件",
}, cli.BoolFlag{
Name: "sync",
Usage: "本地同步到网盘(会同步删除网盘文件)",
}),
}
}
func OpenSyncDb(path string) (panupload.SyncDb, error) {
return panupload.OpenSyncDb(path, BackupMetaBucketName)
}
// 删除那些本地不存在而网盘存在的网盘文件 默认使用本地数据库判断,如果 flagSync 为 true 则遍历网盘文件列表进行判断(速度较慢)。
func DelRemoteFileFromDB(driveId string, localDir string, savePath string, flagSync bool) {
activeUser := config.Config.ActiveUser()
var db panupload.SyncDb
var err error
dbpath := filepath.Join(localDir, BackupMetaDirName)
db, err = OpenSyncDb(dbpath + string(os.PathSeparator) + "db")
if err != nil {
fmt.Println("同步数据库打开失败!", err)
return
}
defer db.Close()
savePath = path.Join(savePath, filepath.Base(localDir))
//判断本地文件是否存在,如果存在返回 true 否则删除数据库相关记录和网盘上的文件。
isLocalFileExist := func(ent *panupload.UploadedFileMeta) (isExists bool) {
testPath := strings.TrimPrefix(ent.Path, savePath)
testPath = filepath.Join(localDir, testPath)
logger.Verboseln("同步删除检测:", testPath, ent.Path)
//为防止误删,只有当 err 是文件不存在的时候才进行删除处理。
if fi, err := os.Stat(testPath); err == nil || !os.IsNotExist(err) {
//使用sync功能时没有传时间参数进来,为方便对比回写数据库需补上时间。
if fi != nil {
ent.ModTime = fi.ModTime().Unix()
}
return true
}
var err *apierror.ApiError
// 尝试从本地数据库查找
if ent.ParentId == "" {
if test := db.Get(path.Dir(ent.Path)); test != nil && test.IsFolder && test.FileId != "" {
ent.ParentId = test.FileId
}
}
// 从网盘查找
if ent.FileId == "" || ent.ParentId == "" {
efi, err := activeUser.PanClient().FileInfoById(driveId, ent.FileId)
//网盘上不存在这个文件或目录,只需要清理数据库
if err != nil && err.Code == apierror.ApiCodeFileNotFoundCode {
db.DelWithPrefix(ent.Path)
logger.Verboseln("删除数据库记录", ent.Path)
return
}
if efi != nil {
ent.FileId = efi.FileId
ent.ParentId = efi.ParentFileId
}
}
if ent.FileId == "" {
return
}
// 本地文件不存在
// 删除网盘对应文件
fileDeleteResult, err := activeUser.PanClient().FileDelete([]*aliyunpan.FileBatchActionParam{{DriveId:driveId, FileId:ent.FileId}})
if err != nil || len(fileDeleteResult) == 0 {
fmt.Println("删除网盘文件或目录失败", ent.Path, err)
} else {
db.DelWithPrefix(ent.Path)
logger.Verboseln("删除网盘文件和数据库记录", ent.Path)
}
return
}
// 根据数据库记录删除不存在的文件
if !flagSync {
for ent, err := db.First(savePath); err == nil; ent, err = db.Next(savePath) {
isLocalFileExist(ent)
}
return
}
parent := db.Get(savePath)
if parent.FileId == "" {
efi, err := activeUser.PanClient().FileInfoByPath(driveId, savePath)
if err != nil {
return
}
parent.FileId = efi.FileId
}
var syncFunc func(curPath, parentID string)
syncFunc = func(curPath, parentID string) {
param := &aliyunpan.FileListParam{
DriveId: driveId,
ParentFileId: parentID,
}
fileResult, err := activeUser.PanClient().FileListGetAll(param)
if err != nil {
return
}
if fileResult == nil || len(fileResult) == 0 {
return
}
for _, fileEntity := range fileResult {
ufm := &panupload.UploadedFileMeta{
FileId: fileEntity.FileId,
ParentId: fileEntity.ParentFileId,
Size: fileEntity.FileSize,
IsFolder: fileEntity.IsFolder(),
Path: path.Join(curPath, fileEntity.FileName),
SHA1: strings.ToLower(fileEntity.ContentHash),
}
if !isLocalFileExist(ufm) {
continue
}
//如果这是一个目录就直接更新数据库,否则判断原始记录的Hash信息,如果一致才更新。
if ufm.IsFolder {
db.Put(ufm.Path, ufm)
syncFunc(ufm.Path, ufm.FileId)
} else if test := db.Get(ufm.Path); test.SHA1 == ufm.SHA1 {
db.Put(ufm.Path, ufm)
}
}
}
//开启自动清理功能
db.AutoClean(parent.Path, true)
db.Put(parent.Path, parent)
syncFunc(savePath, parent.FileId)
}
func checkPath(localdir string) (string, error) {
fullPath, err := filepath.Abs(localdir)
if err != nil {
fullPath = localdir
}
if fi, err := os.Stat(fullPath); err != nil && !fi.IsDir() {
return fullPath, os.ErrInvalid
}
dbpath := filepath.Join(fullPath, BackupMetaDirName)
//数据库目录判断
fi, err := os.Stat(dbpath)
if err != nil {
if os.IsNotExist(err) {
err = os.Mkdir(dbpath, 0755)
}
if err != nil {
return fullPath, fmt.Errorf("数据库目录[%s]创建失败,跳过处理: %s", dbpath, err)
}
}
if fi != nil && !fi.IsDir() {
return fullPath, os.ErrPermission
}
return fullPath, nil
}
func Backup(c *cli.Context) error {
if c.NArg() < 2 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
subArgs := c.Args()
localpaths := make([]string, 0)
flagSync := c.Bool("sync")
flagDelete := c.Bool("delete")
opt := &UploadOptions{
AllParallel: c.Int("p"),
Parallel: 1, // 阿里云盘一个文件只支持单线程上传
MaxRetry: c.Int("retry"),
NoRapidUpload: c.Bool("norapid"),
ShowProgress: !c.Bool("np"),
IsOverwrite: true,
DriveId: parseDriveId(c),
ExcludeNames: c.StringSlice("exn"),
BlockSize: int64(c.Int("bs") * 1024),
}
localCount := c.NArg() - 1
savePath := GetActiveUser().PathJoin(opt.DriveId, subArgs[localCount])
wg := sync.WaitGroup{}
wg.Add(localCount)
for _, p := range subArgs[:localCount] {
go func(p string) {
defer wg.Done()
fullPath, err := checkPath(p)
switch err {
case nil:
if flagSync || flagDelete {
DelRemoteFileFromDB(opt.DriveId, fullPath, savePath, flagSync)
}
case os.ErrInvalid:
default:
return
}
localpaths = append(localpaths, fullPath)
}(p)
}
wg.Wait()
if len(localpaths) == 0 {
return nil
}
RunUpload(localpaths, savePath, opt)
return nil
}
+42 -9
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -33,14 +33,17 @@ func CmdCd() cli.Command {
切换 /我的资源 工作目录:
aliyunpan cd /我的资源
切换 /我的资源 工作目录,使用通配符:
aliyunpan cd /我的*
切换上级目录:
aliyunpan cd ..
切换根目录:
aliyunpan cd /
`,
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -69,7 +72,7 @@ func CmdPwd() cli.Command {
Usage: "输出工作目录",
UsageText: cmder.App().Name + " pwd",
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
@@ -78,6 +81,8 @@ func CmdPwd() cli.Command {
activeUser := config.Config.ActiveUser()
if activeUser.IsFileDriveActive() {
fmt.Println(activeUser.Workdir)
} else if activeUser.IsResourceDriveActive() {
fmt.Println(activeUser.ResourceWorkdir)
} else if activeUser.IsAlbumDriveActive() {
fmt.Println(activeUser.AlbumWorkdir)
}
@@ -90,24 +95,52 @@ func RunChangeDirectory(driveId, targetPath string) {
user := config.Config.ActiveUser()
targetPath = user.PathJoin(driveId, targetPath)
targetPathInfo, err := user.PanClient().FileInfoByPath(driveId, targetPath)
// 获取目标路径文件信息
targetPathInfo, err := user.PanClient().OpenapiPanClient().FileInfoByPath(driveId, targetPath)
if err != nil {
fmt.Println(err)
return
}
// 适配通配符路径获取目标文件信息(弃用,容易触发风控)
//files, err := matchPathByShellPattern(driveId, targetPath)
//if err != nil {
// fmt.Println(err)
// return
//}
//
//var targetPathInfo *aliyunpan.FileEntity
//if len(files) == 1 {
// targetPathInfo = files[0]
//} else {
// for _, f := range files {
// if f.IsFolder() {
// targetPathInfo = f
// break
// }
// }
//}
if targetPathInfo == nil {
fmt.Println("路径不存在")
return
}
if !targetPathInfo.IsFolder() {
fmt.Printf("错误: %s 不是一个目录 (文件夹)\n", targetPath)
fmt.Printf("错误: %s 不是一个目录 (文件夹)\n", targetPathInfo.Path)
return
}
if user.IsFileDriveActive() {
user.Workdir = targetPath
user.Workdir = targetPathInfo.Path
user.WorkdirFileEntity = *targetPathInfo
} else if user.IsResourceDriveActive() {
user.ResourceWorkdir = targetPathInfo.Path
user.ResourceWorkdirFileEntity = *targetPathInfo
} else if user.IsAlbumDriveActive() {
user.AlbumWorkdir = targetPath
user.AlbumWorkdir = targetPathInfo.Path
user.AlbumWorkdirFileEntity = *targetPathInfo
}
fmt.Printf("改变工作目录: %s\n", targetPath)
fmt.Printf("改变工作目录: %s\n", targetPathInfo.Path)
}
+31 -120
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -18,26 +18,13 @@ import (
"fmt"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdutil"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/library/crypto"
"github.com/tickstep/library-go/getip"
"github.com/urfave/cli"
"net/url"
"path"
"strconv"
"strings"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/internal/config"
)
type (
// 秒传数据项
RapidUploadItem struct {
FileSha1 string
FileSize int64
FilePath string // 绝对路径,包含文件名
}
)
type ()
const (
cryptoDescription = `
@@ -58,7 +45,7 @@ const (
var ErrBadArgs = errors.New("参数错误")
var ErrNotLogined = errors.New("未登录账号")
func GetActivePanClient() *aliyunpan.PanClient {
func GetActivePanClient() *config.PanClient {
return config.Config.ActiveUser().PanClient()
}
@@ -74,96 +61,16 @@ func parseDriveId(c *cli.Context) string {
return driveId
}
// newRapidUploadItem 通过解析秒传链接创建秒传实体
func newRapidUploadItem(rapidUploadShareLink string) (*RapidUploadItem, error) {
if strings.IndexAny(rapidUploadShareLink, "aliyunpan://") != 0 {
return nil, fmt.Errorf("秒传链接格式错误: %s", rapidUploadShareLink)
}
// 格式:aliyunpan://文件名|sha1|文件大小|<相对路径>
rapidUploadShareLinkStr := strings.Replace(rapidUploadShareLink, "aliyunpan://", "", 1)
item := &RapidUploadItem{}
parts := strings.Split(rapidUploadShareLinkStr, "|")
if len(parts) < 4 {
return nil, fmt.Errorf("秒传链接格式错误: %s", rapidUploadShareLink)
}
// hash
if len(parts[1]) == 0 {
return nil, fmt.Errorf("文件sha1错误: %s", rapidUploadShareLink)
}
item.FileSha1 = strings.TrimSpace(parts[1])
// size
if size,e := strconv.ParseInt(parts[2], 10, 64); e == nil{
item.FileSize = size
} else {
return nil, fmt.Errorf("文件大小错误: %s", rapidUploadShareLink)
}
// path
relativePath, _ := url.QueryUnescape(parts[3])
item.FilePath = path.Join(relativePath, parts[0])
// result
return item, nil
}
func newRapidUploadItemFromFileEntity(fileEntity *aliyunpan.FileEntity) *RapidUploadItem {
if fileEntity == nil {
return nil
}
return &RapidUploadItem{
FileSha1: fileEntity.ContentHash,
FileSize: fileEntity.FileSize,
FilePath: fileEntity.Path,
}
}
// 创建秒传链接
// 链接格式说明:aliyunpan://文件名|sha1|文件大小|<相对路径>
// "相对路径" 可以为空,为空代表存储到网盘根目录
func (r *RapidUploadItem) createRapidUploadLink(hideRelativePath bool) string {
fullLink := &strings.Builder{}
p := r.FilePath
p = strings.ReplaceAll(p, "\\", "/")
fileName := path.Base(p)
dirPath := path.Dir(p)
// 去掉开头/
if strings.Index(dirPath, "/") == 0 {
dirPath = dirPath[1:]
}
// 相对路径编码
dirPath = url.QueryEscape(dirPath)
// 隐藏相对路径
if hideRelativePath {
dirPath = ""
}
// 拼接
fmt.Fprintf(fullLink, "aliyunpan://%s|%s|%d|%s",
fileName, strings.ToUpper(r.FileSha1), r.FileSize, dirPath)
return fullLink.String()
}
func CmdConfig() cli.Command {
return cli.Command{
Name: "config",
Usage: "显示和修改程序配置项",
Description: "显示和修改程序配置项",
Category: "配置",
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
fmt.Printf("----\n运行 %s config set 可进行设置配置\n\n当前配置:\n", cmder.App().Name)
fmt.Printf("----\n当前配置目录: %s\n运行 %s config set 可进行设置配置\n\n当前配置:\n", config.GetConfigDir(), cmder.App().Name)
config.Config.PrintTable()
return nil
},
@@ -200,9 +107,6 @@ func CmdConfig() cli.Command {
if c.IsSet("max_upload_parallel") {
config.Config.MaxUploadParallel = c.Int("max_upload_parallel")
}
if c.IsSet("max_download_load") {
config.Config.MaxDownloadLoad = c.Int("max_download_load")
}
if c.IsSet("max_download_rate") {
err := config.Config.SetMaxDownloadRateByStr(c.String("max_download_rate"))
if err != nil {
@@ -217,10 +121,6 @@ func CmdConfig() cli.Command {
return nil
}
}
if c.IsSet("transfer_url_type") {
config.Config.TransferUrlType = c.Int("transfer_url_type")
}
if c.IsSet("savedir") {
config.Config.SaveDir = c.String("savedir")
}
@@ -230,6 +130,15 @@ func CmdConfig() cli.Command {
if c.IsSet("local_addrs") {
config.Config.SetLocalAddrs(c.String("local_addrs"))
}
if c.IsSet("ip_type") {
config.Config.SetPreferIPType(c.String("ip_type"))
}
if c.IsSet("file_record_config") {
config.Config.SetFileRecorderConfig(c.String("file_record_config"))
}
if c.IsSet("device_id") {
config.Config.SetDeviceId(c.String("device_id"))
}
err := config.Config.Save()
if err != nil {
@@ -245,19 +154,15 @@ func CmdConfig() cli.Command {
Flags: []cli.Flag{
cli.StringFlag{
Name: "cache_size",
Usage: "下载缓存",
Usage: "下载缓存,单位:KB",
},
cli.IntFlag{
Name: "max_download_parallel",
Usage: "下载网络连接的最大并发量",
Usage: "下载文件最大并发量",
},
cli.IntFlag{
Name: "max_upload_parallel",
Usage: "上传网络连接的最大并发量",
},
cli.IntFlag{
Name: "max_download_load",
Usage: "同时进行下载文件的最大数量",
Usage: "上传文件最大并发量",
},
cli.StringFlag{
Name: "max_download_rate",
@@ -267,11 +172,6 @@ func CmdConfig() cli.Command {
Name: "max_upload_rate",
Usage: "限制最大上传速度, 0代表不限制",
},
cli.IntFlag{
Name: "transfer_url_type",
Usage: "上传下载URL类别,1-默认,2-阿里云ECS",
Value: 1,
},
cli.StringFlag{
Name: "savedir",
Usage: "下载文件的储存目录",
@@ -284,13 +184,24 @@ func CmdConfig() cli.Command {
Name: "local_addrs",
Usage: "设置本地网卡地址, 多个地址用逗号隔开",
},
cli.StringFlag{
Name: "ip_type",
Usage: "设置域名解析IP优先类型",
},
cli.StringFlag{
Name: "file_record_config",
Usage: "设置是否开启上传、下载、同步文件的结果记录功能",
},
cli.StringFlag{
Name: "device_id",
Usage: "设置客户端ID,24位的字符串",
},
},
},
},
}
}
func CmdTool() cli.Command {
return cli.Command{
Name: "tool",
+3 -11
View File
@@ -1,21 +1,13 @@
package command
import (
"fmt"
"testing"
)
func TestRapidUploadItem_createRapidUploadLink(t *testing.T) {
item := &RapidUploadItem{
FileSha1: "752FCCBFB2436A6FFCA3B287831D4FAA5654B07E",
FileSize: 7005440,
FilePath: "/dgsdg/rtt5/我的文件夹/file我的文件.dmg",
}
fmt.Println(item.createRapidUploadLink(false))
}
func TestRapidUploadItem_newRapidUploadItem(t *testing.T) {
link := "aliyunpan://file我的文件.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|dgsdg%2Frtt5%2F%E6%88%91%E7%9A%84%E6%96%87%E4%BB%B6%E5%A4%B9"
item,_ := newRapidUploadItem(link)
fmt.Println(item)
}
}
+123
View File
@@ -0,0 +1,123 @@
// Copyright (c) 2020 tickstep.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
"os"
"strconv"
)
func CmdCp() cli.Command {
return cli.Command{
Name: "cp",
Usage: "复制文件/目录",
UsageText: `
aliyunpan cp <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>`,
Description: `
同网盘内复制文件或者目录。支持通配符匹配复制文件,通配符当前只能匹配文件名,不能匹配文件路径。
示例:
将 /我的资源/1.mp4 复制到 根目录 /
aliyunpan cp /我的资源/1.mp4 /
将 /我的资源 目录下所有的.png文件 复制到 /我的图片 目录下面,使用通配符匹配
aliyunpan cp /我的资源/*.png /我的图片
`,
Category: "阿里云盘",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() <= 1 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
RunCopy(parseDriveId(c), c.Args()...)
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
},
}
}
// RunCopy 执行复制文件/目录
func RunCopy(driveId string, paths ...string) {
activeUser := GetActiveUser()
cacheCleanPaths := []string{}
opFileList, targetFile, _, err := getFileInfo(driveId, paths...)
if err != nil {
fmt.Println(err)
return
}
if targetFile == nil {
fmt.Println("目标文件不存在")
return
}
if opFileList == nil || len(opFileList) == 0 {
fmt.Println("没有有效的文件可复制")
return
}
cacheCleanPaths = append(cacheCleanPaths, targetFile.Path)
failedCopyFiles := []*aliyunpan.FileEntity{}
successCopyFiles := []*aliyunpan.FileEntity{}
for _, mfi := range opFileList {
_, er := activeUser.PanClient().OpenapiPanClient().FileCopy(&aliyunpan.FileCopyParam{
DriveId: driveId,
FileId: mfi.FileId,
ToParentFileId: targetFile.FileId,
})
if er == nil {
successCopyFiles = append(successCopyFiles, mfi)
} else {
failedCopyFiles = append(failedCopyFiles, mfi)
}
}
if len(failedCopyFiles) > 0 {
fmt.Println("以下文件复制失败:")
for _, f := range failedCopyFiles {
fmt.Println(f.Path)
}
fmt.Println("")
}
if len(successCopyFiles) > 0 {
pnt := func() {
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "文件/目录"})
for k, rs := range successCopyFiles {
tb.Append([]string{strconv.Itoa(k + 1), rs.Path})
}
tb.Render()
}
fmt.Println("操作成功, 以下文件已复制到目标目录: ", targetFile.Path)
pnt()
activeUser.DeleteCache(cacheCleanPaths)
} else {
fmt.Println("无法复制文件,请稍后重试")
}
}
+273 -98
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,36 +15,48 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"os"
"path"
"path/filepath"
"runtime"
"sort"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/file/downloader"
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
"github.com/tickstep/aliyunpan/internal/global"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/taskframework"
"github.com/tickstep/aliyunpan/internal/ui"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester/rio/speeds"
"github.com/urfave/cli"
"os"
"path/filepath"
"runtime"
)
type (
//DownloadOptions 下载可选参数
DownloadOptions struct {
// DownloadActionId 下载动作ID,唯一标识一次下载动作的ID。这个ID每次下载任务启动的时候会自动生成,同一次下载任务下载的文件,这个ID都是同一个值。
DownloadActionId string
IsPrintStatus bool
IsExecutedPermission bool
IsOverwrite bool
SaveTo string
Parallel int
Parallel int // 文件下载最大线程数,即同时进行下载文件的数量,阿里云盘限制最大为3
SliceParallel int // 单个文件分片下载最大线程数,阿里云盘限制最大为3
Load int
MaxRetry int
NoCheck bool
ShowProgress bool
DriveId string
UseInternalUrl bool // 是否使用内置链接
DriveId string
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
IsMultiUserDownload bool // 是否启用多用户联合下载
IsUseUIDashboard bool // 是否使用UI下载面板显示下载进度
}
// LocateDownloadOption 获取下载链接可选参数
@@ -68,13 +80,10 @@ func CmdDownload() cli.Command {
Usage: "下载文件/目录",
UsageText: cmder.App().Name + " download <文件/目录路径1> <文件/目录2> <文件/目录3> ...",
Description: `
下载的文件默认保存到, 程序所在目录的 download/ 目录.
通过 aliyunpan config set -savedir <savedir>, 自定义保存的目录.
支持多个文件或目录下载.
自动跳过下载重名的文件!
下载的文件默认保存到, 程序所在目录的 download/ 目录。支持软链接文件,包括Linux/macOS(ln命令)和Windows(mklink命令)创建的符号链接文件。
通过 aliyunpan config set -savedir <savedir>, 自定义保存的目录。支持多个文件或目录下载,支持自动跳过下载重名的文件!
示例:
设置保存目录, 保存到 D:\Downloads
注意区别反斜杠 "\" 和 斜杠 "/" !!!
aliyunpan config set -savedir D:\\Downloads
@@ -82,16 +91,33 @@ func CmdDownload() cli.Command {
aliyunpan config set -savedir D:/Downloads
下载 /我的资源/1.mp4
aliyunpan d /我的资源/1.mp4
aliyunpan download /我的资源/1.mp4
下载 /我的资源 目录下面所有的mp4文件,使用通配符
aliyunpan download /我的资源/*.mp4
下载 /我的资源 整个目录!!
aliyunpan d /我的资源
aliyunpan download /我的资源
下载 /我的资源/1.mp4 并保存下载的文件到本地的 d:/panfile
aliyunpan d --saveto d:/panfile /我的资源/1.mp4
下载 /我的资源 整个目录,但是排除所有的jpg文件
aliyunpan download -exn "\.jpg$" /我的资源
下载 /我的资源/1.mp4 并保存下载的文件到本地的 d:/panfile
aliyunpan download --saveto d:/panfile /我的资源/1.mp4
使用多用户联合下载 /我的资源/1.mp4 文件。必须保证所有登录的用户在相同的网盘(备份盘/资源盘)下,相同的路径下,有相同的文件
aliyunpan download /我的资源/1.mp4 -md
参考:
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式。在正则表达式中,^表示匹配开头,$表示匹配结尾。
1)排除@eadir文件或者文件夹:-exn "^@eadir$"
2)排除.jpg文件:-exn "\.jpg$"
3)排除.号开头的文件:-exn "^\."
4)排除~号开头的文件:-exn "^~"
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -103,22 +129,29 @@ func CmdDownload() cli.Command {
saveTo string
)
if c.Bool("save") {
saveTo = "."
// 使用当前工作目录
pwd, _ := os.Getwd()
saveTo = path.Clean(pwd)
} else if c.String("saveto") != "" {
saveTo = filepath.Clean(c.String("saveto"))
}
do := &DownloadOptions{
DownloadActionId: utils.UuidStr(),
IsPrintStatus: c.Bool("status"),
IsExecutedPermission: c.Bool("x"),
IsOverwrite: c.Bool("ow"),
SaveTo: saveTo,
Parallel: c.Int("p"),
Load: c.Int("l"),
SliceParallel: c.Int("sp"),
Load: 0,
MaxRetry: c.Int("retry"),
NoCheck: c.Bool("nocheck"),
ShowProgress: !c.Bool("np"),
DriveId: parseDriveId(c),
DriveId: parseDriveId(c),
ExcludeNames: c.StringSlice("exn"),
IsMultiUserDownload: c.Bool("md"),
IsUseUIDashboard: c.Bool("ui"),
}
RunDownload(c.Args(), do)
@@ -147,11 +180,13 @@ func CmdDownload() cli.Command {
},
cli.IntFlag{
Name: "p",
Usage: "指定下载线程数",
Usage: "parallel,指定同时进行下载文件的数量(取值范围:1 ~ 3)",
Value: 1,
},
cli.IntFlag{
Name: "l",
Usage: "指定同时进行下载文件的数量",
Name: "sp",
Usage: "slice parallel,指定单个文件下载的最大线程(分片)数(取值范围:1 ~ 3)",
Value: 0,
},
cli.IntFlag{
Name: "retry",
@@ -171,27 +206,49 @@ func CmdDownload() cli.Command {
Usage: "网盘ID",
Value: "",
},
cli.StringSliceFlag{
Name: "exn",
Usage: "exclude name,指定排除的文件夹或者文件的名称,被排除的文件不会进行下载,只支持正则表达式。支持同时排除多个名称,每一个名称就是一个exn参数",
Value: nil,
},
cli.BoolFlag{
Name: "md",
Usage: "(BETA) Multi-User Download,使用多用户联合下载,可以对单一文件叠加所有登录用户的下载速度",
},
cli.BoolFlag{
Name: "ui",
Usage: "(BETA) 使用UI面板显示下载详情和进度,更加直观和友好",
},
},
}
}
func downloadPrintFormat(load int) string {
if load <= 1 {
return pandownload.DefaultPrintFormat
}
return "\r[%s] ↓ %s/%s %s/s in %s, left %s ..."
}
// RunDownload 执行下载网盘内文件
func RunDownload(paths []string, options *DownloadOptions) {
activeUser := GetActiveUser()
activeUser.PanClient().OpenapiPanClient().EnableCache()
activeUser.PanClient().OpenapiPanClient().ClearCache()
defer activeUser.PanClient().OpenapiPanClient().DisableCache()
//// pan token expired checker
//continueFlag := int32(0)
//atomic.StoreInt32(&continueFlag, 0)
//defer func() {
// atomic.StoreInt32(&continueFlag, 1)
//}()
//go func(flag *int32) {
// for atomic.LoadInt32(flag) == 0 {
// time.Sleep(time.Duration(1) * time.Minute)
// if RefreshWebTokenInNeed(activeUser, config.Config.DeviceName) {
// logger.Verboseln("update access token for download task")
// }
// }
//}(&continueFlag)
if options == nil {
options = &DownloadOptions{}
}
if options.Load <= 0 {
options.Load = config.Config.MaxDownloadLoad
}
if options.MaxRetry < 0 {
options.MaxRetry = pandownload.DefaultDownloadMaxRetry
}
@@ -208,8 +265,8 @@ func RunDownload(paths []string, options *DownloadOptions) {
BlockSize: MaxDownloadRangeSize,
MaxRate: config.Config.MaxDownloadRate,
InstanceStateStorageFormat: downloader.InstanceStateStorageFormatJSON,
ShowProgress: options.ShowProgress,
UseInternalUrl: config.Config.TransferUrlType == 2,
ShowProgress: options.ShowProgress,
ExcludeNames: options.ExcludeNames,
}
if cfg.CacheSize == 0 {
cfg.CacheSize = int(DownloadCacheSize)
@@ -218,99 +275,217 @@ func RunDownload(paths []string, options *DownloadOptions) {
// 设置下载最大并发量
if options.Parallel < 1 {
options.Parallel = config.Config.MaxDownloadParallel
if options.Parallel == 0 {
options.Parallel = config.DefaultFileDownloadParallelNum
}
}
if options.Parallel > config.MaxFileDownloadParallelNum {
options.Parallel = config.MaxFileDownloadParallelNum
}
paths, err := matchPathByShellPattern(options.DriveId, paths...)
// 设置单个文件下载分片线程数
if options.SliceParallel < 1 { // 用户没有主动设置,则使用下面自动配置的策略
if options.Parallel > 1 {
// 如果是多文件下载,则一个文件只能同时使用1个线程
options.SliceParallel = 1
} else {
// 如果是单文件下载,则可以使用3个最大线程
options.SliceParallel = 3
}
}
// 保存文件的本地根文件夹
originSaveRootPath := ""
if options.SaveTo != "" {
originSaveRootPath = options.SaveTo
} else {
// 使用默认的保存路径
originSaveRootPath = GetActiveUser().GetSavePath("")
}
fi, err1 := os.Stat(originSaveRootPath)
if err1 != nil && !os.IsExist(err1) {
os.MkdirAll(originSaveRootPath, 0777) // 首先在本地创建目录
} else {
if !fi.IsDir() {
fmt.Println("本地保存路径不是文件夹,请删除或者创建对应的文件夹:", originSaveRootPath)
return
}
}
paths, err := makePathAbsolute(options.DriveId, paths...)
if err != nil {
fmt.Println(err)
return
}
fmt.Print("\n")
fmt.Printf("[0] 提示: 当前下载最大并发量为: %d, 下载缓存为: %d\n", options.Parallel, cfg.CacheSize)
var (
panClient = GetActivePanClient()
loadCount = 0
)
// 预测要下载的文件数量
for k := range paths {
// 使用递归获取文件的方法计算路径包含的文件的总数量
panClient.FilesDirectoriesRecurseList(options.DriveId, paths[k], func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
if apiError != nil {
panCommandVerbose.Warnf("%s\n", apiError)
return true
// 多用户下载的辅助账号列表
var subPanClientList []*config.PanClient
if options.IsMultiUserDownload { // 多用户下载
c := config.Config
for _, u := range config.Config.UserList {
if u.UserId == activeUser.UserId {
// 当前登录用户,作为主用户,跳过
continue
}
// 忽略统计文件夹数量
if !fd.IsFolder() {
loadCount++
if loadCount >= options.Load { // 文件的总数量超过指定的指定数量,则不再进行下层的递归查找文件
return false
}
// 初始化客户端
user, err := config.SetupUserByCookie(u.OpenapiToken, u.WebapiToken,
u.TicketId, u.UserId,
c.DeviceId, c.DeviceName,
c.ClientId, c.ClientSecret)
if err != nil {
logger.Verboseln("setup user error")
continue
}
return true
})
if subPanClientList == nil {
subPanClientList = []*config.PanClient{}
}
subPanClientList = append(subPanClientList, user.PanClient())
}
if loadCount >= options.Load {
break
if subPanClientList == nil || len(subPanClientList) == 0 {
fmt.Printf("\n当前登录用户只有一个,无法启用多用户联合下载\n")
subPanClientList = nil
}
}
// 修改Load, 设置MaxParallel
if loadCount > 0 {
options.Load = loadCount
// 取平均值
cfg.MaxParallel = config.AverageParallel(options.Parallel, loadCount)
} else {
cfg.MaxParallel = options.Parallel
if subPanClientList != nil || len(subPanClientList) > 0 {
// 已启用多用户下载
userCount := len(subPanClientList) + 1
fmt.Printf("\n*** 已启用多用户联合下载,用户数: %d ***\n", userCount)
// 多用户下载,并发数必须为1,以获得最大下载速度
options.Parallel = 1
// 阿里OpenAPI规定:文件分片下载的并发数为3,即某用户使用 App 时,可以同时下载 1 个文件的 3 个分片,或者同时下载 3 个文件的各 1 个分片。
options.SliceParallel = userCount * 3
}
var (
panClient = activeUser.PanClient()
)
cfg.MaxParallel = options.Parallel
cfg.SliceParallel = options.SliceParallel
var (
executor = taskframework.TaskExecutor{
IsFailedDeque: true, // 统计失败的列表
}
statistic = &pandownload.DownloadStatistic{}
)
// 配置执行器任务并发数,即同时下载文件并发数
executor.SetParallel(cfg.MaxParallel)
// 全局速度统计
globalSpeedsStat := &speeds.Speeds{}
// 下载统计UI面板
var dashboard *ui.DashboardPanel = nil
if options.IsUseUIDashboard && options.ShowProgress && !options.IsPrintStatus && ui.IsTerminal(os.Stdout) {
dashboard = ui.NewDashboardPanel(ui.DashboardPanelDownload, cfg.MaxParallel, globalSpeedsStat, &ui.DashboardOptions{
Title: "下载统计UI面板",
ActiveSlots: 3, // 下载文件进度展示,最大同时展示3个
MaxHistory: 50, // 下载日志显示,最多同时显示50条,这个会按照窗口大小进行自适应显示
})
}
logf := func(format string, a ...interface{}) {
if dashboard != nil {
dashboard.Logf(format, a...)
return
}
fmt.Printf(format, a...)
}
// 下载记录器
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/download_file_records.csv")
// 输出当前下载配置信息
logf("\n[0] 当前文件下载最大并发量为: %d, 单文件下载分片线程数为: %d, 下载缓存为: %s\n", options.Parallel, options.SliceParallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
// 阿里云盘下载并发超额警告,阿里云盘下载最大并发数为3
if options.Parallel*options.SliceParallel > 3 {
logf("[0] 警告:当前下载并发配置数已超过阿里云盘限制的最大并发数,下载任务会被风控容易导致下载失败\n")
}
// 处理队列
for k := range paths {
newCfg := *cfg
unit := pandownload.DownloadTaskUnit{
Cfg: &newCfg, // 复制一份新的cfg
PanClient: panClient,
VerbosePrinter: panCommandVerbose,
PrintFormat: downloadPrintFormat(options.Load),
ParentTaskExecutor: &executor,
DownloadStatistic: statistic,
IsPrintStatus: options.IsPrintStatus,
IsExecutedPermission: options.IsExecutedPermission,
IsOverwrite: options.IsOverwrite,
NoCheck: options.NoCheck,
FilePanPath: paths[k],
DriveId: options.DriveId,
// 使用通配符匹配
fileList, err2 := matchPathByShellPattern(options.DriveId, paths[k])
if err2 != nil {
logf("获取文件出错,请稍后重试: %s\n", paths[k])
continue
}
if fileList == nil || len(fileList) == 0 {
// 文件不存在
logf("文件不存在: %s\n", paths[k])
continue
}
// 排序,按名称排序,从小到大
sort.Slice(fileList, func(i, j int) bool {
return fileList[i].FileName < fileList[j].FileName
})
// 逐一下载
for _, f := range fileList {
newCfg := *cfg
// 设置储存的路径
if options.SaveTo != "" {
unit.OriginSaveRootPath = options.SaveTo
unit.SavePath = filepath.Join(options.SaveTo, filepath.Base(paths[k]))
} else {
// 使用默认的保存路径
unit.OriginSaveRootPath = GetActiveUser().GetSavePath("")
unit.SavePath = GetActiveUser().GetSavePath(paths[k])
// 是否排除下载
if utils.IsExcludeFile(f.Path, &newCfg.ExcludeNames) {
logf("排除文件: %s\n", f.Path)
continue
}
// 匹配的文件
unit := pandownload.DownloadTaskUnit{
DownloadActionId: options.DownloadActionId,
Cfg: &newCfg, // 复制一份新的cfg
PanClient: panClient,
SubPanClientList: subPanClientList,
VerbosePrinter: panCommandVerbose,
ParentTaskExecutor: &executor,
DownloadStatistic: statistic,
IsPrintStatus: options.IsPrintStatus,
IsExecutedPermission: options.IsExecutedPermission,
IsOverwrite: options.IsOverwrite,
NoCheck: options.NoCheck,
FilePanSource: global.FileSource,
FilePanPath: f.Path,
DriveId: options.DriveId,
GlobalSpeedsStat: globalSpeedsStat,
FileRecorder: fileRecorder,
UI: dashboard,
}
// 设置储存的路径
if options.SaveTo != "" {
unit.OriginSaveRootPath = options.SaveTo
unit.SavePath = filepath.Join(options.SaveTo, f.Path)
} else {
// 使用默认的保存路径
unit.OriginSaveRootPath = GetActiveUser().GetSavePath("")
unit.SavePath = GetActiveUser().GetSavePath(f.Path)
}
info := executor.Append(&unit, options.MaxRetry)
if dashboard != nil {
dashboard.RegisterTask(info.Id(), f.Path, f.FileSize, f.IsFile())
}
logf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
}
info := executor.Append(&unit, options.MaxRetry)
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), paths[k])
}
// 开始计时
statistic.StartTimer()
// 启动UI面板显示
if dashboard != nil {
dashboard.Start()
}
// 开始执行
executor.Execute()
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", statistic.Elapsed()/1e6*1e6, converter.ConvertFileSize(statistic.TotalSize()))
// 关闭UI面板
if dashboard != nil {
dashboard.Close()
}
// 完成下载,输出统计结果
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
// 输出失败的文件列表
failedList := executor.FailedDeque()
+19 -12
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,7 +17,6 @@ import (
"fmt"
"github.com/olekukonko/tablewriter"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
@@ -28,9 +27,9 @@ import (
func CmdDrive() cli.Command {
return cli.Command{
Name: "drive",
Usage: "切换网盘(文件网盘/相册网盘)",
Usage: "切换网盘(备份盘/资源库)",
Description: `
切换已登录的阿里云盘的云工作网盘(文件网盘/相册网盘)
切换已登录账号的阿里云盘的工作网盘(备份盘/资源库)
如果运行该条命令没有提供参数, 程序将会列出所有的网盘列表, 供选择切换.
示例:
@@ -38,8 +37,8 @@ func CmdDrive() cli.Command {
aliyunpan drive <driveId>
`,
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
inputData := c.Args().Get(0)
targetDriveId := strings.TrimSpace(inputData)
@@ -49,10 +48,10 @@ func CmdDrive() cli.Command {
}
}
func RunSwitchDriveList(targetDriveId string) {
func RunSwitchDriveList(targetDriveId string) {
currentDriveId := config.Config.ActiveUser().ActiveDriveId
var activeDriveInfo *config.DriveInfo = nil
driveList,renderStr := getDriveOptionList()
driveList, renderStr := getDriveOptionList()
if driveList == nil || len(driveList) == 0 {
fmt.Println("切换网盘失败")
@@ -71,15 +70,15 @@ func RunSwitchDriveList(targetDriveId string) {
return
}
if n, err := strconv.Atoi(index); err == nil && n >= 0 && n < len(driveList) {
activeDriveInfo = driveList[n]
if n, err1 := strconv.Atoi(index); err1 == nil && (n-1) >= 0 && (n-1) < len(driveList) {
activeDriveInfo = driveList[n-1]
} else {
fmt.Printf("切换网盘失败, 请检查 # 值是否正确\n")
return
}
} else {
// 直接切换
for _,driveInfo := range driveList {
for _, driveInfo := range driveList {
if driveInfo.DriveId == targetDriveId {
activeDriveInfo = driveInfo
break
@@ -101,6 +100,11 @@ func RunSwitchDriveList(targetDriveId string) {
config.Config.ActiveUser().Workdir = "/"
config.Config.ActiveUser().WorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
}
} else if activeUser.IsResourceDriveActive() {
if activeUser.ResourceWorkdir == "" {
config.Config.ActiveUser().ResourceWorkdir = "/"
config.Config.ActiveUser().ResourceWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
}
} else if activeUser.IsAlbumDriveActive() {
if activeUser.AlbumWorkdir == "" {
config.Config.ActiveUser().AlbumWorkdir = "/"
@@ -121,7 +125,10 @@ func getDriveOptionList() (config.DriveInfoList, string) {
tb.SetHeader([]string{"#", "drive_id", "网盘名称"})
for k, info := range driveList {
tb.Append([]string{strconv.Itoa(k), info.DriveId, info.DriveName})
if info.IsAlbumDrive() {
continue
}
tb.Append([]string{strconv.Itoa(k + 1), info.DriveId, info.DriveName})
}
tb.Render()
return driveList, builder.String()
-152
View File
@@ -1,152 +0,0 @@
// Copyright (c) 2020 tickstep.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"log"
"os"
"path"
"strconv"
"time"
)
func CmdExport() cli.Command {
return cli.Command{
Name: "export",
Usage: "导出文件/目录元数据",
UsageText: cmder.App().Name + " export <网盘文件/目录的路径1> <文件/目录2> <文件/目录3> ... <本地保存文件路径>",
Description: `
导出指定文件/目录下面的所有文件的元数据信息,并保存到指定的本地文件里面。导出的文件元信息可以使用 import 命令(秒传文件功能)导入到网盘中。
支持多个文件或目录的导出.
示例:
导出 /我的资源/1.mp4 元数据到文件 /Users/tickstep/Downloads/export_files.txt
aliyunpan export /我的资源/1.mp4 /Users/tickstep/Downloads/export_files.txt
导出 /我的资源 整个目录 元数据到文件 /Users/tickstep/Downloads/export_files.txt
aliyunpan export /我的资源 /Users/tickstep/Downloads/export_files.txt
导出 网盘 整个目录 元数据到文件 /Users/tickstep/Downloads/export_files.txt
aliyunpan export / /Users/tickstep/Downloads/export_files.txt
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() < 2 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
subArgs := c.Args()
RunExportFiles(parseDriveId(c), c.Bool("ow"), subArgs[:len(subArgs)-1], subArgs[len(subArgs)-1])
return nil
},
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ow",
Usage: "overwrite, 覆盖已存在的导出文件",
},
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
},
}
}
func RunExportFiles(driveId string, overwrite bool, panPaths []string, saveLocalFilePath string) {
activeUser := config.Config.ActiveUser()
panClient := activeUser.PanClient()
lfi,_ := os.Stat(saveLocalFilePath)
realSaveFilePath := saveLocalFilePath
if lfi != nil {
if lfi.IsDir() {
realSaveFilePath = path.Join(saveLocalFilePath, "export_file_") + strconv.FormatInt(time.Now().Unix(), 10) + ".txt"
} else {
if !overwrite {
fmt.Println("导出文件已存在")
return
}
}
} else {
// create file
localDir := path.Dir(saveLocalFilePath)
dirFs,_ := os.Stat(localDir)
if dirFs != nil {
if !dirFs.IsDir() {
fmt.Println("指定的保存文件路径不合法")
return
}
} else {
er := os.MkdirAll(localDir, 0755)
if er != nil {
fmt.Println("创建本地文件夹出错")
return
}
}
realSaveFilePath = saveLocalFilePath
}
totalCount := 0
saveFile, err := os.OpenFile(realSaveFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
log.Fatal(err)
return
}
for _,panPath := range panPaths {
panPath = activeUser.PathJoin(driveId, panPath)
panClient.FilesDirectoriesRecurseList(driveId, panPath, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
if apiError != nil {
logger.Verbosef("%s\n", apiError)
return true
}
// 只需要存储文件即可
if !fd.IsFolder() {
item := newRapidUploadItemFromFileEntity(fd)
jstr := item.createRapidUploadLink(false)
if len(jstr) <= 0 {
logger.Verboseln("create rapid upload link err")
return false
}
saveFile.WriteString(jstr + "\n")
totalCount += 1
time.Sleep(time.Duration(100) * time.Millisecond)
fmt.Printf("\r导出文件数量: %d", totalCount)
}
return true
})
}
// close and save
if err := saveFile.Close(); err != nil {
log.Fatal(err)
}
fmt.Printf("\r导出文件总数量: %d\n", totalCount)
fmt.Printf("导出文件保存路径: %s\n", realSaveFilePath)
}
-277
View File
@@ -1,277 +0,0 @@
// Copyright (c) 2020 tickstep.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"strings"
"time"
)
type (
dirFileListData struct {
Dir *aliyunpan.MkdirResult
FileList aliyunpan.FileList
}
)
const (
DefaultSaveToPanPath = "/aliyunpan"
)
func CmdImport() cli.Command {
return cli.Command{
Name: "import",
Usage: "导入文件",
UsageText: cmder.App().Name + " export <本地元数据文件路径>",
Description: `
导入文件中记录的元数据文件到网盘。保存到网盘的文件会使用文件元数据记录的路径位置,如果没有指定云盘目录(saveto)则默认导入到目录 aliyunpan 中。
导入的文件可以使用 export 命令获得。
导入文件每一行是一个文件元数据,样例如下:
aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder
示例:
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项
aliyunpan import /Users/tickstep/Downloads/export_files.txt
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项并保存到目录 /my2021 中
aliyunpan import -saveto=/my2021 /Users/tickstep/Downloads/export_files.txt
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项并保存到网盘根目录 / 中
aliyunpan import -saveto=/ /Users/tickstep/Downloads/export_files.txt
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
saveTo := ""
if c.String("saveto") != "" {
saveTo = filepath.Clean(c.String("saveto"))
}
subArgs := c.Args()
RunImportFiles(parseDriveId(c), c.Bool("ow"), saveTo, subArgs[0])
return nil
},
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ow",
Usage: "overwrite, 覆盖已存在的网盘文件",
},
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
cli.StringFlag{
Name: "saveto",
Usage: "将文件保存到指定的目录",
},
},
}
}
func RunImportFiles(driveId string, overwrite bool, panSavePath, localFilePath string) {
lfi,_ := os.Stat(localFilePath)
if lfi != nil {
if lfi.IsDir() {
fmt.Println("请指定导入文件")
return
}
} else {
// create file
fmt.Println("导入文件不存在")
return
}
if panSavePath == "" {
// use default
panSavePath = DefaultSaveToPanPath
}
fmt.Println("导入的文件会存储到目录:" + panSavePath)
importFile, err := os.OpenFile(localFilePath, os.O_RDONLY, 0755)
if err != nil {
log.Fatal(err)
return
}
defer importFile.Close()
fileData,err := ioutil.ReadAll(importFile)
if err != nil {
fmt.Println("读取文件出错")
return
}
fileText := string(fileData)
if len(fileText) == 0 {
fmt.Println("文件为空")
return
}
fileText = strings.TrimSpace(fileText)
fileLines := strings.Split(fileText, "\n")
importFileItems := []RapidUploadItem{}
for _,line := range fileLines {
line = strings.TrimSpace(line)
if item,e := newRapidUploadItem(line); e != nil {
fmt.Println(e)
continue
} else {
item.FilePath = strings.ReplaceAll(path.Join(panSavePath, item.FilePath), "\\", "/")
importFileItems = append(importFileItems, *item)
}
}
if len(importFileItems) == 0 {
fmt.Println("没有可以导入的文件项目")
return
}
fmt.Println("正在准备导入...")
dirMap := prepareMkdir(driveId, importFileItems)
fmt.Println("正在导入...")
successImportFiles := []RapidUploadItem{}
failedImportFiles := []RapidUploadItem{}
for _,item := range importFileItems {
fmt.Printf("正在处理导入: %s\n", item.FilePath)
result, abort := processOneImport(driveId, overwrite, dirMap, item)
if abort {
fmt.Println("导入任务终止了")
break
}
if result {
successImportFiles = append(successImportFiles, item)
} else {
failedImportFiles = append(failedImportFiles, item)
}
time.Sleep(time.Duration(200) * time.Millisecond)
}
if len(failedImportFiles) > 0 {
fmt.Println("\n以下文件导入失败")
for _,f := range failedImportFiles {
fmt.Printf("%s %s\n", f.FileSha1, f.FilePath)
}
fmt.Println("")
}
fmt.Printf("导入结果, 成功 %d, 失败 %d\n", len(successImportFiles), len(failedImportFiles))
}
func processOneImport(driveId string, isOverwrite bool, dirMap map[string]*dirFileListData, item RapidUploadItem) (result, abort bool) {
panClient := config.Config.ActiveUser().PanClient()
panDir,fileName := path.Split(item.FilePath)
dataItem := dirMap[path.Dir(panDir)]
if isOverwrite {
// 标记覆盖旧同名文件
// 检查同名文件是否存在
var efi *aliyunpan.FileEntity = nil
for _,fileItem := range dataItem.FileList {
if !fileItem.IsFolder() && fileItem.FileName == fileName {
efi = fileItem
break
}
}
if efi != nil && efi.FileId != "" {
// existed, delete it
fdr, err := panClient.FileDelete([]*aliyunpan.FileBatchActionParam{
{
DriveId:driveId,
FileId:efi.FileId,
},
})
if err != nil || fdr == nil || !fdr[0].Success {
fmt.Println("无法删除文件,请稍后重试")
return false, false
}
time.Sleep(time.Duration(500) * time.Millisecond)
fmt.Println("检测到同名文件,已移动到回收站")
}
}
appCreateUploadFileParam := &aliyunpan.CreateFileUploadParam{
DriveId: driveId,
Name: fileName,
Size: item.FileSize,
ContentHash: item.FileSha1,
ParentFileId: dataItem.Dir.FileId,
}
uploadOpEntity, apierr := panClient.CreateUploadFile(appCreateUploadFileParam)
if apierr != nil {
fmt.Println("创建秒传任务失败:" + apierr.Error())
return false, true
}
if uploadOpEntity.RapidUpload {
logger.Verboseln("秒传成功, 保存到网盘路径: ", path.Join(panDir, uploadOpEntity.FileName))
} else {
fmt.Println("失败,文件未曾上传,无法秒传")
return false, false
}
return true, false
}
func prepareMkdir(driveId string, importFileItems []RapidUploadItem) map[string]*dirFileListData {
panClient := config.Config.ActiveUser().PanClient()
resultMap := map[string]*dirFileListData{}
for _,item := range importFileItems {
var apierr *apierror.ApiError
var rs *aliyunpan.MkdirResult
panDir := path.Dir(item.FilePath)
if resultMap[panDir] != nil {
continue
}
if panDir != "/" {
rs, apierr = panClient.MkdirRecursive(driveId, "", "", 0, strings.Split(path.Clean(panDir), "/"))
if apierr != nil || rs.FileId == "" {
logger.Verboseln("创建云盘文件夹失败")
continue
}
} else {
rs = &aliyunpan.MkdirResult{}
rs.FileId = aliyunpan.DefaultRootParentFileId
}
dataItem := &dirFileListData{}
dataItem.Dir = rs
// files
param := &aliyunpan.FileListParam{}
param.DriveId = driveId
param.ParentFileId = rs.FileId
allFileInfo, err1 := panClient.FileListGetAll(param)
if err1 != nil {
logger.Verboseln("获取文件信息出错")
continue
}
dataItem.FileList = allFileInfo
resultMap[panDir] = dataItem
time.Sleep(time.Duration(500) * time.Millisecond)
}
return resultMap
}
+72 -26
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,57 +15,54 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdliner"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
"github.com/tickstep/aliyunpan/internal/global"
_ "github.com/tickstep/library-go/requester"
"github.com/urfave/cli"
"strings"
)
func CmdLogin() cli.Command {
return cli.Command{
Name: "login",
Usage: "登录阿里云盘账号",
Description: `
示例:
1.常规登录,按提示一步一步来即可
aliyunpan login
aliyunpan login -RefreshToken=8B12CBBCE89CA8DFC3445985B63B511B5E7EC7...
常规登录:
按提示一步一步来即可.
`,
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc, // 每次进行登录动作的时候需要调用刷新配置
After: cmder.SaveConfigFunc, // 登录完成需要调用保存配置
Before: ReloadConfigFunc, // 每次进行登录动作的时候需要调用刷新配置
After: SaveConfigFunc, // 登录完成需要调用保存配置
Action: func(c *cli.Context) error {
webToken := aliyunpan.WebLoginToken{}
refreshToken := ""
ticketId := ""
openToken := &config.PanClientToken{}
webToken := &config.PanClientToken{}
var err error
refreshToken, webToken, err = RunLogin(c.String("RefreshToken"))
ticketId, openToken, webToken, err = RunLogin()
if err != nil {
fmt.Println(err)
return err
}
cloudUser, err := config.SetupUserByCookie(&webToken)
cloudUser, err := config.SetupUserByCookie(openToken, webToken,
ticketId, "",
config.Config.DeviceId, config.Config.DeviceName,
config.Config.ClientId, config.Config.ClientSecret)
if cloudUser == nil {
fmt.Println("登录失败: ", err)
return nil
}
cloudUser.RefreshToken = refreshToken
cloudUser.TicketId = ticketId
config.Config.SetActiveUser(cloudUser)
fmt.Println("阿里云盘登录成功: ", cloudUser.Nickname)
return nil
},
// 命令的附加options参数说明,使用 help login 命令即可查看
Flags: []cli.Flag{
// aliyunpan login -RefreshToken=8B12CBBCE89CA8DFC3445985B63B511B5E7EC7...
cli.StringFlag{
Name: "RefreshToken",
Usage: "使用 RefreshToken Cookie来登录帐号",
},
},
// 命令的附加options参数说明,使用 help panlogin 命令即可查看
Flags: []cli.Flag{},
}
}
@@ -75,8 +72,8 @@ func CmdLogout() cli.Command {
Usage: "退出阿里帐号",
Description: "退出当前登录的帐号",
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.NumLogins() == 0 {
fmt.Println("未设置任何帐号, 不能退出")
@@ -87,6 +84,9 @@ func CmdLogout() cli.Command {
confirm string
activeUser = config.Config.ActiveUser()
)
if activeUser == nil {
return nil
}
if !c.Bool("y") {
fmt.Printf("确认退出当前帐号: %s ? (y/n) > ", activeUser.Nickname)
@@ -96,6 +96,12 @@ func CmdLogout() cli.Command {
}
}
// 云端注销登录
//if _, e := activeUser.PanClient().WebapiPanClient().DeviceLogout(); e != nil {
// fmt.Printf("登出设备失败,请手动在网页端进行登出: %s\n", e.String())
//}
// 删除用户信息
deletedUser, err := config.Config.DeleteUser(activeUser.UserId)
if err != nil {
fmt.Printf("退出用户 %s, 失败, 错误: %s\n", activeUser.Nickname, err)
@@ -113,6 +119,46 @@ func CmdLogout() cli.Command {
}
}
func RunLogin(refreshToken string) (refreshTokenStr string, webToken aliyunpan.WebLoginToken, error error) {
return cmder.DoLoginHelper(refreshToken)
func RunLogin() (ticketId string, openapiToken, webapiToken *config.PanClientToken, error error) {
h := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
// web login request
qrCodeUrlResult, err := h.GetQRCodeLoginUrl("")
if err != nil {
fmt.Println("登录出错:", err)
return "", nil, nil, err
}
ticketId = qrCodeUrlResult.TokenId
loginUrl := &strings.Builder{}
if global.IsSupportNoneOpenApiCommands {
// 兼容以前的版本
fmt.Fprintf(loginUrl, "https://openapi.alipan.com/oauth/authorize?client_id=%s&redirect_uri=https%%3A%%2F%%2Fapi.tickstep.com%%2Fauth%%2Ftickstep%%2Faliyunpan%%2Ftoken%%2Fopenapi%%2F%s%%2Fauth&scope=user:base,file:all:read,file:all:write,file:share:write,album:shared:read",
config.Config.ClientId, ticketId)
} else {
fmt.Fprintf(loginUrl, "https://openapi.alipan.com/oauth/authorize?client_id=%s&redirect_uri=https%%3A%%2F%%2Fapi.tickstep.com%%2Fauth%%2Ftickstep%%2Faliyunpan%%2Ftoken%%2Fopenapi%%2F%s%%2Fauth2&scope=user:base,file:all:read,file:all:write,file:share:write,album:shared:read",
config.Config.ClientId, ticketId)
}
fmt.Printf("请在浏览器打开以下链接进行登录,链接有效时间为5分钟。\n注意:你需要进行一次授权一次扫码的两次登录。\n%s\n\n", loginUrl)
// handler waiting
line := cmdliner.NewLiner()
defer line.Close()
line.State.Prompt("请在浏览器里面完成扫码登录,然后再按Enter键继续...")
// get login token
comToken, er := h.GetLoginToken(ticketId)
if er != nil {
return ticketId, nil, nil, fmt.Errorf("登录失败,请稍后尝试重新登录")
}
return ticketId,
&config.PanClientToken{
AccessToken: comToken.Openapi.AccessToken,
Expired: comToken.Openapi.Expired,
},
&config.PanClientToken{
AccessToken: comToken.Webapi.AccessToken,
Expired: comToken.Webapi.Expired,
},
nil
}
+99 -27
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,11 +17,11 @@ import (
"fmt"
"github.com/olekukonko/tablewriter"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/text"
"github.com/urfave/cli"
"os"
"strconv"
@@ -62,20 +62,49 @@ func CmdLs() cli.Command {
绝对路径
aliyunpan ls /我的资源
列出 我的资源 内的文件和目录,使用通配符
aliyunpan ls /我的*
详细列出 我的资源 内的文件和目录
aliyunpan ll /我的资源
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
var (
orderBy aliyunpan.FileOrderBy = aliyunpan.FileOrderByUpdatedAt
orderSort aliyunpan.FileOrderDirection = aliyunpan.FileOrderDirectionDesc
)
switch {
case c.IsSet("asc"):
orderSort = aliyunpan.FileOrderDirectionAsc
case c.IsSet("desc"):
orderSort = aliyunpan.FileOrderDirectionDesc
default:
orderSort = aliyunpan.FileOrderDirectionDesc
}
switch {
case c.IsSet("time"):
orderBy = aliyunpan.FileOrderByUpdatedAt
case c.IsSet("name"):
orderBy = aliyunpan.FileOrderByName
case c.IsSet("size"):
orderBy = aliyunpan.FileOrderBySize
default:
orderBy = aliyunpan.FileOrderByUpdatedAt
}
RunLs(parseDriveId(c), c.Args().Get(0), &LsOptions{
Total: c.Bool("l") || c.Parent().Args().Get(0) == "ll",
})
}, orderBy, orderSort)
return nil
},
@@ -85,20 +114,66 @@ func CmdLs() cli.Command {
Usage: "网盘ID",
Value: "",
},
cli.BoolFlag{
Name: "asc",
Usage: "升序排序",
},
cli.BoolFlag{
Name: "desc",
Usage: "降序排序",
},
cli.BoolFlag{
Name: "time",
Usage: "根据修改时间排序",
},
cli.BoolFlag{
Name: "name",
Usage: "根据文件名排序",
},
cli.BoolFlag{
Name: "size",
Usage: "根据大小排序",
},
},
}
}
func RunLs(driveId, targetPath string, lsOptions *LsOptions) {
func RunLs(driveId, targetPath string, lsOptions *LsOptions,
orderBy aliyunpan.FileOrderBy, orderDirection aliyunpan.FileOrderDirection) {
activeUser := config.Config.ActiveUser()
targetPath = activeUser.PathJoin(driveId, targetPath)
if targetPath[len(targetPath) - 1] == '/' {
targetPath = text.Substr(targetPath, 0, len(targetPath) - 1)
// 获取目标路径文件信息
targetPathInfo, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, targetPath)
if err != nil {
if err.Code == apierror.ApiCodeFileNotFoundCode {
fmt.Println("指定目录不存在: " + targetPath)
} else {
fmt.Println(err)
}
return
}
targetPathInfo, err := activeUser.PanClient().FileInfoByPath(driveId, targetPath)
if err != nil {
fmt.Println(err)
// 适配通配符路径获取目标文件信息(弃用,容易触发风控)
//files, err := matchPathByShellPattern(driveId, targetPath)
//if err != nil {
// fmt.Println(err)
// return
//}
//var targetPathInfo *aliyunpan.FileEntity
//if len(files) == 1 {
// targetPathInfo = files[0]
//} else {
// for _, f := range files {
// if f.IsFolder() {
// targetPathInfo = f
// break
// }
// }
//}
if targetPathInfo == nil {
fmt.Println("目录路径不存在")
return
}
@@ -106,20 +181,21 @@ func RunLs(driveId, targetPath string, lsOptions *LsOptions) {
fileListParam := &aliyunpan.FileListParam{}
fileListParam.ParentFileId = targetPathInfo.FileId
fileListParam.DriveId = driveId
fileListParam.OrderBy = orderBy
fileListParam.OrderDirection = orderDirection
if targetPathInfo.IsFolder() {
fileResult, err := activeUser.PanClient().FileListGetAll(fileListParam)
if err != nil {
fmt.Println(err)
fileResult, err1 := activeUser.PanClient().OpenapiPanClient().FileListGetAll(fileListParam, 200)
if err1 != nil {
fmt.Println(err1)
return
}
fileList = fileResult
} else {
fileList = append(fileList, targetPathInfo)
}
renderTable(opLs, lsOptions.Total, targetPath, fileList)
renderTable(opLs, lsOptions.Total, targetPathInfo.Path, fileList)
}
func renderTable(op int, isTotal bool, path string, files aliyunpan.FileList) {
tb := cmdtable.NewTable(os.Stdout)
var (
@@ -139,15 +215,15 @@ func renderTable(op int, isTotal bool, path string, files aliyunpan.FileList) {
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
for k, file := range files {
if file.IsFolder() {
tb.Append([]string{strconv.Itoa(k), file.FileId, "-", "-", "-", file.CreatedAt, file.UpdatedAt, file.FileName + aliyunpan.PathSeparator})
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, "-", "-", "-", file.CreatedAt, file.UpdatedAt, file.FileName + aliyunpan.PathSeparator})
continue
}
switch op {
case opLs:
tb.Append([]string{strconv.Itoa(k), file.FileId, converter.ConvertFileSize(file.FileSize, 2), file.ContentHash, strconv.FormatInt(file.FileSize, 10), file.CreatedAt, file.UpdatedAt, file.FileName})
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, converter.ConvertFileSize(file.FileSize, 2), file.ContentHash, strconv.FormatInt(file.FileSize, 10), file.CreatedAt, file.UpdatedAt, file.FileName})
case opSearch:
tb.Append([]string{strconv.Itoa(k), file.FileId, converter.ConvertFileSize(file.FileSize, 2), file.ContentHash, strconv.FormatInt(file.FileSize, 10), file.CreatedAt, file.UpdatedAt, file.Path})
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, converter.ConvertFileSize(file.FileSize, 2), file.ContentHash, strconv.FormatInt(file.FileSize, 10), file.CreatedAt, file.UpdatedAt, file.Path})
}
}
fN, dN = files.Count()
@@ -157,26 +233,22 @@ func renderTable(op int, isTotal bool, path string, files aliyunpan.FileList) {
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
for k, file := range files {
if file.IsFolder() {
tb.Append([]string{strconv.Itoa(k), "-", file.UpdatedAt, file.FileName + aliyunpan.PathSeparator})
tb.Append([]string{strconv.Itoa(k + 1), "-", file.UpdatedAt, file.FileName + aliyunpan.PathSeparator})
continue
}
switch op {
case opLs:
tb.Append([]string{strconv.Itoa(k), converter.ConvertFileSize(file.FileSize, 2), file.UpdatedAt, file.FileName})
tb.Append([]string{strconv.Itoa(k + 1), converter.ConvertFileSize(file.FileSize, 2), file.UpdatedAt, file.FileName})
case opSearch:
tb.Append([]string{strconv.Itoa(k), converter.ConvertFileSize(file.FileSize, 2), file.UpdatedAt, file.Path})
tb.Append([]string{strconv.Itoa(k + 1), converter.ConvertFileSize(file.FileSize, 2), file.UpdatedAt, file.Path})
}
}
fN, dN = files.Count()
tb.Append([]string{"", "总: " + converter.ConvertFileSize(files.TotalSize(), 2), "", fmt.Sprintf("文件总数: %d, 目录总数: %d", fN, dN)})
}
fmt.Printf("\n当前目录: %s\n", path)
fmt.Printf("----\n")
tb.Render()
if fN+dN >= 60 {
fmt.Printf("\n当前目录: %s\n", path)
}
fmt.Printf("----\n")
}
+6 -9
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -20,7 +20,6 @@ import (
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
"strings"
)
func CmdMkdir() cli.Command {
@@ -29,7 +28,7 @@ func CmdMkdir() cli.Command {
Usage: "创建目录",
UsageText: cmder.App().Name + " mkdir <目录>",
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -44,8 +43,8 @@ func CmdMkdir() cli.Command {
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "familyId",
Usage: "家庭云ID",
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
},
@@ -55,11 +54,9 @@ func CmdMkdir() cli.Command {
func RunMkdir(driveId, name string) {
activeUser := GetActiveUser()
fullpath := activeUser.PathJoin(driveId, name)
pathSlice := strings.Split(fullpath, "/")
rs := &aliyunpan.MkdirResult{}
err := apierror.NewFailedApiError("")
rs, err = activeUser.PanClient().MkdirRecursive(driveId,"", "", 0, pathSlice)
rs, err = activeUser.PanClient().OpenapiPanClient().MkdirByFullPath(driveId, fullpath)
if err != nil {
fmt.Println("创建文件夹失败:" + err.Error())
@@ -74,4 +71,4 @@ func RunMkdir(driveId, name string) {
} else {
fmt.Println("创建文件夹失败: ", fullpath)
}
}
}
+56 -32
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,28 +16,33 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
"os"
"path"
"strconv"
)
func CmdMv() cli.Command {
return cli.Command{
Name: "mv",
Usage: "移动文件/目录",
UsageText: `移动:
UsageText: `
aliyunpan mv <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>`,
Description: `
注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败.
注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败。支持通配符匹配移动文件,通配符当前只能匹配文件名,不能匹配文件路径。
示例:
将 /我的资源/1.mp4 移动到 根目录 /
aliyunpan mv /我的资源/1.mp4 /
将 /我的资源 目录下所有的.png文件 移动到 /我的图片 目录下面,使用通配符匹配
aliyunpan mv /我的资源/*.png /我的图片
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() <= 1 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -47,7 +52,6 @@ func CmdMv() cli.Command {
fmt.Println("未登录账号")
return nil
}
RunMove(parseDriveId(c), c.Args()...)
return nil
},
@@ -66,7 +70,7 @@ func RunMove(driveId string, paths ...string) {
activeUser := GetActiveUser()
cacheCleanPaths := []string{}
opFileList, targetFile, _, err := getFileInfo(driveId, paths...)
if err != nil {
if err != nil {
fmt.Println(err)
return
}
@@ -81,40 +85,44 @@ func RunMove(driveId string, paths ...string) {
cacheCleanPaths = append(cacheCleanPaths, targetFile.Path)
failedMoveFiles := []*aliyunpan.FileEntity{}
moveFileParamList := []*aliyunpan.FileMoveParam{}
fileId2FileEntity := map[string]*aliyunpan.FileEntity{}
for _,mfi := range opFileList {
fileId2FileEntity[mfi.FileId] = mfi
moveFileParamList = append(moveFileParamList,
&aliyunpan.FileMoveParam{
DriveId: driveId,
FileId: mfi.FileId,
ToDriveId: driveId,
ToParentFileId: targetFile.FileId,
})
cacheCleanPaths = append(cacheCleanPaths, path.Dir(mfi.Path))
}
fmr,er := activeUser.PanClient().FileMove(moveFileParamList)
for _,rs := range fmr {
if !rs.Success {
failedMoveFiles = append(failedMoveFiles, fileId2FileEntity[rs.FileId])
successMoveFiles := []*aliyunpan.FileEntity{}
for _, mfi := range opFileList {
fmr, er := activeUser.PanClient().OpenapiPanClient().FileMove(&aliyunpan.FileMoveParam{
DriveId: driveId,
FileId: mfi.FileId,
ToDriveId: driveId,
ToParentFileId: targetFile.FileId,
})
if er != nil || !fmr.Success {
failedMoveFiles = append(failedMoveFiles, mfi)
} else {
successMoveFiles = append(successMoveFiles, mfi)
}
cacheCleanPaths = append(cacheCleanPaths, path.Dir(mfi.Path))
}
if len(failedMoveFiles) > 0 {
fmt.Println("以下文件移动失败:")
for _,f := range failedMoveFiles {
fmt.Println(f.FileName)
for _, f := range failedMoveFiles {
fmt.Println(f.Path)
}
fmt.Println("")
}
if er == nil {
fmt.Println("操作成功, 已移动文件到目标目录: ", targetFile.Path)
activeUser.DeleteCache(cacheCleanPaths)
if len(successMoveFiles) > 0 {
pnt := func() {
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "文件/目录"})
for k, rs := range successMoveFiles {
tb.Append([]string{strconv.Itoa(k + 1), rs.Path})
}
tb.Render()
}
fmt.Println("操作成功, 以下文件已移动到目标目录: ", targetFile.Path)
pnt()
} else {
fmt.Println("无法移动文件,请稍后重试")
}
activeUser.DeleteCache(cacheCleanPaths)
}
func getFileInfo(driveId string, paths ...string) (opFileList []*aliyunpan.FileEntity, targetFile *aliyunpan.FileEntity, failedPaths []string, error error) {
@@ -125,11 +133,27 @@ func getFileInfo(driveId string, paths ...string) (opFileList []*aliyunpan.FileE
// the last one is the target file path
targetFilePath := path.Clean(paths[len(paths)-1])
absolutePath := activeUser.PathJoin(driveId, targetFilePath)
targetFile, err := activeUser.PanClient().FileInfoByPath(driveId, absolutePath)
targetFile, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, absolutePath)
if err != nil || !targetFile.IsFolder() {
return nil, nil, nil, fmt.Errorf("指定目标文件夹不存在")
}
opFileList, failedPaths, error = GetAppFileInfoByPaths(driveId, paths[:len(paths)-1]...)
for idx := 0; idx < (len(paths) - 1); idx++ {
absolutePath = path.Clean(activeUser.PathJoin(driveId, paths[idx]))
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
if err1 != nil {
failedPaths = append(failedPaths, absolutePath)
continue
}
if fileList == nil || len(fileList) == 0 {
// 文件不存在
failedPaths = append(failedPaths, absolutePath)
continue
}
for _, f := range fileList {
// 移动匹配的文件
opFileList = append(opFileList, f)
}
}
return
}
+5 -6
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,7 +15,6 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/converter"
"github.com/urfave/cli"
@@ -34,7 +33,7 @@ func CmdQuota() cli.Command {
Usage: "获取当前帐号空间配额",
Description: "获取网盘的总储存空间, 和已使用的储存空间",
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
@@ -42,7 +41,7 @@ func CmdQuota() cli.Command {
}
q, err := RunGetQuotaInfo()
if err == nil {
fmt.Printf("账号: %s, uid: %s, 个人空间总额: %s, 个人空间已使用: %s, 比率: %f%%\n",
fmt.Printf("账号: %s, uid: %s, 个人空间总额: %s, 个人空间已使用: %s, 比率: %.2f%%\n",
config.Config.ActiveUser().Nickname, config.Config.ActiveUser().UserId,
converter.ConvertFileSize(q.Quota, 2), converter.ConvertFileSize(q.UsedSize, 2),
100*float64(q.UsedSize)/float64(q.Quota))
@@ -53,12 +52,12 @@ func CmdQuota() cli.Command {
}
func RunGetQuotaInfo() (quotaInfo *QuotaInfo, error error) {
user, err := GetActivePanClient().GetUserInfo()
user, err := GetActivePanClient().OpenapiPanClient().GetUserInfo()
if err != nil {
return nil, err
}
return &QuotaInfo{
UsedSize: int64(user.UsedSize),
Quota: int64(user.TotalSize),
Quota: int64(user.TotalSize),
}, nil
}
+59 -41
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,13 +17,16 @@ import (
"fmt"
"github.com/olekukonko/tablewriter"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"os"
"strconv"
"time"
)
func CmdRecycle() cli.Command {
@@ -45,7 +48,7 @@ func CmdRecycle() cli.Command {
aliyunpan recycle delete -all
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NumFlags() <= 0 || c.NArg() <= 0 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -59,6 +62,14 @@ func CmdRecycle() cli.Command {
Usage: "列出回收站文件列表",
UsageText: cmder.App().Name + " recycle list",
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
RunRecycleList(parseDriveId(c))
return nil
},
@@ -77,6 +88,14 @@ func CmdRecycle() cli.Command {
UsageText: cmder.App().Name + " recycle restore <file_id 1> <file_id 2> <file_id 3> ...",
Description: `根据文件/目录的 fs_id, 还原回收站指定的文件或目录`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
if c.NArg() <= 0 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
@@ -99,6 +118,14 @@ func CmdRecycle() cli.Command {
UsageText: cmder.App().Name + " recycle delete [-all] <file_id 1> <file_id 2> <file_id 3> ...",
Description: `根据文件/目录的 file_id 或 -all 参数, 删除回收站指定的文件或目录或清空回收站`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
if c.Bool("all") {
// 清空回收站
RunRecycleClear(parseDriveId(c))
@@ -131,7 +158,7 @@ func CmdRecycle() cli.Command {
// RunRecycleList 执行列出回收站文件列表
func RunRecycleList(driveId string) {
panClient := GetActivePanClient()
fdl, err := panClient.RecycleBinFileListGetAll(&aliyunpan.RecycleBinFileListParam{
fdl, err := panClient.WebapiPanClient().RecycleBinFileListGetAll(&aliyunpan_web.RecycleBinFileListParam{
DriveId: driveId,
Limit: 100,
})
@@ -150,7 +177,7 @@ func RunRecycleList(driveId string) {
fn = fn + "/"
fs = "-"
}
tb.Append([]string{strconv.Itoa(k), file.FileId, fn, fs, file.CreatedAt, file.UpdatedAt})
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, fn, fs, file.CreatedAt, file.UpdatedAt})
}
tb.Render()
@@ -161,7 +188,7 @@ func RunRecycleRestore(driveId string, fidStrList ...string) {
panClient := GetActivePanClient()
restoreFileList := []*aliyunpan.FileBatchActionParam{}
for _,fid := range fidStrList {
for _, fid := range fidStrList {
restoreFileList = append(restoreFileList, &aliyunpan.FileBatchActionParam{
DriveId: driveId,
FileId: fid,
@@ -173,7 +200,7 @@ func RunRecycleRestore(driveId string, fidStrList ...string) {
return
}
rbfr, err := panClient.RecycleBinFileRestore(restoreFileList)
rbfr, err := panClient.WebapiPanClient().RecycleBinFileRestore(restoreFileList)
if rbfr != nil && len(rbfr) > 0 {
fmt.Printf("还原文件成功\n")
return
@@ -190,7 +217,7 @@ func RunRecycleDelete(driveId string, fidStrList ...string) {
panClient := GetActivePanClient()
deleteFileList := []*aliyunpan.FileBatchActionParam{}
for _,fid := range fidStrList {
for _, fid := range fidStrList {
deleteFileList = append(deleteFileList, &aliyunpan.FileBatchActionParam{
DriveId: driveId,
FileId: fid,
@@ -202,7 +229,7 @@ func RunRecycleDelete(driveId string, fidStrList ...string) {
return
}
rbfr, err := panClient.RecycleBinFileDelete(deleteFileList)
rbfr, err := panClient.WebapiPanClient().RecycleBinFileDelete(deleteFileList)
if rbfr != nil && len(rbfr) > 0 {
fmt.Printf("彻底删除文件成功\n")
return
@@ -218,39 +245,30 @@ func RunRecycleDelete(driveId string, fidStrList ...string) {
func RunRecycleClear(driveId string) {
panClient := GetActivePanClient()
for {
// get file list
fdl, err := panClient.RecycleBinFileListGetAll(&aliyunpan.RecycleBinFileListParam{
DriveId: driveId,
Limit: 100,
})
if err != nil {
logger.Verboseln(err)
break
}
if fdl == nil || len(fdl) == 0 {
break
}
// delete
deleteFileList := []*aliyunpan.FileBatchActionParam{}
for _,f := range fdl {
deleteFileList = append(deleteFileList, &aliyunpan.FileBatchActionParam{
DriveId: driveId,
FileId: f.FileId,
})
}
if len(deleteFileList) == 0 {
logger.Verboseln("没有需要删除的文件")
break
}
rbfr, err := panClient.RecycleBinFileDelete(deleteFileList)
if rbfr != nil && len(rbfr) > 0 {
logger.Verboseln("彻底删除文件成功")
}
// 提交清空回收站异步任务
r, err := panClient.WebapiPanClient().RecycleBinFileClear(&aliyunpan_web.RecycleBinFileClearParam{
DriveId: driveId,
})
if err != nil {
logger.Verboseln(err)
fmt.Printf("当前无法清空回收站,请稍后重试\n")
}
fmt.Printf("清空回收站成功\n")
for i := 0; i < 10; i++ {
ar, err1 := panClient.WebapiPanClient().AsyncTaskQueryStatus(&aliyunpan_web.AsyncTaskQueryStatusParam{
AsyncTaskId: r.AsyncTaskId,
})
if err1 != nil {
logger.Verboseln(err1)
time.Sleep(2 * time.Second)
continue
}
if ar.Status == "Succeed" {
fmt.Printf("清空回收站成功\n")
return
} else {
time.Sleep(1 * time.Second)
}
}
fmt.Printf("清空回收站失败,请稍后重试\n")
}
+181 -12
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,14 +15,42 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apiutil"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
"path"
"regexp"
"sort"
"strconv"
"strings"
)
type (
fileItem struct {
file *aliyunpan.FileEntity
newFileName string
}
fileArray []*fileItem
)
func newFileItem(file *aliyunpan.FileEntity) *fileItem {
return &fileItem{
file: file,
newFileName: "",
}
}
func (x fileArray) Len() int {
return len(x)
}
func (x fileArray) Less(i, j int) bool {
return x[i].file.FileName < x[j].file.FileName
}
func (x fileArray) Swap(i, j int) {
x[i], x[j] = x[j], x[i]
}
func CmdRename() cli.Command {
return cli.Command{
Name: "rename",
@@ -31,18 +59,32 @@ func CmdRename() cli.Command {
aliyunpan rename <旧文件/目录名> <新文件/目录名>`,
Description: `
示例:
1. 将文件 1.mp4 重命名为 2.mp4
aliyunpan rename 1.mp4 2.mp4
将文件 1.mp4 重命名为 2.mp4
aliyunpan rename 1.mp4 2.mp4
2. 将文件 /test/1.mp4 重命名为 /test/2.mp4
要求必须是同一个文件目录内
aliyunpan rename /test/1.mp4 /test/2.mp4
将文件 /test/1.mp4 重命名为 /test/2.mp4
要求必须是同一个文件目录内
aliyunpan rename /test/1.mp4 /test/2.mp4
批量重命名,规则:rename [expression] [replacement] [file]
其中,
expression - 表达式,即文件名中需要去掉的部分,如果为*代表旧的文件名全部去掉
replacement - 需要替换的新文件名,如果包含 # 号会按顺序替换成数字编号
file - 文件匹配模式,指定需要查找的文件匹配字符串,支持通配符,例如:*.mp4 即当前目录下所有的mp4文件
3. 批量重命名,将当前目录下所有.mp4文件中包含的字符串 "我的视频" 全部改成 "视频"
aliyunpan rename 我的视频 视频 *.mp4
4. 批量重命名,将当前目录下所有.mp4文件全部进行 "视频+编号.mp4" 的重命名操作,旧的名称全部去掉,即:视频001.mp4, 视频002.mp4, 视频003.mp4...
aliyunpan rename * 视频###.mp4 *.mp4
5. 批量重命名,将当前目录下所有.mp4文件全部进行 "视频+编号.mp4" 的重命名操作,旧的名称全部去掉,直接重命名无需人工确认操作
aliyunpan rename -y * 视频###.mp4 *.mp4
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() != 2 {
if c.NArg() != 2 && c.NArg() != 3 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
@@ -50,7 +92,12 @@ func CmdRename() cli.Command {
fmt.Println("未登录账号")
return nil
}
RunRename(parseDriveId(c), c.Args().Get(0), c.Args().Get(1))
if c.NArg() == 2 {
RunRename(parseDriveId(c), c.Args().Get(0), c.Args().Get(1))
} else if c.NArg() == 3 {
// 批量重命名
RunRenameBatch(c.Bool("y"), parseDriveId(c), c.Args().Get(0), c.Args().Get(1), c.Args().Get(2))
}
return nil
},
Flags: []cli.Flag{
@@ -59,6 +106,10 @@ func CmdRename() cli.Command {
Usage: "网盘ID",
Value: "",
},
cli.BoolFlag{
Name: "y",
Usage: "跳过人工确认,对批量操作有效",
},
},
}
}
@@ -85,14 +136,14 @@ func RunRename(driveId string, oldName string, newName string) {
}
fileId := ""
r, err := GetActivePanClient().FileInfoByPath(driveId, activeUser.PathJoin(driveId, oldName))
r, err := GetActivePanClient().OpenapiPanClient().FileInfoByPath(driveId, activeUser.PathJoin(driveId, oldName))
if err != nil {
fmt.Printf("原文件不存在: %s, %s\n", oldName, err)
return
}
fileId = r.FileId
b, e := activeUser.PanClient().FileRename(driveId, fileId, path.Base(newName))
b, e := activeUser.PanClient().OpenapiPanClient().FileRename(driveId, fileId, path.Base(newName))
if e != nil {
fmt.Println(e.Err)
return
@@ -104,3 +155,121 @@ func RunRename(driveId string, oldName string, newName string) {
fmt.Printf("重命名文件成功:%s -> %s\n", path.Base(oldName), path.Base(newName))
activeUser.DeleteOneCache(path.Dir(newName))
}
// RunRenameBatch 批量重命名文件
func RunRenameBatch(skipConfirm bool, driveId string, expression, replacement, filePattern string) {
if len(expression) == 0 {
fmt.Println("旧文件名不能为空")
return
}
if !apiutil.CheckFileNameValid(replacement) {
fmt.Println("新文件名不能包含特殊字符:" + apiutil.FileNameSpecialChars)
return
}
if len(filePattern) == 0 {
fmt.Println("文件匹配模式不能为空")
return
}
if strings.ContainsAny(filePattern, "/") {
fmt.Println("文件匹配模式不能包含路径分隔符")
return
}
if strings.ContainsAny(filePattern, "\\") {
fmt.Println("文件匹配模式不能包含路径分隔符")
return
}
activeUser := GetActiveUser()
absolutePath := path.Clean(activeUser.PathJoin(driveId, filePattern))
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
if err1 != nil {
fmt.Println("查询文件出错:" + err1.Error())
return
}
if fileList == nil || len(fileList) == 0 {
fmt.Println("没有找到符合的文件")
return
}
// 文件列表按照名字排序
files := fileArray{}
for _, f := range fileList {
files = append(files, newFileItem(f))
}
if len(files) > 1 {
sort.Sort(files)
}
// 依次处理
var index = 1
for _, file := range files {
replacementStr := replaceNumStr(replacement, index)
index += 1
if expression == "*" {
// 旧的名称全部替换
file.newFileName = replacementStr
} else {
// 替换指定的名称片段
file.newFileName = strings.ReplaceAll(file.file.FileName, expression, replacementStr)
}
}
// 确认
if !skipConfirm {
fmt.Printf("以下文件将进行对应的重命名\n\n")
idx := 1
for _, file := range files {
fmt.Printf("%d) %s -> %s\n", idx, file.file.FileName, file.newFileName)
idx += 1
}
fmt.Printf("\n是否进行批量重命名,该操作不可逆(y/n): ")
confirm := ""
_, err := fmt.Scanln(&confirm)
if err != nil || (confirm != "y" && confirm != "Y") {
fmt.Println("用户取消了操作")
return
}
}
// 重命名
for _, file := range files {
b, e := activeUser.PanClient().OpenapiPanClient().FileRename(driveId, file.file.FileId, file.newFileName)
if e != nil {
fmt.Println(e.Err)
return
}
if !b {
fmt.Println("重命名文件失败")
return
}
fmt.Printf("重命名文件成功:%s -> %s\n", file.file.FileName, file.newFileName)
activeUser.DeleteOneCache(path.Dir(file.file.Path))
}
}
// replaceNumStr 将#替换成数字编号
func replaceNumStr(name string, num int) string {
pattern, _ := regexp.Compile("[#]+")
r := pattern.FindAllStringSubmatchIndex(name, -1)
if len(r) == 0 {
return name
}
newName := name
c := r[0]
return replaceNumStr(newName[:c[0]]+generateNumStr(num, c[1]-c[0])+newName[c[1]:], num)
}
func generateNumStr(num, count int) string {
s := strconv.Itoa(num)
return generateZeroStr(count-len(s)) + s
}
func generateZeroStr(count int) string {
if count <= 0 {
return ""
}
s := ""
for i := 0; i < count; i++ {
s += "0"
}
return s
}
+41
View File
@@ -0,0 +1,41 @@
package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"sort"
"testing"
)
func TestRenameSort(t *testing.T) {
files := fileArray{}
files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "0我的文件01.txt"}))
files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "4我的文件03.txt"}))
files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "3我的文件02.txt"}))
files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "1我的文件00.txt"}))
sort.Sort(files)
for _, f := range files {
fmt.Println(f.file.FileName)
}
}
func TestRenameNum0(t *testing.T) {
fmt.Println(replaceNumStr("我的文件###.txt", 2))
}
func TestRenameNum1(t *testing.T) {
fmt.Println(replaceNumStr("我的##文件###.txt", 123))
}
func TestRenameNum2(t *testing.T) {
fmt.Println(replaceNumStr("我的文件###.txt", 1233))
}
func TestRenameNum3(t *testing.T) {
fmt.Println(replaceNumStr("我的文件[###].txt", 1233))
}
func TestRenameNum4(t *testing.T) {
fmt.Println(replaceNumStr("我的文件.txt", 1233))
}
func TestRenameNum5(t *testing.T) {
fmt.Println(replaceNumStr("", 1233))
}
+100 -31
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,10 +19,12 @@ import (
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/urfave/cli"
"os"
"path"
"strconv"
"strings"
)
func CmdRm() cli.Command {
@@ -32,7 +34,7 @@ func CmdRm() cli.Command {
UsageText: cmder.App().Name + " rm <文件/目录的路径1> <文件/目录2> <文件/目录3> ...",
Description: `
注意: 删除多个文件和目录时, 请确保每一个文件和目录都存在, 否则删除操作会失败.
被删除的文件或目录可在网盘文件回收站找回.
被删除的文件或目录可在网盘文件回收站找回。支持通配符匹配删除文件,通配符当前只能匹配文件名,不能匹配文件路径。
示例:
@@ -44,9 +46,12 @@ func CmdRm() cli.Command {
删除 /我的资源 整个目录 !!
aliyunpan rm /我的资源
删除 /我的资源 目录下面的所有.zip文件,使用通配符匹配
aliyunpan rm /我的资源/*.zip
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -69,49 +74,116 @@ func CmdRm() cli.Command {
}
}
func queryFileListByPath(activeUser *config.PanUser, driveId, filePathPattern string) (files []*aliyunpan.FileEntity, e error) {
if !strings.ContainsAny(filePathPattern, aliyunpan.ShellPatternCharacters) {
// 没有通配符,直接查询
fi, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, filePathPattern)
if err != nil {
return nil, err
}
files = append(files, fi)
} else {
// 包含通配符,使用通配符查询
files, e = matchPathByShellPattern(driveId, filePathPattern)
}
return files, e
}
// RunRemove 执行 批量删除文件/目录
func RunRemove(driveId string, paths ...string) {
activeUser := GetActiveUser()
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
plugin, _ := pluginManger.GetPlugin()
cacheCleanDirs := []string{}
failedRmPaths := make([]string, 0, len(paths))
delFileInfos := []*aliyunpan.FileBatchActionParam{}
fileId2FileEntity := map[string]*aliyunpan.FileEntity{}
successDelFileEntity := []*aliyunpan.FileEntity{}
for _, p := range paths {
absolutePath := path.Clean(activeUser.PathJoin(driveId, p))
fe, err := activeUser.PanClient().FileInfoByPath(driveId, absolutePath)
if err != nil {
// 查询路径对应的文件信息
fileList, err1 := queryFileListByPath(activeUser, driveId, absolutePath)
if err1 != nil {
failedRmPaths = append(failedRmPaths, absolutePath)
continue
}
fe.Path = absolutePath
delFileInfos = append(delFileInfos, &aliyunpan.FileBatchActionParam{
DriveId:driveId,
FileId:fe.FileId,
})
fileId2FileEntity[fe.FileId] = fe
cacheCleanDirs = append(cacheCleanDirs, path.Dir(fe.Path))
}
// delete
successDelFileEntity := []*aliyunpan.FileEntity{}
fdr, err := activeUser.PanClient().FileDelete(delFileInfos)
if fdr != nil {
for _,item := range fdr {
if !item.Success {
failedRmPaths = append(failedRmPaths, fileId2FileEntity[item.FileId].Path)
} else {
successDelFileEntity = append(successDelFileEntity, fileId2FileEntity[item.FileId])
if fileList == nil || len(fileList) == 0 {
// 跳过,没有满足条件的文件
failedRmPaths = append(failedRmPaths, absolutePath)
continue
}
// 调用插件
approvedToRemoveFiles := []*aliyunpan.FileEntity{}
pluginParam := &plugins.RemoveFilePrepareParams{
Count: len(fileList),
Items: make([]*plugins.RemoveFilePrepareItem, 0),
}
for _, f := range fileList {
pluginParam.Items = append(pluginParam.Items, &plugins.RemoveFilePrepareItem{
DriveId: f.DriveId,
DriveFileId: f.FileId,
DriveFileName: f.FileName,
DriveFilePath: f.Path,
DriveFileSize: f.FileSize,
DriveFileType: f.FileType,
DriveFileUpdatedAt: f.UpdatedAt,
DriveFileCreatedAt: f.CreatedAt,
})
}
if removeFilePrepareResult, er := plugin.RemoveFilePrepareCallback(plugins.GetContext(config.Config.ActiveUser()), pluginParam); er == nil && removeFilePrepareResult != nil {
for _, f := range fileList {
matchResult := false
for _, r := range removeFilePrepareResult.Result {
if strings.Compare(f.FileId, r.DriveFileId) == 0 {
matchResult = true
if strings.Compare("yes", r.RemoveApproved) != 0 {
// skip this file
fmt.Printf("插件不允许删除该文件: %s\n", f.Path)
} else {
approvedToRemoveFiles = append(approvedToRemoveFiles, f)
}
break
}
}
if !matchResult {
// 该文件没有确认结果,则默认删除
approvedToRemoveFiles = append(approvedToRemoveFiles, f)
}
}
} else {
// 默认删除全部文件
approvedToRemoveFiles = fileList
}
for _, f := range approvedToRemoveFiles {
// 删除匹配的文件
fdr, err := activeUser.PanClient().OpenapiPanClient().FileDelete(&aliyunpan.FileBatchActionParam{
DriveId: driveId,
FileId: f.FileId,
})
if err != nil || !fdr.Success {
failedRmPaths = append(failedRmPaths, absolutePath)
} else {
successDelFileEntity = append(successDelFileEntity, f)
}
cacheCleanDirs = append(cacheCleanDirs, path.Dir(f.Path))
}
}
// output
if len(failedRmPaths) > 0 {
fmt.Println("以下文件删除失败:")
for _, fp := range failedRmPaths {
fmt.Println(fp)
}
fmt.Println("")
}
pnt := func() {
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "文件/目录"})
for k := range successDelFileEntity {
tb.Append([]string{strconv.Itoa(k), successDelFileEntity[k].Path})
tb.Append([]string{strconv.Itoa(k + 1), successDelFileEntity[k].Path})
}
tb.Render()
}
@@ -119,10 +191,7 @@ func RunRemove(driveId string, paths ...string) {
fmt.Println("操作成功, 以下文件/目录已删除, 可在云盘文件回收站找回: ")
pnt()
activeUser.DeleteCache(cacheCleanDirs)
}
if len(successDelFileEntity) == 0 && err != nil {
fmt.Println("无法删除文件,请稍后重试")
return
} else {
fmt.Println("本次操作没有删除任何文件")
}
}
+25
View File
@@ -0,0 +1,25 @@
package command
import (
"fmt"
"testing"
)
func TestWildcard(t *testing.T) {
fmt.Println(isIncludeFile("ab0.txt", "ab0.txt"))
fmt.Println(isIncludeFile("*.zip", "aliyunpan-v0.0.1-darwin-macos-amd64.zip"))
fmt.Println(isIncludeFile(".*.swp", ".vd.txt.swp"))
fmt.Println(isIncludeFile("*.swp", ".swp"))
fmt.Println(isIncludeFile("*.swp", ".1swp"))
fmt.Println(isIncludeFile("a*b", "ab"))
fmt.Println(isIncludeFile("a*b", "aab"))
fmt.Println(isIncludeFile("a*b", "accccccccdb"))
fmt.Println(isIncludeFile("a?b", "acb"))
fmt.Println(isIncludeFile("a?b", "accb"))
fmt.Println(isIncludeFile("a[xyz]b", "axb"))
fmt.Println(isIncludeFile("ab[0-9].txt", "ab0.txt"))
fmt.Println(isIncludeFile("a*b/ab[0-9].txt", "acb/ab0.txt"))
fmt.Println(isIncludeFile("aliyunpan*", "aliyunpan-v0.0.1-darwin-macos-amd64[TNT].zip"))
}
+179
View File
@@ -0,0 +1,179 @@
// Copyright (c) 2020 tickstep.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
"path"
"strings"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
)
func CmdSave() cli.Command {
return cli.Command{
Name: "save",
Usage: "保存分享文件/目录",
UsageText: `
aliyunpan save <分享链接> (<提取码>) <目标目录>`,
Description: `
注意: 保存大量文件时, 命令完成后可能还需要额外等待一段时间; 分享的根目录下如果包含大量文件或文件夹, 可能存在不稳定的情况
示例:
将 公开分享 保存到 根目录 /
aliyunpan save ABCD1234wxyz /
aliyunpan save https://www.alipan.com/s/ABCD1234wxyz /
将 私密分享 保存到 指定目录 /资源分享
aliyunpan save ABCD1234wxyz akd1 /资源分享
aliyunpan save https://www.alipan.com/s/ABCD1234wxyz akd1 /资源分享
`,
Category: "阿里云盘",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() <= 1 || c.NArg() > 3 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
RunSave(parseDriveId(c), c.Args()...)
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
},
}
}
// RunSave 保存分享的文件
func RunSave(driveId string, args ...string) {
activeUser := GetActiveUser()
targetFilePath := path.Clean(args[len(args)-1])
absolutePath := activeUser.PathJoin(driveId, targetFilePath)
targetFile, err := activeUser.PanClient().WebapiPanClient().FileInfoByPath(driveId, absolutePath)
if err != nil || !targetFile.IsFolder() {
fmt.Println("指定目标文件夹不存在")
return
}
fmt.Println("保存文件至:", targetFilePath)
shareID := args[0]
if i := strings.Index(shareID, "alipan.com/s/"); i > 0 {
shareID = shareID[i+13:]
}
sharePwd := ""
if len(args) == 3 {
sharePwd = args[1]
}
token, err := activeUser.PanClient().WebapiPanClient().GetShareToken(shareID, sharePwd)
if err != nil {
fmt.Println("读取分享链接失败:", err)
return
}
list, err := activeUser.PanClient().WebapiPanClient().GetListByShare(token.ShareToken, shareID, "")
if err != nil {
fmt.Println("读取分享文件列表失败:", err)
return
}
for list.NextMarker != "" {
list2, err := activeUser.PanClient().WebapiPanClient().GetListByShare(token.ShareToken, shareID, "")
if err != nil {
fmt.Println("读取分享文件列表失败:", err)
return
}
list.Items = append(list.Items, list2.Items...)
list.NextMarker = list2.NextMarker
}
var params []*aliyunpan_web.FileSaveParam
files := make(map[string]*aliyunpan_web.ListByShareItem)
for _, item := range list.Items {
if item.FileExtension != "" {
fmt.Println(" ", item.Name)
} else {
fmt.Println(" ", item.Name+"/")
}
files[item.FileID] = item
params = append(params, &aliyunpan_web.FileSaveParam{
ShareID: shareID,
FileId: item.FileID,
AutoRename: true,
ToDriveId: driveId,
ToParentFileId: targetFile.FileId,
})
}
fmt.Println()
result, err := activeUser.PanClient().WebapiPanClient().FileCopy(token.ShareToken, params)
if err != nil {
fmt.Println("保存分享文件失败:", err)
return
}
var ids []string
var failedSaveFileIds []string
tasks := make(map[string]string)
for _, item := range result {
if item.AsyncTaskId == "" {
if item.Status != 201 {
failedSaveFileIds = append(failedSaveFileIds, item.FileId)
}
} else {
tasks[item.AsyncTaskId] = item.FileId
ids = append(ids, item.AsyncTaskId)
}
}
if ids != nil {
result2, err := activeUser.PanClient().WebapiPanClient().AsyncTaskGet(token.ShareToken, ids)
if err != nil {
fmt.Println("读取保存结果失败:", err)
}
for _, item := range result2 {
if !item.Success {
failedSaveFileIds = append(failedSaveFileIds, tasks[item.AsyncTaskId])
}
}
}
if failedSaveFileIds != nil {
fmt.Println("以下文件保存失败:")
for _, id := range failedSaveFileIds {
if v, ok := files[id]; ok {
fmt.Println(v.Name)
} else {
fmt.Println(id)
}
}
fmt.Println("")
}
fmt.Println("操作成功, 分享文件已保存到目标目录: ", targetFile.Path)
}
+116 -206
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -18,12 +18,10 @@ import (
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"os"
"strconv"
"path"
"strings"
"time"
)
@@ -33,7 +31,7 @@ func CmdShare() cli.Command {
Usage: "分享文件/目录",
UsageText: cmder.App().Name + " share",
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
@@ -48,10 +46,19 @@ func CmdShare() cli.Command {
示例:
创建文件 1.mp4 的分享链接
aliyunpan share set 1.mp4
aliyunpan share set -mode 1 1.mp4
创建 /我的视频/ 目录下所有mp4文件的分享链接,支持通配符
aliyunpan share set -mode 1 /我的视频/*.mp4
创建文件 1.mp4 的分享链接,并指定分享密码为2333
aliyunpan share set -mode 1 -sharePwd 2333 1.mp4
创建文件 1.mp4 的分享链接,并指定有效期为1天
aliyunpan share set -time 1 1.mp4
aliyunpan share set -mode 1 -time 1 1.mp4
创建文件 1.mp4 的快传链接
aliyunpan share set 1.mp4
`,
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
@@ -62,6 +69,8 @@ func CmdShare() cli.Command {
fmt.Println("未登录账号")
return nil
}
// 有效期
et := ""
timeFlag := "0"
if c.IsSet("time") {
@@ -76,17 +85,36 @@ func CmdShare() cli.Command {
et = ""
}
// 密码
sharePwd := ""
modeFlag := "1"
if c.IsSet("sharePwd") {
sharePwd = c.String("sharePwd")
}
if sharePwd == "" {
sharePwd = RandomStr(4)
}
// 模式
modeFlag := "3"
if c.IsSet("mode") {
modeFlag = c.String("mode")
}
if modeFlag == "1" || modeFlag == "2" {
if config.Config.ActiveUser().ActiveDriveId != config.Config.ActiveUser().DriveList.GetResourceDriveId() {
// 只有资源库才支持私有、公开分享
fmt.Println("只有资源库才支持分享链接,其他请使用快传链接")
return nil
}
}
if modeFlag == "1" {
sharePwd = RandomStr(4)
if sharePwd == "" {
sharePwd = RandomStr(4)
}
} else {
sharePwd = ""
}
RunShareSet(parseDriveId(c), c.Args(), et, sharePwd)
RunOpenShareSet(modeFlag, parseDriveId(c), c.Args(), et, sharePwd)
return nil
},
Flags: []cli.Flag{
@@ -102,98 +130,48 @@ func CmdShare() cli.Command {
},
cli.StringFlag{
Name: "mode",
Usage: "有效期,1-私密分享,2-公开分享",
Value: "1",
Usage: "模式,1-私密分享,2-公开分享,3-快传",
Value: "3",
},
cli.StringFlag{
Name: "sharePwd",
Usage: "自定义私密分享密码,4个字符,没有指定则随机生成",
Value: "",
},
},
},
{
Name: "list",
Aliases: []string{"l"},
Usage: "列出已分享文件/目录",
UsageText: cmder.App().Name + " share list",
Action: func(c *cli.Context) error {
RunShareList()
return nil
},
Flags: []cli.Flag{
},
},
{
Name: "cancel",
Aliases: []string{"c"},
Usage: "取消分享文件/目录",
UsageText: cmder.App().Name + " share cancel <shareid_1> <shareid_2> ...",
Description: `目前只支持通过分享id (shareid) 来取消分享.`,
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
RunShareCancel(c.Args())
return nil
},
},
// {
// Name: "mc",
// Aliases: []string{},
// Usage: "创建秒传链接",
// UsageText: cmder.App().Name + " share mc <文件/目录1> <文件/目录2> ...",
// Description: `
//创建文件秒传链接,秒传链接只能是文件,如果是文件夹则会创建文件夹包含的所有文件的秒传链接。秒传链接可以通过RapidUpload命令或者Import命令进行导入到自己的网盘。
//示例:
// 创建文件 1.mp4 的秒传链接
// aliyunpan share mc 1.mp4
//
// 创建文件 1.mp4 的秒传链接,但链接隐藏相对路径
// aliyunpan share mc -hp 1.mp4
//
// 创建文件夹 share_folder 下面所有文件的秒传链接
// aliyunpan share mc share_folder/
//`,
// Action: func(c *cli.Context) error {
// if c.NArg() < 1 {
// cli.ShowCommandHelp(c, c.Command.Name)
// return nil
// }
// if config.Config.ActiveUser() == nil {
// fmt.Println("未登录账号")
// return nil
// }
// hp := false
// if c.IsSet("hp") {
// hp = c.Bool("hp")
// }
// RunShareMc(parseDriveId(c), hp, c.Args())
// return nil
// },
// Flags: []cli.Flag{
// cli.StringFlag{
// Name: "driveId",
// Usage: "网盘ID",
// Value: "",
// },
// cli.BoolFlag{
// Name: "hp",
// Usage: "hide path, 隐藏相对目录",
// },
// },
// },
},
}
}
// RunShareSet 执行分享
func RunShareSet(driveId string, paths []string, expiredTime string, sharePwd string) {
panClient := GetActivePanClient()
fileList, _, err := GetFileInfoByPaths(paths[:len(paths)]...)
if err != nil {
fmt.Println(err)
// RunOpenShareSet 执行分享
func RunOpenShareSet(modeFlag, driveId string, paths []string, expiredTime string, sharePwd string) {
if len(paths) <= 0 {
fmt.Println("请指定文件路径")
return
}
activeUser := GetActiveUser()
panClient := activeUser.PanClient()
allFileList := []*aliyunpan.FileEntity{}
for idx := 0; idx < len(paths); idx++ {
absolutePath := path.Clean(activeUser.PathJoin(driveId, paths[idx]))
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
if err1 != nil {
fmt.Println("文件不存在: " + absolutePath)
continue
}
if fileList == nil || len(fileList) == 0 {
// 文件不存在
fmt.Println("文件不存在: " + absolutePath)
continue
}
// 匹配的文件
allFileList = append(allFileList, fileList...)
}
fidList := []string{}
for _,f := range fileList {
for _, f := range allFileList {
fidList = append(fidList, f.FileId)
}
@@ -202,119 +180,51 @@ func RunShareSet(driveId string, paths []string, expiredTime string, sharePwd st
return
}
r, err1 := panClient.ShareLinkCreate(aliyunpan.ShareCreateParam{
DriveId: driveId,
SharePwd: sharePwd,
Expiration: expiredTime,
FileIdList: fidList,
})
if err1 != nil || r == nil {
if err1.Code == apierror.ApiCodeFileShareNotAllowed {
fmt.Printf("创建分享链接失败: 该文件类型不允许分享\n")
} else {
fmt.Printf("创建分享链接失败: %s\n", err)
}
return
}
fmt.Printf("创建分享链接成功\n")
if len(sharePwd) > 0 {
fmt.Printf("链接:%s 提取码:%s\n", r.ShareUrl, r.SharePwd)
} else {
fmt.Printf("链接:%s\n", r.ShareUrl)
}
}
// RunShareList 执行列出分享列表
func RunShareList() {
activeUser := GetActiveUser()
records, err := activeUser.PanClient().ShareLinkList(activeUser.UserId)
if err != nil {
fmt.Printf("获取分享列表失败: %s\n", err)
return
}
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "ShARE_ID", "分享链接", "提取码", "文件名", "FILE_ID", "过期时间", "状态"})
now := time.Now()
for k, record := range records {
et := "永久有效"
if len(record.Expiration) > 0 {
et = record.Expiration
}
status := "有效"
if record.FirstFile == nil {
status = "已删除"
} else {
cz := time.FixedZone("CST", 8*3600)
if len(record.Expiration) > 0 {
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", record.Expiration, cz)
if expiredTime.Unix() < now.Unix() {
status = "已过期"
}
}
}
tb.Append([]string{strconv.Itoa(k), record.ShareId, record.ShareUrl, record.SharePwd,
record.ShareName,
record.FileIdList[0],
et,
status})
}
tb.Render()
}
// RunShareCancel 执行取消分享
func RunShareCancel(shareIdList []string) {
if len(shareIdList) == 0 {
fmt.Printf("取消分享操作失败, 没有任何 shareid\n")
return
}
activeUser := GetActiveUser()
r, err := activeUser.PanClient().ShareLinkCancel(shareIdList)
if err != nil {
fmt.Printf("取消分享操作失败: %s\n", err)
return
}
if r != nil && len(r) > 0 {
fmt.Printf("取消分享操作成功\n")
} else {
fmt.Printf("取消分享操作失败\n")
}
}
// 创建秒传链接
func RunShareMc(driveId string, hideRelativePath bool, panPaths []string) {
activeUser := config.Config.ActiveUser()
panClient := activeUser.PanClient()
totalCount := 0
for _,panPath := range panPaths {
panPath = activeUser.PathJoin(driveId, panPath)
panClient.FilesDirectoriesRecurseList(driveId, panPath, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
if apiError != nil {
logger.Verbosef("%s\n", apiError)
return true
}
// 只需要文件即可
if !fd.IsFolder() {
item := newRapidUploadItemFromFileEntity(fd)
jstr := item.createRapidUploadLink(hideRelativePath)
if len(jstr) <= 0 {
logger.Verboseln("create rapid upload link err")
return false
}
// print
fmt.Println(jstr)
totalCount += 1
time.Sleep(time.Duration(100) * time.Millisecond)
}
return true
// 创建分享类型
if modeFlag == "3" {
// 快传
r, err1 := panClient.OpenapiPanClient().FastShareLinkCreate(aliyunpan.FastShareCreateParam{
DriveId: driveId,
FileIdList: fidList,
})
if err1 != nil || r == nil {
if err1.Code == apierror.ApiCodeFileShareNotAllowed {
fmt.Printf("创建快传链接失败: 该文件类型不允许分享\n")
} else {
fmt.Printf("创建快传链接失败: %s\n", err1)
}
return
}
fmt.Printf("创建快传链接成功\n")
fmt.Printf("链接:%s\n", strings.ReplaceAll(r.ShareUrl, "https://www.aliyundrive.com", "https://www.alipan.com"))
} else {
// 分享
r, err1 := panClient.OpenapiPanClient().ShareLinkCreate(aliyunpan.ShareCreateParam{
DriveId: driveId,
SharePwd: sharePwd,
Expiration: expiredTime,
FileIdList: fidList,
})
if err1 != nil || r == nil {
if err1.Code == apierror.ApiCodeFileShareNotAllowed {
fmt.Printf("创建分享链接失败: 该文件类型不允许分享\n")
} else {
fmt.Printf("创建分享链接失败: %s\n", err1)
}
return
}
if modeFlag == "1" {
fmt.Printf("创建私密分享链接成功\n")
} else if modeFlag == "2" {
fmt.Printf("创建公开分享链接成功\n")
}
shareUrl := strings.ReplaceAll(r.ShareUrl, "https://www.aliyundrive.com", "https://www.alipan.com")
if len(sharePwd) > 0 {
fmt.Printf("链接:%s 提取码:%s\n", shareUrl, r.SharePwd)
} else {
fmt.Printf("链接:%s\n", shareUrl)
}
}
fmt.Printf("\n秒传链接总数量: %d\n", totalCount)
}
}
+443
View File
@@ -0,0 +1,443 @@
// Copyright (c) 2020 tickstep.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"encoding/csv"
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
"os"
"path"
"path/filepath"
"strconv"
"time"
)
// web端分享接口
func CmdShareWeb() cli.Command {
return cli.Command{
Name: "sharew",
Usage: "分享文件/目录",
UsageText: cmder.App().Name + " sharew",
Category: "阿里云盘",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
},
Subcommands: []cli.Command{
{
Name: "set",
Aliases: []string{"s"},
Usage: "设置分享文件/目录",
UsageText: cmder.App().Name + " share set <文件/目录1> <文件/目录2> ...",
Description: `
示例:
创建文件 1.mp4 的分享链接
aliyunpan share set -mode 1 1.mp4
创建 /我的视频/ 目录下所有mp4文件的分享链接,支持通配符
aliyunpan share set -mode 1 /我的视频/*.mp4
创建文件 1.mp4 的分享链接,并指定分享密码为2333
aliyunpan share set -mode 1 -sharePwd 2333 1.mp4
创建文件 1.mp4 的分享链接,并指定有效期为1天
aliyunpan share set -mode 1 -time 1 1.mp4
创建文件 1.mp4 的快传链接
aliyunpan share set 1.mp4
`,
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
et := ""
timeFlag := "0"
if c.IsSet("time") {
timeFlag = c.String("time")
}
now := time.Now()
if timeFlag == "1" {
et = now.Add(time.Duration(1) * time.Hour * 24).Format("2006-01-02 15:04:05")
} else if timeFlag == "2" {
et = now.Add(time.Duration(7) * time.Hour * 24).Format("2006-01-02 15:04:05")
} else {
et = ""
}
sharePwd := ""
if c.IsSet("sharePwd") {
sharePwd = c.String("sharePwd")
}
modeFlag := "3"
if c.IsSet("mode") {
modeFlag = c.String("mode")
}
if modeFlag == "1" || modeFlag == "2" {
if config.Config.ActiveUser().ActiveDriveId != config.Config.ActiveUser().DriveList.GetResourceDriveId() {
// 只有资源库才支持私有、公开分享
fmt.Println("只有资源库才支持分享链接,其他请使用快传链接")
return nil
}
}
if modeFlag == "1" {
if sharePwd == "" {
sharePwd = RandomStr(4)
}
} else {
sharePwd = ""
}
RunShareSet(modeFlag, parseDriveId(c), c.Args(), et, sharePwd)
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
cli.StringFlag{
Name: "time",
Usage: "有效期,0-永久,1-1天,2-7天",
Value: "0",
},
cli.StringFlag{
Name: "mode",
Usage: "模式,1-私密分享,2-公开分享,3-快传",
Value: "3",
},
cli.StringFlag{
Name: "sharePwd",
Usage: "自定义私密分享密码,4个字符,没有指定则随机生成",
Value: "",
},
},
},
{
Name: "list",
Aliases: []string{"l"},
Usage: "列出已分享文件/目录",
UsageText: cmder.App().Name + " share list",
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
RunShareList()
return nil
},
Flags: []cli.Flag{},
},
{
Name: "cancel",
Aliases: []string{"c"},
Usage: "取消分享文件/目录",
UsageText: cmder.App().Name + " share cancel <shareid_1> <shareid_2> ...",
Description: `目前只支持通过分享id (shareid) 来取消分享.`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
if c.NArg() < 1 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
RunShareCancel(c.Args())
return nil
},
},
{
Name: "export",
Usage: "导出分享记录保存到文件",
UsageText: cmder.App().Name + " share export <csv file path>",
Description: `
导出分享记录,并保存到指定的文件。目前支持csv格式
示例:
导出所有有效的分享并保存成文件
aliyunpan share export "d:\myfoler\share_list.csv"
导出所有的分享并保存成文件
aliyunpan share export -option 2 "d:\myfoler\share_list.csv"
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
if c.NArg() < 1 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
opt := c.String("option")
if opt == "" {
opt = "1"
}
filePath := c.Args()[0]
RunShareExport(opt, filePath)
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "option",
Usage: "导出选项,1-有效分享 2-全部分享",
Value: "1",
},
},
},
},
}
}
// RunShareSet 执行分享
func RunShareSet(modeFlag, driveId string, paths []string, expiredTime string, sharePwd string) {
if len(paths) <= 0 {
fmt.Println("请指定文件路径")
return
}
activeUser := GetActiveUser()
panClient := activeUser.PanClient()
allFileList := []*aliyunpan.FileEntity{}
for idx := 0; idx < len(paths); idx++ {
absolutePath := path.Clean(activeUser.PathJoin(driveId, paths[idx]))
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
if err1 != nil {
fmt.Println("文件不存在: " + absolutePath)
continue
}
if fileList == nil || len(fileList) == 0 {
// 文件不存在
fmt.Println("文件不存在: " + absolutePath)
continue
}
// 匹配的文件
allFileList = append(allFileList, fileList...)
}
fidList := []string{}
for _, f := range allFileList {
fidList = append(fidList, f.FileId)
}
if len(fidList) == 0 {
fmt.Printf("没有指定有效的文件\n")
return
}
if modeFlag == "3" {
// 快传
r, err1 := panClient.WebapiPanClient().FastShareLinkCreate(aliyunpan.FastShareCreateParam{
DriveId: driveId,
FileIdList: fidList,
})
if err1 != nil || r == nil {
if err1.Code == apierror.ApiCodeFileShareNotAllowed {
fmt.Printf("创建快传链接失败: 该文件类型不允许分享\n")
} else {
fmt.Printf("创建快传链接失败: %s\n", err1)
}
return
}
fmt.Printf("创建快传链接成功\n")
fmt.Printf("链接:%s\n", r.ShareUrl)
} else {
// 分享
r, err1 := panClient.WebapiPanClient().ShareLinkCreate(aliyunpan.ShareCreateParam{
DriveId: driveId,
SharePwd: sharePwd,
Expiration: expiredTime,
FileIdList: fidList,
})
if err1 != nil || r == nil {
if err1.Code == apierror.ApiCodeFileShareNotAllowed {
fmt.Printf("创建分享链接失败: 该文件类型不允许分享\n")
} else {
fmt.Printf("创建分享链接失败: %s\n", err1)
}
return
}
fmt.Printf("创建分享链接成功\n")
if len(sharePwd) > 0 {
fmt.Printf("链接:%s 提取码:%s\n", r.ShareUrl, r.SharePwd)
} else {
fmt.Printf("链接:%s\n", r.ShareUrl)
}
}
}
// RunShareList 执行列出分享列表
func RunShareList() {
activeUser := GetActiveUser()
records, err := activeUser.PanClient().WebapiPanClient().ShareLinkList(activeUser.UserId)
if err != nil {
fmt.Printf("获取分享列表失败: %s\n", err)
return
}
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "ShARE_ID", "分享链接", "提取码", "文件名", "过期时间", "状态"})
now := time.Now()
for k, record := range records {
et := "永久有效"
if len(record.Expiration) > 0 {
et = record.Expiration
}
status := "有效"
if record.Status == "enabled" {
if record.FirstFile == nil {
status = "已删除"
} else {
cz := time.FixedZone("CST", 8*3600)
if len(record.Expiration) > 0 {
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", record.Expiration, cz)
if expiredTime.Unix() < now.Unix() {
status = "已过期"
}
}
}
} else if record.Status == "forbidden" {
status = "违规"
}
tb.Append([]string{strconv.Itoa(k + 1), record.ShareId, record.ShareUrl, record.SharePwd,
record.ShareName,
//record.FileIdList[0],
et,
status})
}
tb.Render()
}
// RunShareCancel 执行取消分享
func RunShareCancel(shareIdList []string) {
if len(shareIdList) == 0 {
fmt.Printf("取消分享操作失败, 没有任何 shareid\n")
return
}
activeUser := GetActiveUser()
r, err := activeUser.PanClient().WebapiPanClient().ShareLinkCancel(shareIdList)
if err != nil {
fmt.Printf("取消分享操作失败: %s\n", err)
return
}
if r != nil && len(r) > 0 {
fmt.Printf("取消分享操作成功\n")
} else {
fmt.Printf("取消分享操作失败\n")
}
}
func RunShareExport(option, saveFilePath string) {
activeUser := GetActiveUser()
records, err := activeUser.PanClient().WebapiPanClient().ShareLinkList(activeUser.UserId)
if err != nil {
fmt.Printf("获取分享列表失败: %s\n", err)
return
}
columns := [][]string{{"序号", "分享ID", "分享链接", "提取码", "文件名", "过期时间", "状态"}}
now := time.Now()
idx := 1
for _, record := range records {
et := "永久有效"
if len(record.Expiration) > 0 {
et = record.Expiration
}
status := "有效"
if record.FirstFile == nil {
status = "已删除"
if option == "1" {
continue
}
} else {
cz := time.FixedZone("CST", 8*3600)
if len(record.Expiration) > 0 {
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", record.Expiration, cz)
if expiredTime.Unix() < now.Unix() {
status = "已过期"
}
}
}
if option == "1" {
if status == "已过期" || status == "已删除" {
continue
}
}
line := []string{strconv.Itoa(idx), record.ShareId, record.ShareUrl, record.SharePwd, record.ShareName, et, status}
idx += 1
columns = append(columns, line)
}
// save to file
if ExportCsv(saveFilePath, columns) {
fmt.Println("分享导出成功:", saveFilePath)
}
}
func ExportCsv(savePath string, data [][]string) bool {
folder := filepath.Dir(savePath)
if _, err := os.Stat(folder); err != nil {
if !os.IsExist(err) {
os.MkdirAll(folder, os.ModePerm)
}
}
fp, err := os.Create(savePath) // 创建文件句柄
if err != nil {
fmt.Println("创建文件["+savePath+"]失败,%v", err)
return false
}
defer fp.Close()
fp.WriteString("\xEF\xBB\xBF") // 写入UTF-8 BOM
w := csv.NewWriter(fp) //创建一个新的写入文件流
w.WriteAll(data)
w.Flush()
return true
}
+435
View File
@@ -0,0 +1,435 @@
// Copyright (c) 2020 tickstep.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/global"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/syncdrive"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"os"
"path"
"strings"
"time"
)
func CmdSync() cli.Command {
return cli.Command{
Name: "sync",
Usage: "同步备份功能(Beta)",
UsageText: cmder.App().Name + " sync",
Description: `
备份功能。支持备份本地文件到云盘,备份云盘文件到本地两种模式。支持JavaScript插件对备份文件进行过滤。
指定本地目录和对应的一个网盘目录,以备份文件。网盘目录必须和本地目录独占使用,不要用作其他用途,不然备份可能会有问题。
备份功能支持以下模式:
1. upload
备份本地文件,即上传本地文件到网盘,始终保持本地文件有一个完整的备份在网盘
2. download
备份云盘文件,即下载网盘文件到本地,始终保持网盘的文件有一个完整的备份在本地
请输入以下命令查看如何配置和启动:
aliyunpan sync start -h
`,
Category: "阿里云盘",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
},
Subcommands: []cli.Command{
{
Name: "start",
Usage: "启动sync同步备份任务",
UsageText: cmder.App().Name + " sync start [arguments...]",
Description: `
备份本地文件到文件网盘,或者备份文件网盘的文件到本地。支持命令行配置启动或者使用备份配置文件启动同步备份任务。
配置文件保存在:(配置目录)/sync_drive/sync_drive_config.json,样例如下:
{
"configVer": "1.0",
"syncTaskList": [
{
"name": "设计文档备份",
"localFolderPath": "D:/tickstep/Documents/设计文档",
"panFolderPath": "/sync_drive/我的文档",
"mode": "upload",
"policy": "increment",
"driveName": "backup"
}
]
}
相关字段说明如下:
name - 任务名称
localFolderPath - 本地目录
panFolderPath - 网盘目录
mode - 备份模式,支持两种: upload(备份本地文件到云盘),download(备份云盘文件到本地)
policy - 备份策略, 支持两种: exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)
driveName - 网盘名称,backup(备份盘),resource(资源盘)
例子:
1. 查看帮助
aliyunpan sync start -h
2. 使用命令行配置启动同步备份服务,将本地目录 D:\tickstep\Documents\设计文档 中的文件备份上传到"备份盘"的云盘目录 /sync_drive/我的文档
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "upload" -drive "backup"
3. 使用命令行配置启动同步备份服务,将云盘目录 /sync_drive/我的文档 中的文件备份下载到本地目录 D:\tickstep\Documents\设计文档
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "download"
4. 使用命令行配置启动同步备份服务,将本地目录 D:\tickstep\Documents\设计文档 中的文件备份到云盘目录 /sync_drive/我的文档
同时配置下载并发为2,上传并发为1,下载分片大小为256KB,上传分片大小为1MB
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "upload" -dp 2 -up 1 -dbs 256 -ubs 1024
5. 使用配置文件启动同步备份服务,使用配置文件可以支持同时启动多个备份任务。配置文件必须存在,否则启动失败。
aliyunpan sync start
6. 使用配置文件启动同步备份服务,并配置下载并发为2,上传并发为1,下载分片大小为256KB,上传分片大小为1MB
aliyunpan sync start -dp 2 -up 1 -dbs 256 -ubs 1024
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
activeUser := GetActiveUser()
if c.String("log") == "true" {
syncdrive.LogPrompt = true
} else {
syncdrive.LogPrompt = false
}
dp := c.Int("dp")
if dp == 0 {
dp = config.Config.MaxDownloadParallel
}
if dp == 0 {
dp = 2
}
up := c.Int("up")
if up == 0 {
up = config.Config.MaxUploadParallel
}
if up == 0 {
up = 2
}
downloadBlockSize := int64(c.Int("dbs") * 1024)
if downloadBlockSize == 0 {
downloadBlockSize = int64(config.Config.CacheSize)
}
if downloadBlockSize == 0 {
downloadBlockSize = int64(256 * 1024)
}
uploadBlockSize := int64(c.Int("ubs") * 1024)
if uploadBlockSize == 0 {
uploadBlockSize = aliyunpan.DefaultChunkSize
}
var syncOpt syncdrive.SyncPriorityOption = syncdrive.SyncPriorityTimestampFirst
//opt := c.String("pri")
//if opt == "local" {
// syncOpt = syncdrive.SyncPriorityLocalFirst
//} else if opt == "pan" {
// syncOpt = syncdrive.SyncPriorityPanFirst
//} else {
// syncOpt = syncdrive.SyncPriorityTimestampFirst
//}
var task *syncdrive.SyncTask
localDir := c.String("ldir")
panDir := c.String("pdir")
mode := c.String("mode")
policy := c.String("policy")
driveName := c.String("drive")
if localDir != "" && panDir != "" {
// make path absolute
if !utils.IsLocalAbsPath(localDir) {
pwd, _ := os.Getwd()
localDir = path.Join(pwd, path.Clean(localDir))
}
panDir = activeUser.PathJoin(activeUser.ActiveDriveId, panDir)
if !utils.IsLocalAbsPath(localDir) {
fmt.Println("本地目录请指定绝对路径")
return nil
}
if !utils.IsPanAbsPath(panDir) {
fmt.Println("网盘目录请指定绝对路径")
return nil
}
//if b, e := utils.PathExists(localDir); e == nil {
// if !b {
// fmt.Println("本地文件夹不存在:", localDir)
// return nil
// }
//} else {
// fmt.Println("本地文件夹不存在:", localDir)
// return nil
//}
task = &syncdrive.SyncTask{}
task.LocalFolderPath = path.Clean(strings.ReplaceAll(localDir, "\\", "/"))
task.PanFolderPath = panDir
task.Mode = syncdrive.Upload
if mode == string(syncdrive.Upload) {
task.Mode = syncdrive.Upload
} else if mode == string(syncdrive.Download) {
task.Mode = syncdrive.Download
} else if mode == string(syncdrive.SyncTwoWay) {
task.Mode = syncdrive.SyncTwoWay
} else {
task.Mode = syncdrive.Upload
}
if policy == string(syncdrive.SyncPolicyExclusive) {
task.Policy = syncdrive.SyncPolicyExclusive
} else {
task.Policy = syncdrive.SyncPolicyIncrement
}
task.Name = path.Base(task.LocalFolderPath)
task.Id = utils.Md5Str(task.LocalFolderPath)
task.Priority = syncOpt
task.UserId = activeUser.UserId
// drive id
task.DriveName = driveName
if strings.ToLower(task.DriveName) == "backup" {
task.DriveId = activeUser.DriveList.GetFileDriveId()
} else if strings.ToLower(task.DriveName) == "resource" {
task.DriveId = activeUser.DriveList.GetResourceDriveId()
}
if len(task.DriveId) == 0 {
task.DriveName = "backup"
task.DriveId = activeUser.DriveList.GetFileDriveId()
}
}
cycleMode := syncdrive.CycleInfiniteLoop
if c.String("cycle") == "onetime" {
cycleMode = syncdrive.CycleOneTime
} else {
cycleMode = syncdrive.CycleInfiniteLoop
}
scanIntervalTime := int64(c.Int("sit") * 60)
if scanIntervalTime == 0 {
// 默认1分钟
scanIntervalTime = 60
}
RunSync(task, cycleMode, dp, up, downloadBlockSize, uploadBlockSize, syncOpt, c.Int("ldt"), scanIntervalTime)
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "drive",
Usage: "drive name, 网盘名称,backup(备份盘),resource(资源盘)",
Value: "backup",
},
cli.StringFlag{
Name: "ldir",
Usage: "local dir, 本地文件夹完整路径",
},
cli.StringFlag{
Name: "pdir",
Usage: "pan dir, 云盘文件夹完整路径",
},
cli.StringFlag{
Name: "mode",
Usage: "备份模式, 支持两种: upload(备份本地文件到云盘),download(备份云盘文件到本地)",
Value: "upload",
},
cli.StringFlag{
Name: "policy",
Usage: "备份策略, 支持两种: exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)",
Value: "increment",
},
//cli.StringFlag{
// Name: "pri",
// Usage: "同步优先级,只对sync模式有效。当网盘和本地存在同名文件,优先使用哪个,选项支持三种: time-时间优先,local-本地优先,pan-网盘优先",
// Value: "time",
//},
cli.StringFlag{
Name: "cycle",
Usage: "备份周期, 支持两种: infinity(永久循环备份),onetime(只运行一次备份)",
Value: "infinity",
},
cli.IntFlag{
Name: "dp",
Usage: "download parallel, 下载并发数量,即可以同时并发下载多少个文件。0代表跟从配置文件设置(取值范围:1 ~ 10)",
Value: 0,
},
cli.IntFlag{
Name: "up",
Usage: "upload parallel, 上传并发数量,即可以同时并发上传多少个文件。0代表跟从配置文件设置(取值范围:1 ~ 10)",
Value: 0,
},
cli.IntFlag{
Name: "dbs",
Usage: "download block size,下载分片大小,单位KB。推荐值:1024 ~ 10240",
Value: 1024,
},
cli.IntFlag{
Name: "ubs",
Usage: "upload block size,上传分片大小,单位KB。推荐值:1024 ~ 10240。当上传极大单文件时候请适当调高该值",
Value: 10240,
},
cli.StringFlag{
Name: "log",
Usage: "是否显示文件备份过程日志,true-显示,false-不显示",
Value: "false",
},
cli.IntFlag{
Name: "ldt",
Usage: "local delay time,本地文件修改检测延迟间隔,单位秒。如果本地文件会被频繁修改,例如录制视频文件,配置好该时间可以避免上传未录制好的文件。",
Value: 3,
},
cli.IntFlag{
Name: "sit",
Usage: "scan interval time,扫描文件间隔时间,单位:分钟。",
Value: 1,
},
},
},
},
}
}
func RunSync(defaultTask *syncdrive.SyncTask, cycleMode syncdrive.CycleMode, fileDownloadParallel, fileUploadParallel int, downloadBlockSize, uploadBlockSize int64,
flag syncdrive.SyncPriorityOption, localDelayTime int, scanTimeInterval int64) {
maxDownloadRate := config.Config.MaxDownloadRate
maxUploadRate := config.Config.MaxUploadRate
activeUser := GetActiveUser()
panClient := activeUser.PanClient()
panClient.OpenapiPanClient().ClearCache()
panClient.OpenapiPanClient().DisableCache()
//// pan token expired checker
//continueFlag := int32(0)
//atomic.StoreInt32(&continueFlag, 0)
//defer func() {
// atomic.StoreInt32(&continueFlag, 1)
//}()
//go func(flag *int32) {
// for atomic.LoadInt32(flag) == 0 {
// time.Sleep(time.Duration(1) * time.Minute)
// if RefreshWebTokenInNeed(activeUser, config.Config.DeviceName) {
// logger.Verboseln("update access token for sync task")
// userWebToken := NewWebLoginToken(activeUser.WebapiToken.AccessToken, activeUser.WebapiToken.Expired)
// panClient.WebapiPanClient().UpdateToken(userWebToken)
// }
// }
//}(&continueFlag)
syncFolderRootPath := config.GetSyncDriveDir()
if b, e := utils.PathExists(syncFolderRootPath); e == nil {
if !b {
os.MkdirAll(syncFolderRootPath, 0755)
}
}
var tasks []*syncdrive.SyncTask
if defaultTask != nil {
tasks = []*syncdrive.SyncTask{}
tasks = append(tasks, defaultTask)
}
fmt.Println("启动同步备份进程")
// 文件同步记录器
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/sync_file_records.csv")
option := syncdrive.SyncOption{
FileDownloadParallel: fileDownloadParallel,
FileUploadParallel: fileUploadParallel,
FileDownloadBlockSize: downloadBlockSize,
FileUploadBlockSize: uploadBlockSize,
MaxDownloadRate: maxDownloadRate,
MaxUploadRate: maxUploadRate,
SyncPriority: flag,
LocalFileModifiedCheckIntervalSec: localDelayTime,
FileRecorder: fileRecorder,
}
syncMgr := syncdrive.NewSyncTaskManager(activeUser, panClient, syncFolderRootPath, option)
syncConfigFile := syncMgr.ConfigFilePath()
if tasks != nil {
syncConfigFile = "(使用命令行配置)"
}
fmt.Printf("备份配置文件:%s\n下载并发:%d\n上传并发:%d\n下载分片大小:%s\n上传分片大小:%s\n",
syncConfigFile, fileDownloadParallel, fileUploadParallel, converter.ConvertFileSize(downloadBlockSize, 2),
converter.ConvertFileSize(uploadBlockSize, 2))
if _, e := syncMgr.Start(tasks, cycleMode, scanTimeInterval); e != nil {
fmt.Println("启动任务失败:", e)
return
}
_, ok := os.LookupEnv("ALIYUNPAN_DOCKER")
if ok {
// in docker container
if cycleMode == syncdrive.CycleInfiniteLoop {
// 使用休眠以节省CPU资源
fmt.Println("本命令不会退出,程序正在以Docker的方式运行。如需退出请借助Docker提供的方式。")
for {
time.Sleep(60 * time.Second)
}
} else {
for {
if syncMgr.IsAllTaskCompletely() {
fmt.Println("所有备份任务已完成")
break
}
time.Sleep(5 * time.Second)
}
syncMgr.DoTaskSyncCompletelyPluginCallback()
}
} else {
if cycleMode == syncdrive.CycleInfiniteLoop {
if global.IsAppInCliMode {
// in cmd mode
c := ""
fmt.Println("本命令不会退出,如需要结束同步备份进程请输入y,然后按Enter键进行停止。")
for strings.ToLower(c) != "y" {
fmt.Scan(&c)
}
} else {
fmt.Println("本命令不会退出,程序正在以非交互的方式运行。如需退出请借助运行环境提供的方式。")
logger.Verboseln("App not in CLI mode, not need to listen to input stream")
for {
time.Sleep(60 * time.Second)
}
}
} else {
for {
if syncMgr.IsAllTaskCompletely() {
fmt.Println("所有备份任务已完成")
break
}
time.Sleep(5 * time.Second)
}
syncMgr.DoTaskSyncCompletelyPluginCallback()
}
}
fmt.Println("正在退出同步备份任务,请稍等...")
// stop task
syncMgr.Stop()
}
-99
View File
@@ -1,99 +0,0 @@
// Copyright (c) 2020 tickstep.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
)
func CmdToken() cli.Command {
return cli.Command{
Name: "token",
Usage: "Token相关操作",
UsageText: cmder.App().Name + " token",
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
Action: func(c *cli.Context) error {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
},
Subcommands: []cli.Command{
{
Name: "update",
Usage: "更新Token",
UsageText: cmder.App().Name + " token update",
Description: `
示例:
更新当前登录用户的Token
aliyunpan token update -mode 1
更新全部登录用户的Token
aliyunpan token update -mode 2
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号,无需刷新Token")
return nil
}
modeFlag := "1"
if c.IsSet("mode") {
modeFlag = c.String("mode")
}
RunTokenUpdate(modeFlag)
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "mode",
Usage: "模式,1-登录用户,2-全部用户",
Value: "1",
},
},
},
},
}
}
// RunTokenUpdate 执行Token更新
func RunTokenUpdate(modeFlag string) {
cmder.ReloadConfigFunc(nil)
userList := config.Config.UserList
if userList == nil || len(userList) == 0 {
fmt.Printf("没有登录用户,无需刷新Token\n")
return
}
for _,user := range userList {
if modeFlag == "1" {
if user.UserId != config.Config.ActiveUID {
continue
}
}
newToken,e := aliyunpan.GetAccessTokenFromRefreshToken(user.RefreshToken)
if e != nil {
fmt.Printf("无法为%s用户获取新的RefreshToken,可能需要重新登录\n", user.Nickname)
continue
}
if newToken != nil && newToken.RefreshToken != "" {
user.RefreshToken = newToken.RefreshToken
fmt.Printf("成功刷新%s用户的RefreshToken\n", user.Nickname)
}
}
cmder.SaveConfigFunc(nil)
}
+242
View File
@@ -0,0 +1,242 @@
package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/converter"
"github.com/urfave/cli"
"path"
"strings"
)
func CmdTree() cli.Command {
return cli.Command{
Name: "tree",
Usage: "列出目录的树形图",
UsageText: cmder.App().Name + " tree <目录>",
Description: `
列出指定目录内的文件和目录, 并以树形图的方式呈现
示例:
列出 当前工作目录 内的文件和目录的树形图
aliyunpan tree
列出 /我的资源 内的文件和目录的树形图
aliyunpan tree /我的资源
列出 /我的资源 内的文件和目录的树形图,使用通配符
aliyunpan tree /我的*
列出 /我的资源 内的文件和目录的树形图,并且显示文件对应的完整绝对路径
aliyunpan tree -fp /我的资源
列出 /我的资源 内的文件和目录的树形图,并且显示文件对应的文件大小
aliyunpan tree -fs /我的资源
列出 /我的资源 内的文件和目录的树形图,过滤大于等于 10kb 并且小于等于 10mb 的文件,同时显示文件对应的文件大小
aliyunpan tree -fs -minSize=1kb -maxSize=10mb /我的资源
`,
Category: "阿里云盘",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
minSize := int64(0)
if c.IsSet("minSize") {
if s, e := converter.ParseFileSizeStr(c.String("minSize")); e == nil {
minSize = s
}
}
maxSize := int64(0)
if c.IsSet("maxSize") {
if s, e := converter.ParseFileSizeStr(c.String("maxSize")); e == nil {
maxSize = s
}
}
RunTree(parseDriveId(c), c.Args().Get(0), c.Bool("fp"), c.Bool("fs"), minSize, maxSize)
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
cli.BoolFlag{
Name: "fp",
Usage: "full path, 树形图显示文件的完整路径",
},
cli.BoolFlag{
Name: "fs",
Usage: "file size, 树形图显示文件的文件大小",
},
cli.StringFlag{
Name: "minSize",
Usage: "min size, 过滤大于等于指定大小的文件,例如:100mb",
},
cli.StringFlag{
Name: "maxSize",
Usage: "max size, 过滤小于等于指定大小的文件,例如:1gb",
},
},
}
}
const (
indentPrefix = "│ "
pathPrefix = "├──"
lastFilePrefix = "└──"
)
type (
treeStatistic struct {
CountOfDir int64
CountOfFile int64
SizeOfFile int64
}
treeConfig struct {
showFullPath bool
showFileSize bool
minFileSize int64
maxFileSize int64
}
)
func getTree(driveId, pathStr string, depth int, statistic *treeStatistic, setting *treeConfig) {
activeUser := config.Config.ActiveUser()
pathStr = activeUser.PathJoin(driveId, pathStr)
pathStr = path.Clean(pathStr)
// 获取目标路径文件信息
targetPathInfo, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, pathStr)
if err != nil {
fmt.Println(err)
return
}
// 适配通配符路径获取目标文件信息(弃用,容易触发风控)
//files, err := matchPathByShellPattern(driveId, pathStr)
//if err != nil {
// fmt.Println(err)
// return
//}
//var targetPathInfo *aliyunpan.FileEntity
//if len(files) == 1 {
// targetPathInfo = files[0]
//} else {
// for _, f := range files {
// if f.IsFolder() {
// targetPathInfo = f
// break
// }
// }
//}
if targetPathInfo == nil {
fmt.Println("路径不存在")
return
}
if depth == 0 {
fmt.Printf("%s\n", targetPathInfo.Path)
}
fileList := aliyunpan.FileList{}
fileListParam := &aliyunpan.FileListParam{}
fileListParam.ParentFileId = targetPathInfo.FileId
fileListParam.DriveId = driveId
fileListParam.OrderBy = aliyunpan.FileOrderByName
fileListParam.OrderDirection = aliyunpan.FileOrderDirectionAsc
if targetPathInfo.IsFolder() {
fileResult, err := activeUser.PanClient().OpenapiPanClient().FileListGetAll(fileListParam, 500)
if err != nil {
fmt.Println(err)
return
}
fileList = append(fileList, fileResult...)
} else {
fileList = append(fileList, targetPathInfo)
}
var (
prefix = pathPrefix
fN = len(fileList)
indentPrefixStr = strings.Repeat(indentPrefix, depth)
)
for i, file := range fileList {
if file.IsFolder() {
statistic.CountOfDir += 1
if setting.showFullPath {
fmt.Printf("%v%v %v/ -> %s\n", indentPrefixStr, pathPrefix, file.FileName, targetPathInfo.Path+"/"+file.FileName)
} else {
fmt.Printf("%v%v %v/\n", indentPrefixStr, pathPrefix, file.FileName)
}
getTree(driveId, targetPathInfo.Path+"/"+file.FileName, depth+1, statistic, setting)
continue
}
// filter file size
if setting.minFileSize > 0 {
if file.FileSize < setting.minFileSize {
continue
}
}
if setting.maxFileSize > 0 {
if file.FileSize > setting.maxFileSize {
continue
}
}
statistic.CountOfFile += 1
statistic.SizeOfFile += file.FileSize
if i+1 == fN {
prefix = lastFilePrefix
}
// 文件大小
fileName := &strings.Builder{}
if setting.showFileSize {
fmt.Fprintf(fileName, "%s (%s)", file.FileName, converter.ConvertFileSize(file.FileSize, 2))
} else {
fmt.Fprintf(fileName, "%s", file.FileName)
}
// 文件完整路径
if setting.showFullPath {
fmt.Printf("%v%v %v -> %s\n", indentPrefixStr, prefix, fileName.String(), targetPathInfo.Path+"/"+file.FileName)
} else {
fmt.Printf("%v%v %v\n", indentPrefixStr, prefix, fileName.String())
}
}
return
}
// RunTree 列出树形图
func RunTree(driveId, pathStr string, showFullPath, showFileSize bool, minSize, maxSize int64) {
activeUser := config.Config.ActiveUser()
activeUser.PanClient().OpenapiPanClient().ClearCache()
activeUser.PanClient().OpenapiPanClient().EnableCache()
defer activeUser.PanClient().OpenapiPanClient().DisableCache()
pathStr = activeUser.PathJoin(driveId, pathStr)
statistic := &treeStatistic{
CountOfDir: 0,
CountOfFile: 0,
SizeOfFile: 0,
}
setting := &treeConfig{
showFullPath: showFullPath,
showFileSize: showFileSize,
minFileSize: minSize,
maxFileSize: maxSize,
}
getTree(driveId, pathStr, 0, statistic, setting)
fmt.Printf("\n%d 个文件夹, %d 个文件, %s 总大小\n", statistic.CountOfDir, statistic.CountOfFile, converter.ConvertFileSize(statistic.SizeOfFile, 2))
}
+229 -354
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,18 +15,21 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/utils"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/tickstep/aliyunpan/internal/ui"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/requester/rio/speeds"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
@@ -53,23 +56,25 @@ const (
type (
// UploadOptions 上传可选项
UploadOptions struct {
AllParallel int // 所有文件并发上传数量,即可以同时并发上传多少个文件
Parallel int // 单个文件并发上传数量
MaxRetry int
NoRapidUpload bool
ShowProgress bool
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
DriveId string
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行上传,支持正则表达式
BlockSize int64 // 分片大小
UseInternalUrl bool // 是否使用内置链接
AllParallel int // 所有文件并发上传数量,即可以同时并发上传多少个文件
Parallel int // 单个文件并发上传数量
MaxRetry int
MaxTimeoutSec int // http请求超时时间,单位秒
NoRapidUpload bool
ShowProgress bool
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
IsSkipSameName bool // 跳过已存在的文件,即使文件内容不一致(不检查SHA1)
DriveId string
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行上传,支持正则表达式
BlockSize int64 // 分片大小
IsUseUIDashboard bool // 是否使用UI面板显示上传进度
}
)
var UploadFlags = []cli.Flag{
cli.IntFlag{
Name: "p",
Usage: "本次操作文件上传并发数量,即可以同时并发上传多少个文件。0代表跟从配置文件设置",
Usage: "本次操作文件上传并发数量,即可以同时并发上传多少个文件。0代表跟从配置文件设置(取值范围:1 ~ 20)",
Value: 0,
},
cli.IntFlag{
@@ -77,6 +82,10 @@ var UploadFlags = []cli.Flag{
Usage: "上传失败最大重试次数",
Value: DefaultUploadMaxRetry,
},
cli.IntFlag{
Name: "timeout",
Usage: "上传请求超时时间,单位为秒。当遇到网络不好导致上传超时可以尝试调大该值,建议设置30秒以上",
},
cli.BoolFlag{
Name: "np",
Usage: "no progress 不展示上传进度条",
@@ -85,9 +94,13 @@ var UploadFlags = []cli.Flag{
Name: "ow",
Usage: "overwrite, 覆盖已存在的同名文件,注意已存在的文件会被移到回收站",
},
cli.BoolFlag{
Name: "skip",
Usage: "skip same name, 跳过已存在的同名文件,即使文件内容不一致(不检查SHA1)",
},
cli.BoolFlag{
Name: "norapid",
Usage: "不检测秒传",
Usage: "不检测秒传。跳过费时的SHA1计算直接上传",
},
cli.StringFlag{
Name: "driveId",
@@ -101,8 +114,12 @@ var UploadFlags = []cli.Flag{
},
cli.IntFlag{
Name: "bs",
Usage: "block size,上传分片大小,单位KB。推荐值:512 ~ 2048",
Value: 512,
Usage: "block size,上传分片大小,单位KB。推荐值:1024 ~ 10240。当上传极大单文件时候请适当调高该值",
Value: 10240,
},
cli.BoolFlag{
Name: "ui",
Usage: "(BETA) 使用UI面板显示上传详情和进度,更加直观和友好",
},
}
@@ -113,7 +130,7 @@ func CmdUpload() cli.Command {
Usage: "上传文件/目录",
UsageText: cmder.App().Name + " upload <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>",
Description: `
上传指定的文件夹或者文件,上传的文件将会保存到 <目标目录>.
上传指定的文件夹或者文件,上传的文件将会保存到 <目标目录>。支持软链接文件,包括Linux/macOS(ln命令)和Windows(mklink命令)创建的符号链接文件。
示例:
1. 将本地的 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录
@@ -126,21 +143,27 @@ func CmdUpload() cli.Command {
3. 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录
aliyunpan upload C:/Users/Administrator/Desktop /视频
4. 使用相对路径
4. 将本地 200GB 极大文件 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录,需要调高上传分片大小
aliyunpan upload -bs 30720 C:/Users/Administrator/Desktop/1.mp4 /视频
5. 使用相对路径
aliyunpan upload 1.mp4 /视频
5. 覆盖上传,已存在的同名文件会被移到回收站
6. 覆盖上传,已存在的同名文件会被移到回收站
aliyunpan upload -ow 1.mp4 /视频
6. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
7. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
aliyunpan upload -exn "\.jpg$" C:/Users/Administrator/Video /视频
7. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
8. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
aliyunpan upload -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视频
8. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的 @eadir 文件夹
9. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的 @eadir 文件夹
aliyunpan upload -exn "^@eadir$" C:/Users/Administrator/Video /视频
10. 跳过已存在的同名文件,即使文件内容不一致(不检查SHA1)
aliyunpan upload -skip 1.mp4 /视频
参考:
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式。在正则表达式中,^表示匹配开头,$表示匹配结尾。
1)排除@eadir文件或者文件夹:-exn "^@eadir$"
@@ -150,7 +173,7 @@ func CmdUpload() cli.Command {
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() < 2 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -158,96 +181,54 @@ func CmdUpload() cli.Command {
}
subArgs := c.Args()
timeout := 0
if c.IsSet("timeout") {
timeout = c.Int("timeout")
if timeout < 0 {
timeout = 0
}
}
// 获取上传文件锁,保证上传操作单实例
//locker := filelocker.NewFileLocker(config.GetLockerDir() + "/aliyunpan-upload")
//if e := filelocker.LockFile(locker, 0755, true, 5*time.Second); e != nil {
// logger.Verboseln(e)
// fmt.Println("本应用其他实例正在执行上传,请先停止或者等待其完成")
// return nil
//}
RunUpload(subArgs[:c.NArg()-1], subArgs[c.NArg()-1], &UploadOptions{
AllParallel: c.Int("p"), // 多文件上传的时候,允许同时并行上传的文件数量
Parallel: 1, // 一个文件同时多少个线程并发上传的数量。阿里云盘只支持单线程按顺序进行文件part数据上传,所以只能是1
MaxRetry: c.Int("retry"),
NoRapidUpload: c.Bool("norapid"),
ShowProgress: !c.Bool("np"),
IsOverwrite: c.Bool("ow"),
DriveId: parseDriveId(c),
ExcludeNames: c.StringSlice("exn"),
BlockSize: int64(c.Int("bs") * 1024),
AllParallel: c.Int("p"), // 多文件上传的时候,允许同时并行上传的文件数量
Parallel: 1, // 一个文件同时多少个线程并发上传的数量。阿里云盘只支持单线程按顺序进行文件part数据上传,所以只能是1
MaxRetry: c.Int("retry"),
MaxTimeoutSec: timeout,
NoRapidUpload: c.Bool("norapid"),
ShowProgress: !c.Bool("np"),
IsOverwrite: c.Bool("ow"),
IsSkipSameName: c.Bool("skip"),
DriveId: parseDriveId(c),
ExcludeNames: c.StringSlice("exn"),
BlockSize: int64(c.Int("bs") * 1024),
IsUseUIDashboard: c.Bool("ui"),
})
// 释放文件锁
//if locker != nil {
// filelocker.UnlockFile(locker)
//}
return nil
},
Flags: UploadFlags,
}
}
func CmdRapidUpload() cli.Command {
return cli.Command{
Name: "rapidupload",
Aliases: []string{"ru"},
Usage: "手动秒传文件",
UsageText: cmder.App().Name + " rapidupload \"aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder\"",
Description: `
使用此功能秒传文件, 前提是知道文件的大小, sha1, 且网盘中存在一模一样的文件.
上传的文件将会保存到网盘的目标目录。文件的秒传链接可以通过share或者export命令获取。
链接格式说明:aliyunpan://文件名|sha1|文件大小|<相对路径>
"相对路径" 可以为空,为空代表存储到网盘根目录
示例:
1. 如果秒传成功, 则保存到网盘路径 /pan_folder/file.dmg
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder"
2. 如果秒传成功, 则保存到网盘路径 /file.dmg
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|"
3. 同时秒传多个文件,如果秒传成功, 则保存到网盘路径 /pan_folder/file.dmg, /pan_folder/file1.dmg
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder" "aliyunpan://file1.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder"
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() <= 0 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
subArgs := c.Args()
RunRapidUpload(parseDriveId(c), c.Bool("ow"), subArgs, c.String("path"))
return nil
},
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ow",
Usage: "overwrite, 覆盖已存在的文件。已存在的文件会并移到回收站",
},
cli.StringFlag{
Name: "path",
Usage: "存储到网盘目录,绝对路径,例如:/myfolder",
Value: "",
},
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
},
}
}
// RunUpload 执行文件上传
func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
activeUser := GetActiveUser()
// pan token expired checker
go func() {
cz := time.FixedZone("CST", 8*3600) // 东8区
for {
time.Sleep(time.Duration(1) * time.Minute)
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", activeUser.WebToken.ExpireTime, cz)
now := time.Now()
if (expiredTime.Unix() - now.Unix()) <= (10 * 60) {
// need refresh token
if wt, er := aliyunpan.GetAccessTokenFromRefreshToken(activeUser.RefreshToken); er == nil {
activeUser.WebToken = *wt
activeUser.PanClient().UpdateToken(*wt)
logger.Verboseln("update access token for upload task")
}
}
}
}()
activeUser.PanClient().OpenapiPanClient().EnableCache()
activeUser.PanClient().OpenapiPanClient().ClearCache()
defer activeUser.PanClient().OpenapiPanClient().DisableCache()
if opt == nil {
opt = &UploadOptions{}
@@ -256,17 +237,31 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
// 检测opt
if opt.AllParallel <= 0 {
opt.AllParallel = config.Config.MaxUploadParallel
if opt.AllParallel == 0 {
opt.AllParallel = config.DefaultFileUploadParallelNum
}
}
if opt.AllParallel > config.MaxFileUploadParallelNum {
fmt.Printf("警告: 当前上传文件并发数过大,可能会被阿里风控导致上传失败,建议调小。\n")
}
if opt.Parallel <= 0 {
opt.Parallel = 1
}
if opt.MaxRetry < 0 {
opt.MaxRetry = DefaultUploadMaxRetry
}
opt.UseInternalUrl = config.Config.TransferUrlType == 2
// 超时时间
if opt.MaxTimeoutSec > 0 {
activeUser.PanClient().OpenapiPanClient().SetTimeout(time.Duration(opt.MaxTimeoutSec) * time.Second)
}
targetDriveName := config.Config.ActiveUser().DriveList.GetDriveNameById(opt.DriveId)
fmt.Printf("\n[0] 当前文件上传最大并发量为: %d, 上传分片大小为: %s, 目标网盘: %s\n", opt.AllParallel, converter.ConvertFileSize(opt.BlockSize, 2), targetDriveName)
savePath = activeUser.PathJoin(opt.DriveId, savePath)
_, err1 := activeUser.PanClient().FileInfoByPath(opt.DriveId, savePath)
_, err1 := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(opt.DriveId, savePath)
if err1 != nil {
fmt.Printf("警告: 上传文件, 获取云盘路径 %s 错误, %s\n", savePath, err1)
}
@@ -277,7 +272,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
return
}
// 打开上传状态
// 打开上传状态数据库
uploadDatabase, err := panupload.NewUploadingDatabase()
if err != nil {
fmt.Printf("打开上传未完成数据库错误: %s\n", err)
@@ -294,20 +289,47 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
statistic = &panupload.UploadStatistic{}
folderCreateMutex = &sync.Mutex{}
pluginManger = plugins.NewPluginManager(config.GetPluginDir())
)
executor.SetParallel(opt.AllParallel)
statistic.StartTimer() // 开始计时
// 全局速度统计
globalSpeedsStat := &speeds.Speeds{}
// 上传统计UI面板
var dashboard *ui.DashboardPanel = nil
if opt.IsUseUIDashboard && opt.ShowProgress && ui.IsTerminal(os.Stdout) {
dashboard = ui.NewDashboardPanel(ui.DashboardPanelUpload, opt.AllParallel, globalSpeedsStat, &ui.DashboardOptions{
Title: "上传统计UI面板",
ActiveSlots: 3, // 下载文件进度展示,最大同时展示3个
MaxHistory: 50, // 下载日志显示,最多同时显示50条,这个会按照窗口大小进行自适应显示
})
}
logf := func(format string, a ...interface{}) {
if dashboard != nil {
dashboard.Logf(format, a...)
return
}
fmt.Printf(format, a...)
}
// 获取当前插件
plugin, _ := pluginManger.GetPlugin()
// 上传记录器
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/upload_file_records.csv")
// 遍历指定的文件并创建上传任务
for _, curPath := range localPaths {
var walkFunc filepath.WalkFunc
var db panupload.SyncDb
var walkFunc localfile.MyWalkFunc
curPath = filepath.Clean(curPath)
localPathDir := filepath.Dir(curPath)
// 是否排除上传
if isExcludeFile(curPath, opt) {
fmt.Printf("排除文件: %s\n", curPath)
if utils.IsExcludeFile(curPath, &opt.ExcludeNames) {
logf("排除文件: %s\n", curPath)
continue
}
@@ -316,121 +338,134 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
localPathDir = ""
}
if fi, err := os.Stat(curPath); err == nil && fi.IsDir() {
//使用绝对路径避免异常
dbpath, err := filepath.Abs(curPath)
walkFunc = func(file localfile.SymlinkFile, fi os.FileInfo, err error) error {
if err != nil {
dbpath = curPath
// skip this error file and continue recurse
logger.Verboseln("upload process file: ", file, " error: ", err)
return nil
}
dbpath += string(os.PathSeparator) + BackupMetaDirName
if di, err := os.Stat(dbpath); err == nil && di.IsDir() {
db, err = panupload.OpenSyncDb(dbpath+string(os.PathSeparator) + "db", BackupMetaBucketName)
if db != nil {
defer func(syncDb panupload.SyncDb) {
db.Close()
}(db)
} else {
fmt.Println(curPath, "同步数据库打开失败,跳过该目录的备份", err)
continue
}
}
}
walkFunc = func(file string, fi os.FileInfo, err error) error {
if err != nil {
return err
if os.PathSeparator == '\\' {
file.LogicPath = cmdutil.ConvertToWindowsPathSeparator(file.LogicPath)
file.RealPath = cmdutil.ConvertToWindowsPathSeparator(file.RealPath)
}
// 是否排除上传
if isExcludeFile(file, opt) {
fmt.Printf("排除文件: %s\n", file)
if utils.IsExcludeFile(file.LogicPath, &opt.ExcludeNames) {
logf("排除文件: %s\n", file.LogicPath)
return filepath.SkipDir
}
if fi.Mode()&os.ModeSymlink != 0 { // 读取 symbol link
err = WalkAllFile(file+string(os.PathSeparator), walkFunc)
return err
}
subSavePath := strings.TrimPrefix(file, localPathDir)
subSavePath := strings.TrimPrefix(file.LogicPath, localPathDir)
// 针对 windows 的目录处理
if os.PathSeparator == '\\' {
subSavePath = cmdutil.ConvertToUnixPathSeparator(subSavePath)
}
subSavePath = path.Clean(savePath + aliyunpan.PathSeparator + subSavePath)
var ufm *panupload.UploadedFileMeta
if db != nil {
if ufm = db.Get(subSavePath); ufm.Size == fi.Size() && ufm.ModTime == fi.ModTime().Unix() {
logger.Verbosef("文件未修改跳过:%s\n", file)
return nil
}
// 插件回调
ft := "file"
if fi.IsDir() {
ft = "folder"
}
if fi.IsDir() { // 备份目录处理
if strings.HasPrefix(fi.Name(), BackupMetaDirName) {
pluginParam := &plugins.UploadFilePrepareParams{
LocalFilePath: file.LogicPath,
LocalFileName: fi.Name(),
LocalFileSize: fi.Size(),
LocalFileType: ft,
LocalFileUpdatedAt: fi.ModTime().Format("2006-01-02 15:04:05"),
DriveId: activeUser.ActiveDriveId,
DriveFilePath: strings.TrimPrefix(strings.TrimPrefix(subSavePath, savePath), "/"),
}
if uploadFilePrepareResult, er := plugin.UploadFilePrepareCallback(plugins.GetContext(activeUser), pluginParam); er == nil && uploadFilePrepareResult != nil {
if strings.Compare("yes", uploadFilePrepareResult.UploadApproved) != 0 {
// skip upload this file
logf("插件禁止该文件上传: %s\n", file.LogicPath)
return filepath.SkipDir
}
//不存在同步数据库时跳过
if db == nil || ufm.FileId != "" {
return nil
if uploadFilePrepareResult.DriveFilePath != "" {
targetSavePanRelativePath := strings.TrimPrefix(uploadFilePrepareResult.DriveFilePath, "/")
subSavePath = path.Clean(savePath + aliyunpan.PathSeparator + targetSavePanRelativePath)
logf("插件修改文件网盘保存路径为: %s\n", subSavePath)
}
panClient := activeUser.PanClient()
fmt.Println(subSavePath, "云盘文件夹预创建")
//首先尝试直接创建文件夹
if ufm = db.Get(path.Dir(subSavePath)); ufm.IsFolder == true && ufm.FileId != "" {
rs, err := panClient.Mkdir(opt.DriveId, ufm.FileId, fi.Name())
if err == nil && rs != nil && rs.FileId != "" {
db.Put(subSavePath, &panupload.UploadedFileMeta{FileId: rs.FileId, IsFolder: true, ModTime: fi.ModTime().Unix(), ParentId: rs.ParentFileId})
return nil
}
}
rs, err := panClient.MkdirRecursive(opt.DriveId, "", "", 0, strings.Split(path.Clean(subSavePath), "/"))
if err == nil && rs != nil && rs.FileId != "" {
db.Put(subSavePath, &panupload.UploadedFileMeta{FileId: rs.FileId, IsFolder: true, ModTime: fi.ModTime().Unix(), ParentId: rs.ParentFileId})
return nil
}
fmt.Println(subSavePath, "创建云盘文件夹失败", err)
return filepath.SkipDir
}
taskinfo := executor.Append(&panupload.UploadTaskUnit{
LocalFileChecksum: localfile.NewLocalFileEntity(file),
SavePath: subSavePath,
DriveId: opt.DriveId,
PanClient: activeUser.PanClient(),
UploadingDatabase: uploadDatabase,
FolderCreateMutex: folderCreateMutex,
Parallel: opt.Parallel,
NoRapidUpload: opt.NoRapidUpload,
BlockSize: opt.BlockSize,
UploadStatistic: statistic,
ShowProgress: opt.ShowProgress,
IsOverwrite: opt.IsOverwrite,
FolderSyncDb: db,
UseInternalUrl: opt.UseInternalUrl,
}, opt.MaxRetry)
fmt.Printf("%s [%s] 加入上传队列: %s\n", time.Now().Format("2006-01-02 15:04:05"), taskinfo.Id(), file)
// 创建对应的文件上传任务
// 上传里面的文件会创建对应的缺失文件夹
if !fi.IsDir() {
taskinfo := executor.Append(&panupload.UploadTaskUnit{
LocalFileChecksum: localfile.NewLocalSymlinkFileEntity(file),
SavePath: subSavePath,
DriveId: opt.DriveId,
PanClient: activeUser.PanClient(),
UploadingDatabase: uploadDatabase,
FolderCreateMutex: folderCreateMutex,
Parallel: opt.Parallel,
NoRapidUpload: opt.NoRapidUpload,
BlockSize: opt.BlockSize,
UploadStatistic: statistic,
ShowProgress: opt.ShowProgress,
IsOverwrite: opt.IsOverwrite,
IsSkipSameName: opt.IsSkipSameName,
GlobalSpeedsStat: globalSpeedsStat,
FileRecorder: fileRecorder,
UI: dashboard,
}, opt.MaxRetry)
logf("[%s] 加入上传队列: %s\n", taskinfo.Id(), file.LogicPath)
if dashboard != nil {
dashboard.RegisterTask(taskinfo.Id(), file.LogicPath, fi.Size(), !fi.IsDir())
}
} else {
// 创建文件夹
// 这样空文件夹也可以正确上传
saveFilePath := subSavePath
if saveFilePath != "/" {
folderCreateMutex.Lock()
logf("正在检测和创建云盘文件夹: %s\n", saveFilePath)
_, apierr1 := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(opt.DriveId, saveFilePath)
time.Sleep(1 * time.Second)
if apierr1 != nil && apierr1.Code == apierror.ApiCodeFileNotFoundCode {
logger.Verbosef("%s 创建云盘文件夹: %s\n", utils.NowTimeStr(), saveFilePath)
rs, apierr := activeUser.PanClient().OpenapiPanClient().MkdirByFullPath(opt.DriveId, saveFilePath)
if apierr != nil || rs.FileId == "" {
logf("创建云盘文件夹失败: %s\n", saveFilePath)
}
}
folderCreateMutex.Unlock()
}
}
return nil
}
if err := WalkAllFile(curPath, walkFunc); err != nil {
fmt.Printf("警告: 遍历错误: %s\n", err)
file := localfile.NewSymlinkFile(curPath)
if err = localfile.WalkAllFile(file, walkFunc); err != nil {
if err != filepath.SkipDir {
logf("警告: 遍历错误: %s\n", err)
}
}
}
// 执行上传任务
var failedList []*lane.Deque
// 启动UI面板显示
if dashboard != nil {
dashboard.Start()
}
executor.Execute()
failed := executor.FailedDeque()
if failed.Size() > 0 {
failedList = append(failedList, failed)
}
// 关闭UI面板
if dashboard != nil {
dashboard.Close()
}
fmt.Printf("\n")
fmt.Printf("上传结束, 时间: %s, 总大小: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize()))
fmt.Printf("上传结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
// 输出上传失败的文件列表
for _, failed := range failedList {
@@ -439,170 +474,10 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
tb := cmdtable.NewTable(os.Stdout)
for e := failed.Shift(); e != nil; e = failed.Shift() {
item := e.(*taskframework.TaskInfoItem)
tb.Append([]string{item.Info.Id(), item.Unit.(*panupload.UploadTaskUnit).LocalFileChecksum.Path})
tb.Append([]string{item.Info.Id(), item.Unit.(*panupload.UploadTaskUnit).LocalFileChecksum.Path.LogicPath})
}
tb.Render()
}
}
activeUser.DeleteCache(GetAllPathFolderByPath(savePath))
}
// 是否是排除上传的文件
func isExcludeFile(filePath string, opt *UploadOptions) bool {
if opt == nil || len(opt.ExcludeNames) == 0{
return false
}
for _,pattern := range opt.ExcludeNames {
fileName := path.Base(filePath)
m,_ := regexp.MatchString(pattern, fileName)
if m {
return true
}
}
return false
}
func WalkAllFile(dirPath string, walkFn filepath.WalkFunc) error {
info, err := os.Lstat(dirPath)
if err != nil {
err = walkFn(dirPath, nil, err)
} else {
err = walkAllFile(dirPath, info, walkFn)
}
return err
}
func walkAllFile(dirPath string, info os.FileInfo, walkFn filepath.WalkFunc) error {
if !info.IsDir() {
return walkFn(dirPath, info, nil)
}
files, err := ioutil.ReadDir(dirPath)
if err != nil {
return walkFn(dirPath, nil, err)
}
for _, fi := range files {
subFilePath := dirPath + "/" + fi.Name()
err := walkFn(subFilePath, fi, err)
if err != nil && err != filepath.SkipDir {
return err
}
if fi.IsDir() {
if err == filepath.SkipDir {
continue
}
err := walkAllFile(subFilePath, fi, walkFn)
if err != nil {
return err
}
}
}
return nil
}
// RunRapidUpload 秒传
func RunRapidUpload(driveId string, isOverwrite bool, fileMetaList []string, savePanPath string) {
activeUser := GetActiveUser()
savePanPath = activeUser.PathJoin(driveId, savePanPath)
if len(fileMetaList) == 0 {
fmt.Println("秒传链接为空")
return
}
items := []*RapidUploadItem{}
// parse file meta strings
for _,fileMeta := range fileMetaList {
item,e := newRapidUploadItem(fileMeta)
if e != nil {
fmt.Println(e)
continue
}
if item == nil {
fmt.Println("秒传链接格式错误: ", fileMeta)
continue
}
// pan path
item.FilePath = path.Join(savePanPath, item.FilePath)
// append
items = append(items, item)
}
// upload one by one
for _,item := range items {
fmt.Println("准备秒传:", item.FilePath)
if ee := doRapidUpload(driveId, isOverwrite, item); ee != nil {
fmt.Println(ee)
} else {
fmt.Printf("秒传成功, 保存到网盘路径:%s\n", item.FilePath)
}
}
}
func doRapidUpload(driveId string, isOverwrite bool, item *RapidUploadItem) error {
activeUser := GetActiveUser()
panClient := activeUser.PanClient()
var apierr *apierror.ApiError
var rs *aliyunpan.MkdirResult
var appCreateUploadFileParam *aliyunpan.CreateFileUploadParam
var saveFilePath string
panDir, panFileName := path.Split(item.FilePath)
saveFilePath = item.FilePath
if panDir != "/" {
rs, apierr = panClient.MkdirRecursive(driveId, "", "", 0, strings.Split(path.Clean(panDir), "/"))
if apierr != nil || rs.FileId == "" {
return fmt.Errorf("创建云盘文件夹失败")
}
} else {
rs = &aliyunpan.MkdirResult{}
rs.FileId = aliyunpan.DefaultRootParentFileId
}
time.Sleep(time.Duration(2) * time.Second)
if isOverwrite {
// 标记覆盖旧同名文件
// 检查同名文件是否存在
efi, apierr := panClient.FileInfoByPath(driveId, saveFilePath)
if apierr != nil && apierr.Code != apierror.ApiCodeFileNotFoundCode {
return fmt.Errorf("检测同名文件失败,请稍后重试")
}
if efi != nil && efi.FileId != "" {
// existed, delete it
fileDeleteResult, err1 := panClient.FileDelete([]*aliyunpan.FileBatchActionParam{{DriveId:efi.DriveId, FileId:efi.FileId}})
if err1 != nil || len(fileDeleteResult) == 0 {
return fmt.Errorf("无法删除文件,请稍后重试")
}
time.Sleep(time.Duration(500) * time.Millisecond)
if fileDeleteResult[0].Success {
logger.Verboseln("检测到同名文件,已移动到回收站: ", saveFilePath)
} else {
return fmt.Errorf("无法删除文件,请稍后重试")
}
}
}
appCreateUploadFileParam = &aliyunpan.CreateFileUploadParam{
DriveId: driveId,
Name: panFileName,
Size: item.FileSize,
ContentHash: item.FileSha1,
ParentFileId: rs.FileId,
}
uploadOpEntity, apierr := panClient.CreateUploadFile(appCreateUploadFileParam)
if apierr != nil {
return fmt.Errorf("创建秒传任务失败:" + apierr.Error())
}
if uploadOpEntity.RapidUpload {
logger.Verboseln("秒传成功, 保存到网盘路径: ", path.Join(panDir, uploadOpEntity.FileName))
} else {
return fmt.Errorf("失败,文件未曾上传,无法秒传")
}
return nil
}
+18 -17
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,10 +15,10 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
"os"
"strconv"
)
@@ -28,7 +28,7 @@ func CmdLoglist() cli.Command {
Usage: "列出帐号列表",
Description: "列出所有已登录的阿里账号",
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
fmt.Println(config.Config.UserList.String())
return nil
@@ -46,11 +46,11 @@ func CmdSu() cli.Command {
示例:
aliyunpan su
aliyunpan su <uid or name>
aliyunpan su <uid>
`,
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() >= 2 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -84,8 +84,8 @@ func CmdSu() cli.Command {
return nil
}
if n, err := strconv.Atoi(index); err == nil && n >= 0 && n < numLogins {
uid = config.Config.UserList[n].UserId
if n, err1 := strconv.Atoi(index); err1 == nil && (n-1) >= 0 && (n-1) < numLogins {
uid = config.Config.UserList[n-1].UserId
} else {
fmt.Printf("切换用户失败, 请检查 # 值是否正确\n")
return nil
@@ -94,14 +94,14 @@ func CmdSu() cli.Command {
cli.ShowCommandHelp(c, c.Command.Name)
}
switchedUser, err := config.Config.SwitchUser(uid, inputData)
switchedUser, err := config.Config.SwitchUser(uid)
if err != nil {
fmt.Printf("切换用户失败, %s\n", err)
return nil
}
if switchedUser == nil {
switchedUser = cmder.TryLogin()
switchedUser = TryLogin()
}
if switchedUser != nil {
@@ -121,20 +121,21 @@ func CmdWho() cli.Command {
Usage: "获取当前帐号",
Description: "获取当前帐号的信息",
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
os.Exit(1)
}
activeUser := config.Config.ActiveUser()
cloudName := activeUser.GetDriveById(activeUser.ActiveDriveId).DriveName
fmt.Printf("当前帐号UID: %s, 昵称: %s, 用户名: %s, 网盘:%s\n", activeUser.UserId, activeUser.Nickname, activeUser.AccountName, cloudName)
user, _ := GetActivePanClient().OpenapiPanClient().GetUserInfo()
thirdParty := "未开通"
if user.ThirdPartyVip {
thirdParty = "已开通(" + user.ThirdPartyVipExpire + ")"
}
fmt.Printf("当前帐号UID: %s, 昵称: %s, 三方权益包: %s, 当前使用网盘:%s\n", activeUser.UserId, activeUser.Nickname, thirdParty, cloudName)
return nil
},
}
}
func RunGetUserInfo() (userInfo *aliyunpan.UserInfo, error error) {
return GetActivePanClient().GetUserInfo()
}
+360 -40
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,65 +16,76 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"math"
"math/rand"
"net/url"
"path"
"path/filepath"
"strings"
"sync"
"time"
)
var (
panCommandVerbose = logger.New("PANCOMMAND", config.EnvVerbose)
)
const(
// 备份数据库桶分区标志
BackupMetaBucketName = "adrive"
saveConfigMutex *sync.Mutex = new(sync.Mutex)
// 备份数据文件夹目录名称,隐藏目录
BackupMetaDirName = ".adrive"
)
// GetFileInfoByPaths 获取指定文件路径的文件详情信息
func GetAppFileInfoByPaths(driveId string, paths ...string) (fileInfoList []*aliyunpan.FileEntity, failedPaths []string, error error) {
if len(paths) <= 0 {
return nil, nil, fmt.Errorf("请指定文件路径")
}
activeUser := GetActiveUser()
for idx := 0; idx < len(paths); idx++ {
absolutePath := path.Clean(activeUser.PathJoin(driveId, paths[idx]))
fe, err := activeUser.PanClient().FileInfoByPath(driveId, absolutePath)
ReloadConfigFunc = func(c *cli.Context) error {
err := config.Config.Reload()
if err != nil {
failedPaths = append(failedPaths, absolutePath)
continue
fmt.Printf("重载配置错误: %s\n", err)
}
fileInfoList = append(fileInfoList, fe)
return nil
}
SaveConfigFunc = func(c *cli.Context) error {
saveConfigMutex.Lock()
defer saveConfigMutex.Unlock()
err := config.Config.Save()
if err != nil {
fmt.Printf("保存配置错误: %s\n", err)
}
return nil
}
)
// RunTestShellPattern 执行测试通配符
func RunTestShellPattern(driveId string, pattern string) {
acUser := GetActiveUser()
files, err := acUser.PanClient().OpenapiPanClient().MatchPathByShellPattern(driveId, GetActiveUser().PathJoin(driveId, pattern))
if err != nil {
fmt.Println(err)
return
}
for _, f := range *files {
fmt.Printf("%s\n", f.Path)
}
return
}
// GetFileInfoByPaths 获取指定文件路径的文件详情信息
func GetFileInfoByPaths(paths ...string) (fileInfoList []*aliyunpan.FileEntity, failedPaths []string, error error) {
if len(paths) <= 0 {
return nil, nil, fmt.Errorf("请指定文件路径")
}
activeUser := GetActiveUser()
for idx := 0; idx < len(paths); idx++ {
absolutePath := path.Clean(activeUser.PathJoin(activeUser.ActiveDriveId, paths[idx]))
fe, err := activeUser.PanClient().FileInfoByPath(activeUser.ActiveDriveId, absolutePath)
// matchPathByShellPattern 通配符匹配路径,允许返回多个匹配结果
func matchPathByShellPattern(driveId string, patterns ...string) (files []*aliyunpan.FileEntity, e error) {
acUser := GetActiveUser()
for k := range patterns {
ps, err := acUser.PanClient().OpenapiPanClient().MatchPathByShellPattern(driveId, acUser.PathJoin(driveId, patterns[k]))
if err != nil {
failedPaths = append(failedPaths, absolutePath)
continue
return nil, err
}
fileInfoList = append(fileInfoList, fe)
files = append(files, *ps...)
}
return
return files, nil
}
func matchPathByShellPattern(driveId string, patterns ...string) (panpaths []string, err error) {
// makePathAbsolute 拼接路径,确定返回路径为绝对路径
func makePathAbsolute(driveId string, patterns ...string) (panpaths []string, err error) {
acUser := GetActiveUser()
for k := range patterns {
ps := acUser.PathJoin(driveId, patterns[k])
@@ -88,7 +99,7 @@ func RandomStr(count int) string {
STR_SET := "abcdefjhijklmnopqrstuvwxyz1234567890"
rand.Seed(time.Now().UnixNano())
str := strings.Builder{}
for i := 0; i < count; i++ {
for i := 0; i < count; i++ {
str.WriteByte(byte(STR_SET[rand.Intn(len(STR_SET))]))
}
return str.String()
@@ -99,9 +110,318 @@ func GetAllPathFolderByPath(pathStr string) []string {
dirs := []string{}
p := "/"
dirs = append(dirs, p)
for _,s := range dirNames {
for _, s := range dirNames {
p = path.Join(p, s)
dirs = append(dirs, p)
}
return dirs
}
}
// EscapeStr 转义字符串
func EscapeStr(s string) string {
return url.PathEscape(s)
}
// UnescapeStr 反转义字符串
func UnescapeStr(s string) string {
r, _ := url.PathUnescape(s)
return r
}
func NewWebLoginToken(accessToken string, expired int64) aliyunpan_web.WebLoginToken {
webapiToken := &config.PanClientToken{
AccessToken: accessToken,
Expired: expired,
}
return aliyunpan_web.WebLoginToken{
AccessTokenType: "Bearer",
AccessToken: webapiToken.AccessToken,
RefreshToken: "",
ExpiresIn: 7200,
ExpireTime: webapiToken.GetExpiredTimeCstStr(),
}
}
// RefreshWebTokenInNeed 刷新 webapi access token
func RefreshWebTokenInNeed(activeUser *config.PanUser) *panlogin.CommonResultError {
if activeUser == nil {
return panlogin.NewCommonResultError("activeUser is nil")
}
// refresh expired web token
if activeUser.PanClient().WebapiPanClient() != nil {
if activeUser.WebapiToken != nil && len(activeUser.WebapiToken.AccessToken) > 0 {
cz := time.FixedZone("CST", 8*3600) // 东8区
expiredTime := time.Unix(activeUser.WebapiToken.Expired, 0).In(cz)
now := time.Now()
if (expiredTime.Unix() - now.Unix()) <= (10 * 60) { // 有效期小于10min就刷新
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
plugin, _ := pluginManger.GetPlugin()
params := &plugins.UserTokenRefreshFinishParams{
Result: "success",
Message: "webapi",
OldToken: "",
NewToken: "",
UpdatedAt: utils.NowTimeStr(),
}
// need update refresh token
logger.Verboseln("web access token expired, get new from server")
loginHelper := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
wt, re := loginHelper.GetWebapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().WebapiPanClient().GetAccessToken())
if re != nil {
logger.Verboseln("get web token from server error: ", re.Msg)
if re.Code != panlogin.SUCCESS {
return re
}
}
if wt != nil {
params.Result = "success"
params.OldToken = activeUser.WebapiToken.AccessToken
params.NewToken = wt.AccessToken
// update for user & client
userWebToken := NewWebLoginToken(wt.AccessToken, wt.Expired)
activeUser.WebapiToken = &config.PanClientToken{
AccessToken: wt.AccessToken,
Expired: wt.Expired,
}
activeUser.PanClient().WebapiPanClient().UpdateToken(userWebToken)
logger.Verboseln("get new access token success")
// plugin callback
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
}
// create new signature
_, e1 := activeUser.PanClient().WebapiPanClient().CreateSession(nil)
if e1 != nil {
logger.Verboseln("call CreateSession error in RefreshWebTokenInNeed: " + e1.Error())
}
return &panlogin.CommonResultError{
Code: 0,
Msg: "",
}
} else {
// token refresh error
// if token has expired, callback plugin api for notify
if now.Unix() >= expiredTime.Unix() {
params.Result = "fail"
params.Message = re.Msg
params.OldToken = activeUser.WebapiToken.AccessToken
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
}
}
}
} else {
return &panlogin.CommonResultError{
Code: 0,
Msg: "web token is validate, not need to refresh",
}
}
}
}
return panlogin.NewCommonResultError("activeUser is invalidate")
}
// RefreshOpenTokenInNeed 刷新 openapi access token
func RefreshOpenTokenInNeed(activeUser *config.PanUser) *panlogin.CommonResultError {
if activeUser == nil {
return panlogin.NewCommonResultError("activeUser is nil")
}
// OpenAPI的token只有2个小时有效期,需要在token过期之前进行刷新并把获取到的新token存储在本地
// refresh expired openapi token
if activeUser.PanClient().OpenapiPanClient() != nil {
if activeUser.OpenapiToken != nil && len(activeUser.OpenapiToken.AccessToken) > 0 {
cz := time.FixedZone("CST", 8*3600) // 东8区
expiredTime := time.Unix(activeUser.OpenapiToken.Expired, 0).In(cz)
now := time.Now().In(cz)
if (expiredTime.Unix() - now.Unix()) <= (10 * 60) { // 有效期小于10min就刷新
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
plugin, _ := pluginManger.GetPlugin()
params := &plugins.UserTokenRefreshFinishParams{
Result: "success",
Message: "openapi",
OldToken: "",
NewToken: "",
UpdatedAt: utils.NowTimeStr(),
}
// need update refresh token
logger.Verboseln("openapi access token expired, get new from server")
loginHelper := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
wt, re := loginHelper.GetOpenapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().OpenapiPanClient().GetAccessToken())
if re != nil {
logger.Verboseln("get openapi token from server error: ", re.Msg)
if re.Code != panlogin.SUCCESS {
return re
}
}
if wt != nil {
// 存储新token到本地
params.Result = "success"
params.OldToken = activeUser.OpenapiToken.AccessToken
params.NewToken = wt.AccessToken
// update for user
activeUser.OpenapiToken = &config.PanClientToken{
AccessToken: wt.AccessToken,
Expired: wt.Expired,
}
logger.Verboseln("get new access token success")
// plugin callback
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
}
return &panlogin.CommonResultError{
Code: 0,
Msg: "",
}
} else {
// token refresh error
// if token has expired, callback plugin api for notify
if now.Unix() >= expiredTime.Unix() {
params.Result = "fail"
params.Message = re.Msg
params.OldToken = activeUser.OpenapiToken.AccessToken
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
}
}
}
} else {
return &panlogin.CommonResultError{
Code: 0,
Msg: "openapi token is validate, not need to refresh",
}
}
}
}
return panlogin.NewCommonResultError("activeUser is invalidate")
}
// AutomaticallyRefreshTokenTask 自动刷新Token的后台任务
func AutomaticallyRefreshTokenTask() {
go func() {
// 延迟启动自动刷新Token任务,因为主程序启动的时候默认会进行Token的初始化和刷新,避免无效的并行初始化Token
time.Sleep(time.Duration(15) * time.Minute)
// 检测延迟时间(分钟)
delayMinuteTime := time.Duration(1) * time.Minute
//delayMinuteTime := time.Duration(10) * time.Second // for test
// 请求重试次数
attempt := 1
for {
// Token刷新进程,不管是CLI命令行模式,还是直接命令模式,本刷新任务都会执行
time.Sleep(delayMinuteTime) // 定时器,每1分钟调用一次
// 重新加载配置文件
ReloadConfigFunc(nil)
activeUser := config.Config.ActiveUser()
if activeUser != nil && activeUser.UserId != "" && activeUser.PanClient() != nil {
// 刷新Openapi端Token
errResult := RefreshOpenTokenInNeed(activeUser)
attempt += 1
if errResult != nil && errResult.Code == panlogin.SUCCESS {
// 刷新成功,重置定时器
attempt = 1
delayMinuteTime = time.Duration(1) * time.Minute
// 刷新Webapi端Token
RefreshWebTokenInNeed(activeUser)
// 保存新token到配置文件
SaveConfigFunc(nil)
} else {
// 刷新Token失败
logger.Verboseln("刷新Token失败:", errResult.Msg)
if errResult.Code == panlogin.ERROR_NEED_LOGIN_AGAIN {
// token已经失效,需要重新登录
logger.Verboseln("token已经失效,需要重新登录,退出自动刷新Token任务")
return
} else if errResult.Code == panlogin.ERROR {
// 访问错误
delayMinuteTime = ProgressiveBackoffAlg(attempt)
} else if errResult.Code == panlogin.ERROR_TOO_MANY_REQUESTS {
// 请求过于频繁
delayMinuteTime = ProgressiveBackoffAlg(attempt)
}
}
}
}
}()
}
// ProgressiveBackoffAlg 指数退降算法
func ProgressiveBackoffAlg(attempt int) time.Duration {
maxRetries := 120
baseDelay := float64(1)
maxDelay := float64(1440)
if attempt >= maxRetries {
return time.Duration(maxDelay) * time.Minute
}
// 计算当前延迟
var delay float64
if attempt < 100 {
// 前 100 次:线性增长 + 小指数基数(缓慢增加)
delay = baseDelay * (1 + float64(attempt)/5) // 调整分母控制增长速度
} else {
// 100 次后:标准指数退避
delay = baseDelay * math.Pow(1.5, float64(attempt-100))
if delay > maxDelay {
delay = maxDelay
}
}
// 添加随机抖动(最多 20% 的扰动)
jitterSec := rand.Float64() * delay * 0.2 // ±20% 随机扰动
delay += jitterSec
return time.Duration(delay * float64(time.Minute))
}
func isIncludeFile(pattern string, fileName string) bool {
b, er := filepath.Match(pattern, fileName)
if er != nil {
return false
}
return b
}
// isMatchWildcardPattern 是否是统配符字符串
func isMatchWildcardPattern(name string) bool {
return strings.ContainsAny(name, "*") || strings.ContainsAny(name, "?") || strings.ContainsAny(name, "[")
}
// TryLogin 尝试登录,基础应用支持的各类登录方式进行尝试成功登录
func TryLogin() *config.PanUser {
// can do automatically login?
for _, u := range config.Config.UserList {
if u.UserId == config.Config.ActiveUID {
// login
cloudUser, err := config.SetupUserByCookie(u.OpenapiToken, u.WebapiToken,
u.TicketId, u.UserId,
config.Config.DeviceId, config.Config.DeviceName,
config.Config.ClientId, config.Config.ClientSecret)
if cloudUser == nil {
logger.Verboseln("尝试登录失败: ", err)
fmt.Println("尝试登录失败,请使用 login 命令进行重新登录")
return nil
}
// save
SaveConfigFunc(nil)
// reload
ReloadConfigFunc(nil)
return config.Config.ActiveUser()
}
}
return nil
}
+13
View File
@@ -0,0 +1,13 @@
package command
import (
"fmt"
"testing"
)
func TestProgressiveBackoffAlg(t *testing.T) {
for attempt := 1; attempt <= 150; attempt++ {
delayTime := ProgressiveBackoffAlg(attempt)
fmt.Printf("第%d次,延迟分钟: %.2f\n", attempt, delayTime.Minutes())
}
}
+183
View File
@@ -0,0 +1,183 @@
// Copyright (c) 2020 tickstep.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
"os"
"path"
"strconv"
)
func CmdXcp() cli.Command {
return cli.Command{
Name: "xcp",
Usage: "备份盘和资源库之间转存文件",
UsageText: `
aliyunpan xcp <文件/目录1> <文件/目录2> <文件/目录3> ... <目标盘目录>`,
Description: `
注意: 拷贝多个文件和目录时, 请确保每一个文件和目录都存在, 否则拷贝操作会失败。
示例:
当前程序工作在备份盘下,将备份盘 /1.mp4 文件转存复制到 资源库下的 /来自备份盘/video 目录下
aliyunpan xcp /1.mp4 /来自备份盘/video
当前程序工作在资源库下,将资源库 /1.mp4 文件转存复制到 备份盘下的 /来自资源库/video 目录下
aliyunpan xcp /1.mp4 /来自资源库/video
当前程序工作在备份盘下,将 /我的资源 目录下所有的.mp4文件 复制到 /来自备份盘/video 目录下面,使用通配符匹配
aliyunpan xcp /我的资源/*.mp4 /来自备份盘/video
`,
Category: "阿里云盘",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() <= 0 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
return nil
}
srcDriveId := parseDriveId(c)
dstDriveId := ""
driveList := config.Config.ActiveUser().DriveList
if driveList.GetFileDriveId() == srcDriveId {
dstDriveId = driveList.GetResourceDriveId()
} else if driveList.GetResourceDriveId() == srcDriveId {
dstDriveId = driveList.GetFileDriveId()
} else {
fmt.Println("不支持该操作")
return nil
}
RunXCopy(srcDriveId, dstDriveId, c.Args()...)
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "driveId",
Usage: "源网盘ID",
Value: "",
},
},
}
}
// RunXCopy 执行复制文件/目录
func RunXCopy(srcDriveId, dstDriveId string, paths ...string) {
activeUser := GetActiveUser()
cacheCleanPaths := []string{}
opFileList, targetFile, _, err := getCrossCopyFileInfo(srcDriveId, dstDriveId, paths...)
if err != nil {
fmt.Println(err)
return
}
if targetFile == nil {
fmt.Println("目标目录不存在")
return
}
if opFileList == nil || len(opFileList) == 0 {
fmt.Println("没有有效的文件可复制")
return
}
cacheCleanPaths = append(cacheCleanPaths, targetFile.Path)
failedCopyFiles := []*aliyunpan.FileEntity{}
copyFileParamList := []string{}
fileId2FileEntity := map[string]*aliyunpan.FileEntity{}
for _, mfi := range opFileList {
fileId2FileEntity[mfi.FileId] = mfi
copyFileParamList = append(copyFileParamList, mfi.FileId)
cacheCleanPaths = append(cacheCleanPaths, path.Dir(mfi.Path))
}
fccr, er := activeUser.PanClient().WebapiPanClient().FileCrossDriveCopy(&aliyunpan_web.FileCrossCopyParam{
FromDriveId: srcDriveId,
FromFileIds: copyFileParamList,
ToDriveId: dstDriveId,
ToParentFileId: targetFile.FileId,
})
for _, rs := range fccr {
if rs.Status != 201 {
failedCopyFiles = append(failedCopyFiles, fileId2FileEntity[rs.SourceFileId])
}
}
if len(failedCopyFiles) > 0 {
fmt.Println("以下文件复制失败:")
for _, f := range failedCopyFiles {
fmt.Println(f.FileName)
}
fmt.Println("")
}
if er == nil {
pnt := func() {
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "文件/目录"})
for k, rs := range fccr {
tb.Append([]string{strconv.Itoa(k + 1), fileId2FileEntity[rs.SourceFileId].Path})
}
tb.Render()
}
fmt.Println("操作成功, 以下文件已复制到目标目录: ", targetFile.Path)
pnt()
activeUser.DeleteCache(cacheCleanPaths)
} else {
fmt.Println("无法复制文件,请稍后重试")
}
}
func getCrossCopyFileInfo(srcDriveId, dstDriveId string, paths ...string) (opFileList []*aliyunpan.FileEntity, targetFile *aliyunpan.FileEntity, failedPaths []string, error error) {
if len(paths) <= 1 {
return nil, nil, nil, fmt.Errorf("请指定目标文件夹路径")
}
activeUser := GetActiveUser()
// the last one is the target file path
targetFilePath := path.Clean(paths[len(paths)-1])
absolutePath := activeUser.PathJoin(dstDriveId, targetFilePath)
targetFile, err := activeUser.PanClient().WebapiPanClient().FileInfoByPath(dstDriveId, absolutePath)
if err != nil || !targetFile.IsFolder() {
return nil, nil, nil, fmt.Errorf("指定目标文件夹不存在")
}
// source file list
for idx := 0; idx < (len(paths) - 1); idx++ {
absolutePath = path.Clean(activeUser.PathJoin(srcDriveId, paths[idx]))
fileList, err1 := matchPathByShellPattern(srcDriveId, absolutePath)
if err1 != nil {
failedPaths = append(failedPaths, absolutePath)
continue
}
if fileList == nil || len(fileList) == 0 {
// 文件不存在
failedPaths = append(failedPaths, absolutePath)
continue
}
for _, f := range fileList {
// 移动匹配的文件
opFileList = append(opFileList, f)
}
}
return
}
+99
View File
@@ -0,0 +1,99 @@
// Copyright (c) 2020 tickstep.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command_local
import (
"fmt"
"github.com/tickstep/aliyunpan/internal/command"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
"os"
"runtime"
"strings"
)
// CmdLocalCd 切换本地工作目录
func CmdLocalCd() cli.Command {
return cli.Command{
Name: "lcd",
Category: "本地命令",
Usage: "切换本地工作目录",
Description: `
aliyunpan lcd <本地目录, 绝对路径或相对路径>
示例:
切换 /我的资源 工作目录:
aliyunpan lcd /我的资源
切换上级目录:
aliyunpan lcd ..
切换根目录:
aliyunpan lcd /
`,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
RunChangeLocalDirectory(c.Args().Get(0))
return nil
},
Flags: []cli.Flag{},
}
}
// CmdLocalPwd 当前本地工作目录
func CmdLocalPwd() cli.Command {
return cli.Command{
Name: "lpwd",
Usage: "输出本地工作目录",
Category: "本地命令",
Before: command.ReloadConfigFunc,
Action: func(c *cli.Context) error {
lwd := config.Config.LocalWorkdir
if lwd == "" {
// 默认为用户主页目录
lwd = GetLocalHomeDir()
config.Config.LocalWorkdir = lwd
}
if runtime.GOOS == "windows" {
lwd = strings.ReplaceAll(lwd, "/", "\\")
} else {
// Unix-like system, so just assume Unix
lwd = strings.ReplaceAll(lwd, "\\", "/")
}
fmt.Println("本地工作目录: " + lwd)
return nil
},
}
}
func RunChangeLocalDirectory(targetPath string) {
targetPath = LocalPathJoin(targetPath)
// 获取目标路径文件信息
localFileInfo, er := os.Stat(targetPath)
if er != nil {
fmt.Println("目录路径不存在")
return
}
if !localFileInfo.IsDir() {
fmt.Printf("错误: %s 不是一个目录 (文件夹)\n", targetPath)
return
}
config.Config.LocalWorkdir = strings.ReplaceAll(targetPath, "\\", "/")
fmt.Printf("改变本地工作目录: %s\n", config.Config.LocalWorkdir)
}
+216
View File
@@ -0,0 +1,216 @@
// Copyright (c) 2020 tickstep.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command_local
import (
"fmt"
"github.com/olekukonko/tablewriter"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/library-go/converter"
"github.com/urfave/cli"
"log"
"os"
"sort"
"strconv"
"strings"
)
func CmdLocalLs() cli.Command {
return cli.Command{
Name: "lls",
Category: "本地命令",
Usage: "列出本地目录",
Description: `
列出当前本地工作目录内的文件和目录, 或指定目录内的文件和目录
示例:
列出 我的资源 内的文件和目录
aliyunpan lls 我的资源
绝对路径
aliyunpan lls /我的资源
详细列出 我的资源 内的文件和目录
aliyunpan lls /我的资源
`,
Action: func(c *cli.Context) error {
var (
orderBy aliyunpan.FileOrderBy = aliyunpan.FileOrderByUpdatedAt
orderSort aliyunpan.FileOrderDirection = aliyunpan.FileOrderDirectionDesc
showAllFile bool = false
)
if c.IsSet("a") {
showAllFile = true
}
switch {
case c.IsSet("asc"):
orderSort = aliyunpan.FileOrderDirectionAsc
case c.IsSet("desc"):
orderSort = aliyunpan.FileOrderDirectionDesc
default:
orderSort = aliyunpan.FileOrderDirectionDesc
}
switch {
case c.IsSet("time"):
orderBy = aliyunpan.FileOrderByUpdatedAt
case c.IsSet("name"):
orderBy = aliyunpan.FileOrderByName
case c.IsSet("size"):
orderBy = aliyunpan.FileOrderBySize
default:
orderBy = aliyunpan.FileOrderByUpdatedAt
}
RunLocalLs(c.Args().Get(0), showAllFile, orderBy, orderSort)
return nil
},
Flags: []cli.Flag{
cli.BoolFlag{
Name: "a",
Usage: "显示所有文件",
},
cli.BoolFlag{
Name: "asc",
Usage: "升序排序",
},
cli.BoolFlag{
Name: "desc",
Usage: "降序排序",
},
cli.BoolFlag{
Name: "time",
Usage: "根据修改时间排序",
},
cli.BoolFlag{
Name: "name",
Usage: "根据文件名排序",
},
cli.BoolFlag{
Name: "size",
Usage: "根据大小排序",
},
},
}
}
func RunLocalLs(targetPath string,
showAllFile bool,
orderBy aliyunpan.FileOrderBy, orderDirection aliyunpan.FileOrderDirection) {
targetPath = LocalPathJoin(targetPath)
// 获取目标路径文件信息
localFileInfo, er := os.Stat(targetPath)
if er != nil {
fmt.Println("目录路径不存在")
return
}
if !localFileInfo.IsDir() {
fmt.Println("指定的路径不是目录")
return
}
fileEntryList, err := os.ReadDir(targetPath)
if err != nil {
log.Fatal(err)
}
fileInfoList := []os.FileInfo{}
folderInfoList := []os.FileInfo{}
for _, entry := range fileEntryList {
if fi, e := entry.Info(); e == nil {
if strings.HasPrefix(fi.Name(), ".") {
if !showAllFile {
continue
}
}
if fi.IsDir() {
folderInfoList = append(folderInfoList, fi)
} else {
fileInfoList = append(fileInfoList, fi)
}
}
}
// 过滤
// 排序
sortFileInfoList(folderInfoList, orderBy, orderDirection)
sortFileInfoList(fileInfoList, orderBy, orderDirection)
// 合并并渲染列表
renderFileList := append(folderInfoList, fileInfoList...)
renderTable(targetPath, renderFileList)
}
func sortFileInfoList(fileInfoList []os.FileInfo,
orderBy aliyunpan.FileOrderBy, orderDirection aliyunpan.FileOrderDirection) []os.FileInfo {
switch orderBy {
case aliyunpan.FileOrderByUpdatedAt: // 基于修改时间排序
sort.Slice(fileInfoList, func(i, j int) bool {
if orderDirection == aliyunpan.FileOrderDirectionAsc {
return fileInfoList[i].ModTime().UnixMilli() < fileInfoList[j].ModTime().UnixMilli()
} else {
return fileInfoList[i].ModTime().UnixMilli() > fileInfoList[j].ModTime().UnixMilli()
}
})
break
case aliyunpan.FileOrderByName: // 基于文件名排序
sort.Slice(fileInfoList, func(i, j int) bool {
if orderDirection == aliyunpan.FileOrderDirectionAsc {
return fileInfoList[i].Name() < fileInfoList[j].Name()
} else {
return fileInfoList[i].Name() > fileInfoList[j].Name()
}
})
break
case aliyunpan.FileOrderBySize: // 基于文件大小排序
sort.Slice(fileInfoList, func(i, j int) bool {
if orderDirection == aliyunpan.FileOrderDirectionAsc {
return fileInfoList[i].Size() < fileInfoList[j].Size()
} else {
return fileInfoList[i].Size() > fileInfoList[j].Size()
}
})
break
}
return fileInfoList
}
func renderTable(path string, files []os.FileInfo) {
tb := cmdtable.NewTable(os.Stdout)
var (
fileCount, dirCount, totalSize int64
)
tb.SetHeader([]string{"#", "文件大小", "修改日期", "文件(目录)"})
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
for k, file := range files {
if file.IsDir() {
dirCount += 1
tb.Append([]string{strconv.Itoa(k + 1), "-", file.ModTime().Format("2006-01-02 15:04:05"), file.Name() + aliyunpan.PathSeparator})
continue
}
fileCount += 1
totalSize += file.Size()
tb.Append([]string{strconv.Itoa(k + 1), converter.ConvertFileSize(file.Size(), 2), file.ModTime().Format("2006-01-02 15:04:05"), file.Name()})
}
tb.Append([]string{"", "总: " + converter.ConvertFileSize(totalSize, 2), "", fmt.Sprintf("文件总数: %d, 目录总数: %d", fileCount, dirCount)})
fmt.Printf("\n本地当前目录: %s\n", path)
fmt.Printf("----\n")
tb.Render()
fmt.Printf("----\n")
}
+62
View File
@@ -0,0 +1,62 @@
package command_local
import (
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/library/homedir"
"os"
"path"
"path/filepath"
"strings"
)
// GetLocalHomeDir 获取本地用户主目录
func GetLocalHomeDir() string {
// 默认为用户主页目录
if hd, e := os.UserHomeDir(); e == nil {
return hd
}
return ""
}
// LocalPathJoin 拼接本地路径
func LocalPathJoin(p string) string {
p = path.Clean(strings.ReplaceAll(p, "\\", "/"))
if filepath.IsAbs(p) {
return p
} else if strings.HasPrefix(p, "~") {
if d, e := homedir.Expand(p); e == nil {
return d
}
}
wd := config.Config.LocalWorkdir
if wd == "" {
wd = GetLocalHomeDir()
}
return path.Join(wd, p)
}
// LocalPathDir 获取当前文件夹的父文件夹路径
func LocalPathDir(p string) string {
p = strings.ReplaceAll(p, "\\", "/")
if strings.HasSuffix(p, ":/") { // windows卷标路径,例如:C:\ 、D:\、E:\
return p
}
parentDirPath := path.Dir(LocalPathClean(p))
return parentDirPath
}
// LocalPathBase 获取当前路径的文件(文件夹)名
func LocalPathBase(p string) string {
p = strings.ReplaceAll(p, "\\", "/")
if strings.HasSuffix(p, ":/") { // windows卷标路径,例如:C:\ 、D:\、E:\
return ""
}
baseName := path.Base(LocalPathClean(p))
return baseName
}
// LocalPathClean 规范化本地文件夹路径
func LocalPathClean(p string) string {
cleanPath := path.Clean(strings.ReplaceAll(p, "\\", "/"))
return cleanPath
}
+4 -4
View File
@@ -30,15 +30,15 @@ func (pu *PanUser) DeleteOneCache(dirPath string) {
func (pu *PanUser) CacheFilesDirectoriesList(pathStr string) (fdl aliyunpan.FileList, apiError *apierror.ApiError) {
data := pu.cacheOpMap.CacheOperation(pu.ActiveDriveId, pathStr+"_OrderByName", func() expires.DataExpires {
var fi *aliyunpan.FileEntity
fi, apiError = pu.panClient.FileInfoByPath(pu.ActiveDriveId, pathStr)
fi, apiError = pu.panClient.OpenapiPanClient().FileInfoByPath(pu.ActiveDriveId, pathStr)
if apiError != nil {
return nil
}
fileListParam := &aliyunpan.FileListParam{
DriveId: pu.ActiveDriveId,
DriveId: pu.ActiveDriveId,
ParentFileId: fi.FileId,
}
fdl, apiError = pu.panClient.FileListGetAll(fileListParam)
fdl, apiError = pu.panClient.OpenapiPanClient().FileListGetAll(fileListParam, 200)
if apiError != nil {
return nil
}
@@ -46,7 +46,7 @@ func (pu *PanUser) CacheFilesDirectoriesList(pathStr string) (fdl aliyunpan.File
for _, f := range fdl {
f.Path = path.Join(pathStr, f.FileName)
}
return expires.NewDataExpires(fdl, 1*time.Minute)
return expires.NewDataExpires(fdl, 10*time.Minute)
})
if apiError != nil {
return
+31
View File
@@ -0,0 +1,31 @@
package config
import (
"github.com/tickstep/aliyunpan-api/aliyunpan_open"
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
)
type (
// PanClient 云盘客户端
PanClient struct {
// 网页WEB接口客户端
webapiPanClient *aliyunpan_web.WebPanClient
// 阿里openapi接口客户端
openapiPanClient *aliyunpan_open.OpenPanClient
}
)
func NewPanClient(webClient *aliyunpan_web.WebPanClient, openClient *aliyunpan_open.OpenPanClient) *PanClient {
return &PanClient{
webapiPanClient: webClient,
openapiPanClient: openClient,
}
}
func (p *PanClient) WebapiPanClient() *aliyunpan_web.WebPanClient {
return p.webapiPanClient
}
func (p *PanClient) OpenapiPanClient() *aliyunpan_open.OpenPanClient {
return p.openapiPanClient
}
+251 -40
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,13 +15,17 @@ package config
import (
"fmt"
"github.com/adrg/xdg"
jsoniter "github.com/json-iterator/go"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder/cmdutil"
"github.com/tickstep/aliyunpan/cmder/cmdutil/jsonhelper"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/aliyunpan/library/homedir"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"os"
"path"
"path/filepath"
"runtime"
"strings"
@@ -29,6 +33,8 @@ import (
)
const (
// EnvDownloadDir 配置下载环境变量
EnvDownloadDir = "ALIYUNPAN_DOWNLOAD_DIR"
// EnvVerbose 启用调试环境变量
EnvVerbose = "ALIYUNPAN_VERBOSE"
// EnvConfigDir 配置路径环境变量
@@ -37,6 +43,31 @@ const (
ConfigName = "aliyunpan_config.json"
// ConfigVersion 配置文件版本
ConfigVersion string = "1.0"
// DefaultFileUploadParallelNum 默认的文件上传并发数量
DefaultFileUploadParallelNum = 10
// MaxFileUploadParallelNum 最大文件上传并发数量。过大会被阿里云盘风控,导致无法上传
MaxFileUploadParallelNum = 20
// DefaultFileDownloadParallelNum 默认的文件下载并发数量
DefaultFileDownloadParallelNum = 5
// MaxFileDownloadParallelNum 最大文件下载并发数量。过大会被阿里云盘风控,导致无法下载
MaxFileDownloadParallelNum = 3
// DefaultTokenServiceWebHost 默认的token服务
DefaultTokenServiceWebHost = "https://api.tickstep.com"
//DefaultTokenServiceWebHost = "http://localhost:8977"
// DefaultVideoFileExtensions 默认的视频文件后缀
DefaultVideoFileExtensions = "mp4,flv,mkv,mov,rm,rmvb,wmv,wma,mv,asf,asx,mpg,mpeg,mpe,3gp,m4v,avi,vob"
// DefaultDeviceName 默认客户端名称
DefaultDeviceName = "Chrome浏览器"
// DefaultClientId 默认的clientId
DefaultClientId = "cf9f70e8fc61430f8ec5ab5cadf31375"
)
var (
@@ -45,8 +76,6 @@ var (
// Config 配置信息, 由外部调用
Config = NewConfig(configFilePath)
AppVersion string
)
type UpdateCheckInfo struct {
@@ -63,20 +92,32 @@ type PanConfig struct {
UserList PanUserList `json:"userList"`
CacheSize int `json:"cacheSize"` // 下载缓存
MaxDownloadParallel int `json:"maxDownloadParallel"` // 最大下载并发量
MaxDownloadParallel int `json:"maxDownloadParallel"` // 最大下载并发量,即同时下载文件最大数量
MaxUploadParallel int `json:"maxUploadParallel"` // 最大上传并发量,即同时上传文件最大数量
MaxDownloadLoad int `json:"maxDownloadLoad"` // 同时进行下载文件的最大数量
MaxDownloadRate int64 `json:"maxDownloadRate"` // 限制最大下载速度,单位 B/s, 即字节/每秒
MaxUploadRate int64 `json:"maxUploadRate"` // 限制最大上传速度,单位 B/s, 即字节/每秒
TransferUrlType int `json:"transferUrlType"` // 上传/下载URL类别,1-默认,2-阿里云ECS
SaveDir string `json:"saveDir"` // 下载储存路径
Proxy string `json:"proxy"` // 代理
LocalAddrs string `json:"localAddrs"` // 本地网卡地址
Proxy string `json:"proxy"` // 代理
LocalAddrs string `json:"localAddrs"` // 本地网卡地址
PreferIPType string `json:"preferIPType"` // 优先IP类型,IPv4或者IPv6
UpdateCheckInfo UpdateCheckInfo `json:"updateCheckInfo"`
VideoFileExtensions string `json:"videoFileExtensions"`
FileRecordConfig string `json:"fileRecordConfig"` // 上传、下载、同步文件的记录,包括失败和成功的
DeviceId string `json:"deviceId"` // 客户端ID,用于标识登录客户端,阿里单个账号最多允许10个客户端同时登录
DeviceName string `json:"deviceName"` // 客户端名称,默认为:Chrome浏览器
// Openapi客户端信息
ClientId string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
// 本地工作目录(lcd/lpwd/lls命令使用)
LocalWorkdir string `json:"localWorkdir"`
configFilePath string
configFile *os.File
fileMu sync.Mutex
@@ -164,9 +205,23 @@ func (c *PanConfig) init() error {
if c.Proxy != "" {
requester.SetGlobalProxy(c.Proxy)
}
// 设置域名解析策略 IPv4 or IPv6
t := requester.IPAny
if strings.ToLower(c.PreferIPType) == "ipv4" {
t = requester.IPv4
} else if strings.ToLower(c.PreferIPType) == "ipv6" {
t = requester.IPv6
}
requester.SetPreferIPType(t)
// 设置本地网卡地址
if c.LocalAddrs != "" {
requester.SetLocalTCPAddrList(strings.Split(c.LocalAddrs, ",")...)
ips := ParseLocalAddress(c.LocalAddrs, strings.ToLower(c.PreferIPType))
if len(ips) > 0 {
logger.Verboseln("bind local address list: ", ips)
requester.SetLocalTCPAddrList(ips...)
}
}
return nil
@@ -179,8 +234,8 @@ func (c *PanConfig) lazyOpenConfigFile() (err error) {
}
c.fileMu.Lock()
os.MkdirAll(filepath.Dir(c.configFilePath), 0700)
c.configFile, err = os.OpenFile(c.configFilePath, os.O_CREATE|os.O_RDWR, 0600)
os.MkdirAll(filepath.Dir(c.configFilePath), 0755)
c.configFile, err = os.OpenFile(c.configFilePath, os.O_CREATE|os.O_RDWR, 0755)
c.fileMu.Unlock()
if err != nil {
@@ -225,41 +280,154 @@ func (c *PanConfig) loadConfigFromFile() (err error) {
if err != nil {
return ErrConfigContentsParseError
}
if c.DeviceName == "" {
c.DeviceName = DefaultDeviceName
}
if c.ClientId == "" {
c.ClientId = DefaultClientId
}
return nil
}
func (c *PanConfig) initDefaultConfig() {
// 设置默认的下载路径
switch runtime.GOOS {
case "windows":
c.SaveDir = cmdutil.ExecutablePathJoin("Downloads")
case "android":
// TODO: 获取完整的的下载路径
c.SaveDir = "/sdcard/Download"
default:
dataPath, ok := os.LookupEnv("HOME")
if !ok {
CmdConfigVerbose.Warn("Environment HOME not set")
c.SaveDir = cmdutil.ExecutablePathJoin("Downloads")
} else {
c.SaveDir = filepath.Join(dataPath, "Downloads")
}
}
//c.SaveDir = GetDefaultDownloadDir()
c.ConfigVer = ConfigVersion
c.VideoFileExtensions = DefaultVideoFileExtensions
c.DeviceId = RandomDeviceId() // 生成默认客户端ID
c.DeviceName = DefaultDeviceName
c.ClientId = DefaultClientId
c.FileRecordConfig = "2" // 默认关闭
c.PreferIPType = "ipv4" // 默认优先IPv4
}
// GetConfigDir 获取配置路径
func GetConfigDir() string {
// 从环境变量读取
// 按照以下顺序依次获取配置目录
// 1.环境变量ALIYUNPAN_CONFIG_DIR => 2. XDG_CONFIG_HOME/aliyunpan => 3. /etc/aliyunpan/ => 4. ~/.aliyunpan/ => 5.当前程序目录
// 1. 从环境变量读取
configDir, ok := os.LookupEnv(EnvConfigDir)
if ok {
if filepath.IsAbs(configDir) {
logger.Verboseln("use config dir from ALIYUNPAN_CONFIG_DIR env: ", configDir)
return configDir
}
// 如果不是绝对路径, 从程序目录寻找
return cmdutil.ExecutablePathJoin(configDir)
configDir = cmdutil.ExecutablePathJoin(configDir)
logger.Verboseln("use config dir from ALIYUNPAN_CONFIG_DIR env: ", configDir)
return configDir
} else {
// 2. $XDG_CONFIG_HOME/aliyunpan
xdgConfigHome := path.Join(xdg.ConfigHome, "aliyunpan")
if IsFolderExist(xdgConfigHome) {
logger.Verboseln("use XDG home config dir: ", xdgConfigHome)
return xdgConfigHome
}
// 3. /etc/aliyunpan/
if runtime.GOOS == "linux" {
cd := "/etc/aliyunpan"
if IsFolderExist(cd) {
logger.Verboseln("use config dir: ", cd)
return cd
}
}
// 4. ~/.aliyunpan/
if runtime.GOOS == "linux" || runtime.GOOS == "windows" {
cd, er := homedir.Expand("~/.aliyunpan")
if er == nil {
if IsFolderExist(cd) {
logger.Verboseln("use config dir: ", cd)
return cd
}
}
}
}
return cmdutil.ExecutablePathJoin(configDir)
// 5.当前程序所在目录
configDir = cmdutil.ExecutablePathJoin("")
logger.Verboseln("use config dir: ", configDir)
return configDir
}
// GetDefaultDownloadDir 获取默认的下载目录
func GetDefaultDownloadDir() string {
// 按照以下顺序依次获取下载目录
// 1.环境变量ALIYUNPAN_DOWNLOAD_DIR => 2. XDG_DOWNLOAD_DIR目录 => 3. (当前程序目录)/Downloads
downloadDir := ""
// 设置默认的下载路径
switch runtime.GOOS {
case "android":
// TODO: 获取完整的的下载路径
downloadDir = "/sdcard/Download"
return downloadDir
default:
// 1. 环境变量ALIYUNPAN_DOWNLOAD_DIR
d, ok := os.LookupEnv(EnvDownloadDir)
if ok {
logger.Verboseln("use download dir from ALIYUNPAN_DOWNLOAD_DIR env: ", d)
downloadDir = d
return downloadDir
} else {
// 2. $XDG_DOWNLOAD_DIR
xdgDownloadDir := path.Join(xdg.UserDirs.Download)
if IsFolderExist(xdgDownloadDir) {
logger.Verboseln("use XDG download dir: ", xdgDownloadDir)
return xdgDownloadDir
}
}
}
// 3. (当前程序目录)/Downloads
downloadDir = cmdutil.ExecutablePathJoin("Downloads")
logger.Verboseln("use default download dir: ", downloadDir)
return downloadDir
}
// GetDownloadDir 获取当前下载目录
func GetDownloadDir() string {
if Config.SaveDir == "" {
return GetDefaultDownloadDir()
}
return Config.SaveDir
}
// GetPluginDir 获取插件文件夹路径
func GetPluginDir() string {
return strings.TrimSuffix(GetConfigDir(), "/") + "/plugin"
}
// GetPluginKvFile 获取插件KV键值对存储文件
func GetPluginKvFile() string {
return GetPluginDir() + "/kv.bolt"
}
// GetSyncDriveDir 获取同步备份的文件夹路径
func GetSyncDriveDir() string {
return strings.TrimSuffix(GetConfigDir(), "/") + "/sync_drive"
}
// GetLogDir 获取日志文件目录路径
func GetLogDir() string {
return strings.TrimSuffix(GetConfigDir(), "/") + "/logs"
}
// GetLogFilePath 获取日志文件路径
func GetLogFilePath() string {
dirPath := GetLogDir()
if b, e := utils.PathExists(dirPath); e == nil {
if !b {
os.MkdirAll(dirPath, 0755)
}
}
return dirPath + "/" + "aliyunpan_verbose.log"
}
// GetLockerDir 获取文件锁路径
func GetLockerDir() string {
return strings.TrimSuffix(GetConfigDir(), "/")
}
func (c *PanConfig) ActiveUser() *PanUser {
@@ -274,7 +442,10 @@ func (c *PanConfig) ActiveUser() *PanUser {
if u.UserId == c.ActiveUID {
if u.PanClient() == nil {
// restore client
user, err := SetupUserByCookie(&u.WebToken)
user, err := SetupUserByCookie(u.OpenapiToken, u.WebapiToken,
u.TicketId, u.UserId,
c.DeviceId, c.DeviceName,
c.ClientId, c.ClientSecret)
if err != nil {
logger.Verboseln("setup user error")
return nil
@@ -282,25 +453,46 @@ func (c *PanConfig) ActiveUser() *PanUser {
u.panClient = user.panClient
u.Nickname = user.Nickname
if u.ActiveDriveId == "" {
u.ActiveDriveId = user.DriveList.GetFileDriveId()
}
u.DriveList = user.DriveList
// check workdir valid or not
if user.IsFileDriveActive() {
fe, err1 := u.PanClient().FileInfoByPath(u.ActiveDriveId, u.Workdir)
fe, err1 := u.PanClient().OpenapiPanClient().FileInfoByPath(u.ActiveDriveId, u.Workdir)
if err1 != nil {
// default to root
u.Workdir = "/"
u.WorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
} else {
u.WorkdirFileEntity = *fe
if u.Workdir == "" {
u.Workdir = "/"
}
}
} else if user.IsResourceDriveActive() {
fe, err1 := u.PanClient().OpenapiPanClient().FileInfoByPath(u.ActiveDriveId, u.ResourceWorkdir)
if err1 != nil {
// default to root
u.ResourceWorkdir = "/"
u.ResourceWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
} else {
u.ResourceWorkdirFileEntity = *fe
if u.ResourceWorkdir == "" {
u.ResourceWorkdir = "/"
}
}
} else if user.IsAlbumDriveActive() {
fe, err1 := u.PanClient().FileInfoByPath(u.ActiveDriveId, u.AlbumWorkdir)
fe, err1 := u.PanClient().WebapiPanClient().FileInfoByPath(u.ActiveDriveId, u.AlbumWorkdir)
if err1 != nil {
// default to root
u.AlbumWorkdir = "/"
u.AlbumWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
} else {
u.AlbumWorkdirFileEntity = *fe
if u.AlbumWorkdir == "" {
u.AlbumWorkdir = "/"
}
}
}
}
@@ -308,7 +500,7 @@ func (c *PanConfig) ActiveUser() *PanUser {
return u
}
}
return &PanUser{}
return nil
}
return c.activeUser
}
@@ -319,8 +511,12 @@ func (c *PanConfig) SetActiveUser(user *PanUser) *PanUser {
if u.UserId == user.UserId {
// update user info
u.Nickname = user.Nickname
u.WebToken = user.WebToken
u.RefreshToken = user.RefreshToken
u.WebapiToken = user.WebapiToken
u.OpenapiToken = user.OpenapiToken
u.TicketId = user.TicketId
if u.PanClient() != nil && user.PanClient() != nil {
u.UpdateClient(user.PanClient().OpenapiPanClient(), user.PanClient().WebapiPanClient())
}
needToInsert = false
break
}
@@ -348,9 +544,9 @@ func (c *PanConfig) NumLogins() int {
}
// SwitchUser 切换登录用户
func (c *PanConfig) SwitchUser(uid, username string) (*PanUser, error) {
func (c *PanConfig) SwitchUser(uid string) (*PanUser, error) {
for _, u := range c.UserList {
if u.UserId == uid || u.AccountName == username {
if u.UserId == uid {
return c.SetActiveUser(u), nil
}
}
@@ -366,7 +562,7 @@ func (c *PanConfig) DeleteUser(uid string) (*PanUser, error) {
c.ActiveUID = ""
c.activeUser = nil
if len(c.UserList) > 0 {
c.SwitchUser(c.UserList[0].UserId, "")
c.SwitchUser(c.UserList[0].UserId)
}
return u, nil
}
@@ -381,4 +577,19 @@ func (c *PanConfig) HTTPClient(ua string) *requester.HTTPClient {
client.SetUserAgent(ua)
}
return client
}
}
func (c *PanConfig) GetVideoExtensionList() []string {
if c.VideoFileExtensions == "" {
return []string{}
}
exts := strings.Split(c.VideoFileExtensions, ",")
r := []string{}
for _, ex := range exts {
ex = strings.TrimSpace(ex)
if ex != "" {
r = append(r, strings.ToLower(ex))
}
}
return r
}
+47 -11
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -33,7 +33,21 @@ func (c *PanConfig) SetProxy(proxy string) {
// SetLocalAddrs 设置localAddrs
func (c *PanConfig) SetLocalAddrs(localAddrs string) {
c.LocalAddrs = localAddrs
requester.SetLocalTCPAddrList(strings.Split(localAddrs, ",")...)
ips := ParseLocalAddress(localAddrs, strings.ToLower(c.PreferIPType))
if len(ips) > 0 {
requester.SetLocalTCPAddrList(ips...)
}
}
func (c *PanConfig) SetPreferIPType(ipType string) {
c.PreferIPType = ipType
t := requester.IPAny
if strings.ToLower(ipType) == "ipv4" {
t = requester.IPv4
} else if strings.ToLower(ipType) == "ipv6" {
t = requester.IPv6
}
requester.SetPreferIPType(t)
}
// SetCacheSizeByStr 设置cache_size
@@ -66,23 +80,45 @@ func (c *PanConfig) SetMaxUploadRateByStr(sizeStr string) error {
return nil
}
// SetFileRecorderConfig 设置文件记录器
func (c *PanConfig) SetFileRecorderConfig(config string) error {
if config == "1" || config == "2" {
c.FileRecordConfig = config
}
return nil
}
// SetDeviceId 设置客户端ID
func (c *PanConfig) SetDeviceId(deviceId string) error {
if deviceId == "" {
return nil
}
c.DeviceId = deviceId
return nil
}
// PrintTable 输出表格
func (c *PanConfig) PrintTable() {
fileRecorderLabel := "禁用"
if c.FileRecordConfig == "1" {
fileRecorderLabel = "开启"
}
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"名称", "值", "建议值", "描述"})
tb.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
tb.AppendBulk([][]string{
[]string{"cache_size", converter.ConvertFileSize(int64(c.CacheSize), 2), "1KB ~ 256KB", "下载缓存, 如果硬盘占用高或下载速度慢, 请尝试调大此值"},
[]string{"max_download_parallel", strconv.Itoa(c.MaxDownloadParallel), "1 ~ 64", "最大下载并发量"},
[]string{"max_upload_parallel", strconv.Itoa(c.MaxUploadParallel), "1 ~ 100", "最大上传并发量,即同时上传文件最大数量"},
[]string{"max_download_load", strconv.Itoa(c.MaxDownloadLoad), "1 ~ 5", "同时进行下载文件的最大数量"},
[]string{"max_download_rate", showMaxRate(c.MaxDownloadRate), "", "限制最大下载速度, 0代表不限制"},
[]string{"max_upload_rate", showMaxRate(c.MaxUploadRate), "", "限制最大上传速度, 0代表不限制"},
[]string{"transfer_url_type", strconv.Itoa(c.TransferUrlType), "1-默认,2-阿里云ECS", "上传下载URL类别。除非在阿里云ECS服务器中使用,不然请设置1"},
[]string{"savedir", c.SaveDir, "", "下载文件的储存目录"},
[]string{"proxy", c.Proxy, "", "设置代理, 支持 http/socks5 代理,例如:http://127.0.0.1:8888"},
[]string{"local_addrs", c.LocalAddrs, "", "设置本地网卡地址, 多个地址用逗号隔开"},
[]string{"max_download_parallel", strconv.Itoa(c.MaxDownloadParallel), "1 ~ 3", "最大下载并发量,即同时下载文件最大数量"},
[]string{"max_upload_parallel", strconv.Itoa(c.MaxUploadParallel), "1 ~ 20", "最大上传并发量,即同时上传文件最大数量"},
[]string{"max_download_rate", showMaxRate(c.MaxDownloadRate), "", "限制单个文件最大下载速度, 0代表不限制"},
[]string{"max_upload_rate", showMaxRate(c.MaxUploadRate), "", "限制单个文件最大上传速度, 0代表不限制"},
[]string{"savedir", GetDownloadDir(), "", "下载文件的储存目录"},
[]string{"proxy", c.Proxy, "", "设置代理, 支持 http/socks5 代理,例如: http://127.0.0.1:8888 或者 socks5://127.0.0.1:8889"},
[]string{"local_addrs", c.LocalAddrs, "", "绑定本地网卡地址, 多个地址用逗号隔开,支持网口名称,例如: 127.0.0.1,192.168.100.126,en0,eth0"},
[]string{"ip_type", c.PreferIPType, "ipv4-优先IPv4,ipv6-优先IPv6", "设置域名解析IP优先类型。修改后需要重启应用生效"},
[]string{"file_record_config", fileRecorderLabel, "1-开启,2-禁用", "设置是否开启上传、下载、同步文件的结果记录,开启后会把结果记录到CSV文件方便后期查看"},
[]string{"device_id", c.DeviceId, "", "客户端ID,用于标识登录客户端,阿里单个账号最多允许10个客户端同时在线。修改后需要重启应用生效"},
})
tb.Render()
}
+266 -48
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,103 +17,311 @@ import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan-api/aliyunpan_open"
"github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi"
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
"github.com/tickstep/library-go/expires/cachemap"
"github.com/tickstep/library-go/logger"
"path"
"path/filepath"
"time"
)
type DriveInfo struct {
DriveId string `json:"driveId"`
DriveName string `json:"driveName"`
DriveTag string `json:"driveTag"`
DriveTag string `json:"driveTag"`
}
type DriveInfoList []*DriveInfo
func (d DriveInfoList) GetFileDriveId() string {
for _, drive := range d {
if drive.DriveTag == "File" {
return drive.DriveId
}
}
return ""
}
func (d DriveInfoList) GetAlbumDriveId() string {
for _, drive := range d {
if drive.DriveTag == "Album" {
return drive.DriveId
}
}
return ""
}
func (d DriveInfoList) GetResourceDriveId() string {
for _, drive := range d {
if drive.DriveTag == "Resource" {
return drive.DriveId
}
}
return ""
}
func (d DriveInfoList) GetDriveIdByName(name string) string {
for _, drive := range d {
if drive.DriveTag == name {
return drive.DriveId
}
}
return ""
}
func (d DriveInfoList) GetDriveNameById(driveId string) string {
for _, drive := range d {
if drive.DriveId == driveId {
return drive.DriveName
}
}
return ""
}
// PanClientToken 授权Token
type PanClientToken struct {
// AccessToken AccessToken
AccessToken string `json:"accessToken"`
// Expired 过期时间戳,单位秒
Expired int64 `json:"expired"`
}
// GetExpiredTimeCstStr 获取东八区时间字符串
func (t *PanClientToken) GetExpiredTimeCstStr() string {
cz := time.FixedZone("CST", 8*3600) // 东8区
tm := time.Unix(t.Expired, 0).In(cz)
return tm.Format("2006-01-02 15:04:05")
}
type PanUser struct {
// 用户信息
UserId string `json:"userId"`
Nickname string `json:"nickname"`
Nickname string `json:"nickname"`
AccountName string `json:"accountName"`
Workdir string `json:"workdir"`
// 文件(备份盘)
Workdir string `json:"workdir"`
WorkdirFileEntity aliyunpan.FileEntity `json:"workdirFileEntity"`
AlbumWorkdir string `json:"albumWorkdir"`
// 资源库
ResourceWorkdir string `json:"resourceWorkdir"`
ResourceWorkdirFileEntity aliyunpan.FileEntity `json:"resourceWorkdirFileEntity"`
// 相册
AlbumWorkdir string `json:"albumWorkdir"`
AlbumWorkdirFileEntity aliyunpan.FileEntity `json:"albumWorkdirFileEntity"`
ActiveDriveId string `json:"activeDriveId"`
DriveList DriveInfoList `json:"driveList"`
// 用户网盘信息
ActiveDriveId string `json:"activeDriveId"`
DriveList DriveInfoList `json:"driveList"`
RefreshToken string `json:"refreshToken"`
WebToken aliyunpan.WebLoginToken `json:"webToken"`
// 授权Token信息
TicketId string `json:"ticketId"`
WebapiToken *PanClientToken `json:"webapiToken"`
OpenapiToken *PanClientToken `json:"openapiToken"`
panClient *aliyunpan.PanClient
cacheOpMap cachemap.CacheOpMap
// API客户端
panClient *PanClient `json:"-"`
cacheOpMap cachemap.CacheOpMap `json:"-"`
}
type PanUserList []*PanUser
func SetupUserByCookie(webToken *aliyunpan.WebLoginToken) (user *PanUser, err *apierror.ApiError) {
func SetupUserByCookie(openapiToken, webapiToken *PanClientToken, ticketId, userId, deviceId, deviceName, clientId, clientSecret string) (user *PanUser, err *apierror.ApiError) {
tryRefreshWebToken := true
tryRefreshOpenToken := true
loginHelper := panlogin.NewLoginHelper(DefaultTokenServiceWebHost)
if webToken == nil {
return nil, apierror.NewFailedApiError("web token is empty")
if openapiToken == nil {
return nil, apierror.NewFailedApiError("openapi token is empty")
}
//if webapiToken == nil {
// return nil, apierror.NewFailedApiError("webapi token is empty")
//}
doLoginAct:
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{})
u := &PanUser{
WebToken: *webToken,
panClient: panClient,
Workdir: "/",
WorkdirFileEntity: *aliyunpan.NewFileEntityForRootDir(),
}
doOpenLoginAct:
// setup openapi client
openPanClient := aliyunpan_open.NewOpenPanClient(openapi.ApiConfig{
TicketId: ticketId,
UserId: userId,
ClientId: clientId,
ClientSecret: clientSecret,
}, openapi.ApiToken{
AccessToken: openapiToken.AccessToken,
ExpiredAt: openapiToken.Expired,
}, nil)
openPanClient.SetAccessTokenRefreshCallback(func(userId string, newToken openapi.ApiToken) error {
// api层自动刷新Token,回调通知程序
logger.Verboseln("openapi token refresh, use new token")
openapiToken.AccessToken = newToken.AccessToken
openapiToken.Expired = newToken.ExpiredAt
return nil
})
// web api token maybe expired
userInfo, err := panClient.GetUserInfo()
// open api token maybe expired
// check & refresh new one
openUserInfo, err := openPanClient.GetUserInfo()
if err != nil {
if err.Code == apierror.ApiCodeTokenExpiredCode && tryRefreshWebToken {
tryRefreshWebToken = false
webCookie,_ := aliyunpan.GetAccessTokenFromRefreshToken(webToken.RefreshToken)
if webCookie != nil {
webToken = webCookie
goto doLoginAct
if err.Code == apierror.ApiCodeTokenExpiredCode && tryRefreshOpenToken { // 如果是token过期,则尝试刷新
tryRefreshOpenToken = false
wt, re := loginHelper.GetOpenapiNewToken(ticketId, userId, openapiToken.AccessToken)
if re != nil {
logger.Verboseln("get openapi token from server error: ", re.Msg)
return nil, apierror.NewFailedApiError("get new openapi token error, try login again")
}
if wt != nil {
openapiToken = &PanClientToken{
AccessToken: wt.AccessToken,
Expired: wt.Expired,
}
// TODO: 检查token有效期
}
// 回到token检测流程,必须确保token是有效
time.Sleep(time.Duration(1) * time.Second)
goto doOpenLoginAct
}
return nil, err
}
doWebLoginAct:
// setup webapi client
var webUserInfo *aliyunpan.UserInfo
var err2 *apierror.ApiError
var webPanClient *aliyunpan_web.WebPanClient
if webapiToken != nil && webapiToken.AccessToken != "" {
appConfig := aliyunpan_web.AppConfig{
AppId: "25dzX3vbYqktVxyX",
DeviceId: deviceId,
UserId: userId,
Nonce: 0,
PublicKey: "",
}
webPanClient = aliyunpan_web.NewWebPanClient(aliyunpan_web.WebLoginToken{
AccessTokenType: "Bearer",
AccessToken: webapiToken.AccessToken,
RefreshToken: "",
ExpiresIn: 7200,
ExpireTime: webapiToken.GetExpiredTimeCstStr(),
}, aliyunpan_web.AppLoginToken{}, appConfig, aliyunpan_web.SessionConfig{
DeviceName: deviceName,
ModelName: "Windows网页版",
})
// web api token maybe expired
// check & refresh new one
webUserInfo, err2 = webPanClient.GetUserInfo()
if err2 != nil {
if err2.Code == apierror.ApiCodeTokenExpiredCode && tryRefreshWebToken {
tryRefreshWebToken = false
wt, e := loginHelper.GetWebapiNewToken(ticketId, userId, webapiToken.AccessToken)
if e != nil {
logger.Verboseln("get web token from server error: ", e.Msg)
}
if wt != nil {
webapiToken = &PanClientToken{
AccessToken: wt.AccessToken,
Expired: wt.Expired,
}
}
time.Sleep(time.Duration(1) * time.Second)
goto doWebLoginAct
}
webPanClient = nil
//return nil, err2
}
// web create session
if webUserInfo != nil {
appConfig.UserId = webUserInfo.UserId
webPanClient.UpdateAppConfig(appConfig)
r, e := webPanClient.CreateSession(nil)
if e != nil {
logger.Verboseln("call CreateSession error in SetupUserByCookie: " + e.Error())
}
if r != nil && !r.Result {
logger.Verboseln("上传签名秘钥失败,可能是你账号登录的设备已超最大数量")
}
}
}
//
// setup PanUser
//
u := &PanUser{
WebapiToken: webapiToken,
OpenapiToken: openapiToken,
panClient: NewPanClient(webPanClient, openPanClient),
Workdir: "/",
WorkdirFileEntity: *aliyunpan.NewFileEntityForRootDir(),
}
u.PanClient().OpenapiPanClient().SetAccessTokenRefreshCallback(func(userId string, newToken openapi.ApiToken) error {
// api层自动刷新Token,回调通知程序
logger.Verboseln("openapi token refresh, update for user")
u.OpenapiToken = &PanClientToken{
AccessToken: newToken.AccessToken,
Expired: newToken.ExpiredAt,
}
return nil
})
// setup user info
name := "Unknown"
if userInfo != nil {
if userInfo.Nickname != "" {
name = userInfo.Nickname
if openUserInfo != nil {
// fill userId for client
u.PanClient().OpenapiPanClient().UpdateUserId(openUserInfo.UserId)
if u.PanClient().WebapiPanClient() != nil {
u.PanClient().WebapiPanClient().UpdateUserId(openUserInfo.UserId)
}
// update user
u.UserId = userInfo.UserId
if openUserInfo.Nickname != "" {
name = openUserInfo.Nickname
}
u.UserId = openUserInfo.UserId
u.Nickname = name
u.AccountName = userInfo.UserName
u.AccountName = openUserInfo.UserName
// default file drive
if u.ActiveDriveId == "" {
u.ActiveDriveId = userInfo.FileDriveId
u.ActiveDriveId = openUserInfo.FileDriveId
}
// drive list
u.DriveList = DriveInfoList{
{DriveId: userInfo.FileDriveId, DriveTag: "File", DriveName: "文件"},
{DriveId: userInfo.AlbumDriveId, DriveTag: "Album", DriveName: "相册"},
{DriveId: openUserInfo.FileDriveId, DriveTag: "File", DriveName: "备份盘"},
{DriveId: openUserInfo.ResourceDriveId, DriveTag: "Resource", DriveName: "资源库"},
}
if webUserInfo != nil {
u.AccountName = webUserInfo.UserName
u.DriveList = append(u.DriveList, &DriveInfo{DriveId: webUserInfo.AlbumDriveId, DriveTag: "Album", DriveName: "相册"})
}
} else {
// error, maybe the token has expired
return nil, apierror.NewFailedApiError("cannot get user info, the token has expired")
return nil, apierror.NewFailedApiError("cannot get user info, the token has expired. please login again")
}
// return user
return u, nil
}
func (pu *PanUser) PanClient() *aliyunpan.PanClient {
func (pu *PanUser) PanClient() *PanClient {
return pu.panClient
}
func (pu *PanUser) UpdateClient(openClient *aliyunpan_open.OpenPanClient, webClient *aliyunpan_web.WebPanClient) {
if pu.panClient == nil {
pu.panClient = &PanClient{
webapiPanClient: webClient,
openapiPanClient: openClient,
}
} else {
pu.panClient.webapiPanClient = webClient
pu.panClient.openapiPanClient = openClient
}
}
// PathJoin 合并工作目录和相对路径p, 若p为绝对路径则忽略
func (pu *PanUser) PathJoin(driveId, p string) string {
if path.IsAbs(p) {
@@ -124,6 +332,8 @@ func (pu *PanUser) PathJoin(driveId, p string) string {
if di != nil {
if di.IsFileDrive() {
wd = pu.Workdir
} else if di.IsResourceDrive() {
wd = pu.ResourceWorkdir
} else if di.IsAlbumDrive() {
wd = pu.AlbumWorkdir
}
@@ -133,27 +343,26 @@ func (pu *PanUser) PathJoin(driveId, p string) string {
func (pu *PanUser) FreshWorkdirInfo() {
if pu.IsFileDriveActive() {
fe, err := pu.PanClient().FileInfoById(pu.ActiveDriveId, pu.WorkdirFileEntity.FileId)
fe, err := pu.PanClient().OpenapiPanClient().FileInfoById(pu.ActiveDriveId, pu.WorkdirFileEntity.FileId)
if err != nil {
logger.Verboseln("刷新工作目录信息失败")
return
}
pu.WorkdirFileEntity = *fe
} else if pu.IsAlbumDriveActive() {
fe, err := pu.PanClient().FileInfoById(pu.ActiveDriveId, pu.AlbumWorkdirFileEntity.FileId)
fe, err := pu.PanClient().OpenapiPanClient().FileInfoById(pu.ActiveDriveId, pu.AlbumWorkdirFileEntity.FileId)
if err != nil {
logger.Verboseln("刷新工作目录信息失败")
return
}
pu.AlbumWorkdirFileEntity = *fe
}
}
// GetSavePath 根据提供的网盘文件路径 panpath, 返回本地储存路径,
// 返回绝对路径, 获取绝对路径出错时才返回相对路径...
func (pu *PanUser) GetSavePath(filePanPath string) string {
dirStr := filepath.Join(Config.SaveDir, fmt.Sprintf("%s", pu.UserId), filePanPath)
dirStr := filepath.Join(GetDownloadDir(), fmt.Sprintf("%s", pu.UserId), filePanPath)
dir, err := filepath.Abs(dirStr)
if err != nil {
dir = filepath.Clean(dirStr)
@@ -162,7 +371,7 @@ func (pu *PanUser) GetSavePath(filePanPath string) string {
}
func (pu *PanUser) GetDriveByTag(tag string) *DriveInfo {
for _,item := range pu.DriveList {
for _, item := range pu.DriveList {
if item.DriveTag == tag {
return item
}
@@ -171,7 +380,7 @@ func (pu *PanUser) GetDriveByTag(tag string) *DriveInfo {
}
func (pu *PanUser) GetDriveById(id string) *DriveInfo {
for _,item := range pu.DriveList {
for _, item := range pu.DriveList {
if item.DriveId == id {
return item
}
@@ -180,7 +389,7 @@ func (pu *PanUser) GetDriveById(id string) *DriveInfo {
}
func (pu *PanUser) GetActiveDriveInfo() *DriveInfo {
for _,item := range pu.DriveList {
for _, item := range pu.DriveList {
if item.DriveId == pu.ActiveDriveId {
return item
}
@@ -198,10 +407,19 @@ func (pu *PanUser) IsAlbumDriveActive() bool {
return d != nil && d.IsAlbumDrive()
}
func (pu *PanUser) IsResourceDriveActive() bool {
d := pu.GetActiveDriveInfo()
return d != nil && d.IsResourceDrive()
}
func (di *DriveInfo) IsFileDrive() bool {
return di.DriveTag == "File"
}
func (di *DriveInfo) IsAlbumDrive() bool {
return di.DriveTag == "Album"
}
}
func (di *DriveInfo) IsResourceDrive() bool {
return di.DriveTag == "Resource"
}
+84 -3
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,12 +17,17 @@ import (
"encoding/hex"
"github.com/olekukonko/tablewriter"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/library/nets"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/crypto"
"github.com/tickstep/library-go/ids"
"github.com/tickstep/library-go/logger"
"math/rand"
"net"
"os"
"strconv"
"strings"
"time"
)
func (pl *PanUserList) String() string {
@@ -33,7 +38,7 @@ func (pl *PanUserList) String() string {
tb.SetHeader([]string{"#", "uid", "用户名", "昵称"})
for k, userInfo := range *pl {
tb.Append([]string{strconv.Itoa(k), userInfo.UserId, userInfo.AccountName, userInfo.Nickname})
tb.Append([]string{strconv.Itoa(k + 1), userInfo.UserId, userInfo.AccountName, userInfo.Nickname})
}
tb.Render()
@@ -41,6 +46,16 @@ func (pl *PanUserList) String() string {
return builder.String()
}
// GetUserByUserId 根据用户ID获取用户信息
func (pl *PanUserList) GetUserByUserId(userId string) *PanUser {
for _, u := range *pl {
if u.UserId == userId {
return u
}
}
return nil
}
// AverageParallel 返回平均的下载最大并发量
func AverageParallel(parallel, downloadLoad int) int {
if downloadLoad < 1 {
@@ -94,7 +109,7 @@ func DecryptString(text string) string {
if text == "" {
return ""
}
d, _ := hex.DecodeString(text)
d, _ := hex.DecodeString(text)
// use the machine unique id as the key
// but in some OS, this key will be changed if you reinstall the OS
@@ -105,3 +120,69 @@ func DecryptString(text string) string {
}
return string(r)
}
// IsFolderExist 判断文件夹是否存在
func IsFolderExist(pathStr string) bool {
fi, err := os.Stat(pathStr)
if err != nil {
if os.IsExist(err) {
return fi.IsDir()
}
if os.IsNotExist(err) {
return false
}
return false
}
return fi.IsDir()
}
// RandomDeviceId 随机生成device-id
func RandomDeviceId() string {
count := 24
STR_SET := "abcdefjhijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ1234567890"
rand.Seed(time.Now().UnixNano())
str := strings.Builder{}
for i := 0; i < count; i++ {
str.WriteByte(byte(STR_SET[rand.Intn(len(STR_SET))]))
}
return str.String()
}
// ParseLocalAddress 解析网络接口配置为对应的本地IP地址
func ParseLocalAddress(localAddrs string, filterIpType string) []string {
allLocalAddress, _ := nets.GetLocalNetInterfaceAddress()
localAddrNames := strings.Split(localAddrs, ",")
ips := []string{}
for _, addr := range localAddrNames {
if addr == "" {
continue
}
if net.ParseIP(addr) == nil {
// maybe local interface name
if localAddr := allLocalAddress.GetByName(addr); localAddr != nil {
if localAddr.IPv4 != "" {
if filterIpType == "any" || filterIpType == "" { // 不限制
ips = append(ips, localAddr.IPv4)
} else {
if filterIpType == "ipv4" {
ips = append(ips, localAddr.IPv4)
}
}
}
if localAddr.IPv6 != "" {
if filterIpType == "any" || filterIpType == "" { // 不限制
ips = append(ips, localAddr.IPv6)
} else {
if filterIpType == "ipv6" {
ips = append(ips, localAddr.IPv6)
}
}
}
}
} else {
// ip
ips = append(ips, addr)
}
}
return ips
}
+4
View File
@@ -25,3 +25,7 @@ func TestEncryptString(t *testing.T) {
func TestDecryptString(t *testing.T) {
fmt.Println(DecryptString("75b3c8d21607440c0e8a70f4a4861c8669774cc69c70ce2a2c8acb815b6d5d3b"))
}
func TestRandomDeviceId(t *testing.T) {
fmt.Println(RandomDeviceId())
}
+13 -8
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -24,13 +24,18 @@ const (
var (
// MinParallelSize 单个线程最小的数据量
MinParallelSize int64 = 128 * 1024 // 128kb
MinParallelSize int64 = 10 * 1024 * 1024 // 10MB
// MaxParallelWorkerCount 单个文件下载最大并发线程数量
// 阿里云盘规定:并发下载线程数不要超过3,否则会有风控检测处罚的风险
MaxParallelWorkerCount int = 3
)
//Config 下载配置
// Config 下载配置
type Config struct {
Mode transfer.RangeGenMode // 下载Range分配模式
MaxParallel int // 最大下载并发量
SliceParallel int // 单文件下载线程数,为0代表程序自动调度
CacheSize int // 下载缓冲
BlockSize int64 // 每个Range区块的大小, RangeGenMode 为 RangeGenMode2 时才有效
MaxRate int64 // 限制最大下载速度
@@ -38,18 +43,18 @@ type Config struct {
InstanceStatePath string // 断点续传信息路径
TryHTTP bool // 是否尝试使用 http 连接
ShowProgress bool // 是否展示下载进度条
UseInternalUrl bool // 是否使用内置链接
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
}
//NewConfig 返回默认配置
// NewConfig 返回默认配置
func NewConfig() *Config {
return &Config{
MaxParallel: 5,
MaxParallel: 3,
CacheSize: CacheSize,
}
}
//Fix 修复配置信息, 使其合法
// Fix 修复配置信息, 使其合法
func (cfg *Config) Fix() {
fixCacheSize(&cfg.CacheSize)
if cfg.MaxParallel < 1 {
@@ -57,7 +62,7 @@ func (cfg *Config) Fix() {
}
}
//Copy 拷贝新的配置
// Copy 拷贝新的配置
func (cfg *Config) Copy() *Config {
newCfg := *cfg
return &newCfg
+257 -62
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,15 +19,18 @@ import (
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder/cmdutil"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/global"
"github.com/tickstep/aliyunpan/internal/waitgroup"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/cachepool"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/prealloc"
"github.com/tickstep/library-go/requester"
"github.com/tickstep/library-go/requester/rio/speeds"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"io"
"net/http"
"strings"
"sync"
"time"
)
@@ -42,6 +45,7 @@ type (
Downloader struct {
onExecuteEvent requester.Event //开始下载事件
onSuccessEvent requester.Event //成功下载事件
onFailedEvent requester.Event //成功下载事件
onFinishEvent requester.Event //结束下载事件
onPauseEvent requester.Event //暂停下载事件
onResumeEvent requester.Event //恢复下载事件
@@ -49,9 +53,11 @@ type (
onDownloadStatusEvent DownloadStatusFunc //状态处理事件
monitorCancelFunc context.CancelFunc
globalSpeedsStat *speeds.Speeds // 全局速度统计
fileInfo *aliyunpan.FileEntity // 下载的文件信息
driveId string
filePanSource global.FileSourceType // 要下载的网盘文件来源
fileInfo *aliyunpan.FileEntity // 下载的文件信息
driveId string
loadBalancerCompareFunc LoadBalancerCompareFunc // 负载均衡检测函数
durlCheckFunc DURLCheckFunc // 下载url检测函数
statusCodeBodyCheckFunc StatusCodeBodyCheckFunc
@@ -59,7 +65,8 @@ type (
loadBalansers []string
writer io.WriterAt
client *requester.HTTPClient
panClient *aliyunpan.PanClient
panClient *config.PanClient
subPanClientList []*config.PanClient // 辅助下载子账号列表
config *Config
monitor *Monitor
instanceState *InstanceState
@@ -69,29 +76,39 @@ type (
DURLCheckFunc func(client *requester.HTTPClient, durl string) (contentLength int64, resp *http.Response, err error)
// StatusCodeBodyCheckFunc 响应状态码出错的检查函数
StatusCodeBodyCheckFunc func(respBody io.Reader) error
// panClientDownloadUrlEntity 下载url实体,和网盘(用户)客户端绑定
panClientDownloadUrlEntity struct {
PanClient *config.PanClient
FileInfo *aliyunpan.FileEntity
DriveId string
FileId string
FileUrl string
}
)
//NewDownloader 初始化Downloader
func NewDownloader(writer io.WriterAt, config *Config, p *aliyunpan.PanClient) (der *Downloader) {
// NewDownloader 初始化Downloader
func NewDownloader(writer io.WriterAt, config *Config, p *config.PanClient, sp []*config.PanClient, globalSpeedsStat *speeds.Speeds) (der *Downloader) {
der = &Downloader{
config: config,
writer: writer,
panClient: p,
config: config,
writer: writer,
panClient: p,
subPanClientList: sp,
globalSpeedsStat: globalSpeedsStat,
}
return
}
//SetClient 设置http客户端
func (der *Downloader) SetFileInfo(f *aliyunpan.FileEntity) {
// SetFileInfo 设置文件信息
func (der *Downloader) SetFileInfo(source global.FileSourceType, f *aliyunpan.FileEntity) {
der.filePanSource = source
der.fileInfo = f
}
func (der *Downloader) SetDriveId(driveId string) {
der.driveId = driveId
}
//SetClient 设置http客户端
// SetClient 设置http客户端
func (der *Downloader) SetClient(client *requester.HTTPClient) {
der.client = client
}
@@ -101,7 +118,7 @@ func (der *Downloader) SetLoadBalancerCompareFunc(f LoadBalancerCompareFunc) {
der.loadBalancerCompareFunc = f
}
//SetStatusCodeBodyCheckFunc 设置响应状态码出错的检查函数, 当FirstCheckMethod不为HEAD时才有效
// SetStatusCodeBodyCheckFunc 设置响应状态码出错的检查函数, 当FirstCheckMethod不为HEAD时才有效
func (der *Downloader) SetStatusCodeBodyCheckFunc(f StatusCodeBodyCheckFunc) {
der.statusCodeBodyCheckFunc = f
}
@@ -126,19 +143,20 @@ func (der *Downloader) lazyInit() {
}
// SelectParallel 获取合适的 parallel
func (der *Downloader) SelectParallel(single bool, maxParallel int, totalSize int64, instanceRangeList transfer.RangeList) (parallel int) {
isRange := instanceRangeList != nil && len(instanceRangeList) > 0
if single { //不支持多线程
parallel = 1
func (der *Downloader) SelectParallel(prefParallel int, maxParallel int, totalSize int64, instanceRangeList transfer.RangeList) (parallel int) {
isRange := instanceRangeList != nil && len(instanceRangeList) > 0 // 历史下载分片记录存在
if prefParallel > 0 { // 单线程下载
parallel = prefParallel
} else if isRange {
parallel = len(instanceRangeList)
} else {
parallel = der.config.MaxParallel
if int64(parallel) > totalSize/int64(MinParallelSize) {
parallel = maxParallel // 默认为设置为maxParallel个并发线程数
if int64(parallel) > totalSize/int64(MinParallelSize) { // 如果文件太小不足切片成maxParallel数量的分片,则计算最接近的分片数量
parallel = int(totalSize/int64(MinParallelSize)) + 1
}
}
// 其他情况默认使用单线程下载
if parallel < 1 {
parallel = 1
}
@@ -146,9 +164,9 @@ func (der *Downloader) SelectParallel(single bool, maxParallel int, totalSize in
}
// SelectBlockSizeAndInitRangeGen 获取合适的 BlockSize, 和初始化 RangeGen
func (der *Downloader) SelectBlockSizeAndInitRangeGen(single bool, status *transfer.DownloadStatus, parallel int) (blockSize int64, initErr error) {
func (der *Downloader) SelectBlockSizeAndInitRangeGen(status *transfer.DownloadStatus, parallel int) (blockSize int64, initErr error) {
// Range 生成器
if single { // 单线程
if parallel == 1 { // 单线程
blockSize = -1
return
}
@@ -214,7 +232,7 @@ func (der *Downloader) checkLoadBalancers() *LoadBalancerResponseList {
}
loadBalancer := &LoadBalancerResponse{
URL: req.URL.String(),
URL: req.URL.String(),
}
loadBalancerResponses = append(loadBalancerResponses, loadBalancer)
@@ -280,10 +298,16 @@ func (der *Downloader) checkLoadBalancers() *LoadBalancerResponseList {
return loadBalancerResponseList
}
//Execute 开始任务
// Execute 开始任务
func (der *Downloader) Execute() error {
der.lazyInit()
// zero file, no need to download data
if der.fileInfo.FileSize == 0 {
cmdutil.Trigger(der.onFinishEvent)
return nil
}
var (
loadBalancerResponseList = der.checkLoadBalancers()
bii *transfer.DownloadInstanceInfo
@@ -298,7 +322,6 @@ func (der *Downloader) Execute() error {
var (
isInstance = bii != nil // 是否存在断点信息
status *transfer.DownloadStatus
single = false // 开启多线程下载
)
if !isInstance {
bii = &transfer.DownloadInstanceInfo{}
@@ -320,9 +343,9 @@ func (der *Downloader) Execute() error {
defer rl.Stop()
}
// 数据处理
parallel := der.SelectParallel(single, der.config.MaxParallel, status.TotalSize(), bii.Ranges) // 实际的下载并行量
blockSize, err := der.SelectBlockSizeAndInitRangeGen(single, status, parallel) // 实际的BlockSize
// 计算本文件下载的并发线程数(分片数)
parallel := der.SelectParallel(der.config.SliceParallel, MaxParallelWorkerCount, status.TotalSize(), bii.Ranges) // 实际的下载并行量
blockSize, err := der.SelectBlockSizeAndInitRangeGen(status, parallel) // 实际的BlockSize
if err != nil {
return err
}
@@ -350,7 +373,7 @@ func (der *Downloader) Execute() error {
// 没有使用断点续传
// 分配线程
bii.Ranges = make(transfer.RangeList, 0, parallel)
if single { // 单线程
if parallel == 1 { // 单线程
bii.Ranges = append(bii.Ranges, &transfer.Range{Begin: 0, End: der.fileInfo.FileSize})
} else {
gen := status.RangeListGen()
@@ -367,35 +390,33 @@ func (der *Downloader) Execute() error {
var (
writeMu = &sync.Mutex{}
)
// 获取各个账号网盘客户端的下载链接
panClientFileUrl, err := der.getFileAllClientDownloadUrl()
if err != nil || panClientFileUrl == nil {
logger.Verbosef("ERROR: get download url error: %s\n", der.fileInfo.FileId)
cmdutil.Trigger(der.onCancelEvent)
return err
}
// 初始化下载worker
factorNum := len(bii.Ranges) / len(panClientFileUrl)
for k, r := range bii.Ranges {
panClientUrl := panClientFileUrl[int(k/factorNum)]
loadBalancer := loadBalancerResponseList.SequentialGet()
if loadBalancer == nil {
continue
}
// 获取下载链接
var apierr *apierror.ApiError
durl, apierr := der.panClient.GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
DriveId: der.driveId,
FileId: der.fileInfo.FileId,
})
time.Sleep(time.Duration(200) * time.Millisecond)
if apierr != nil {
logger.Verbosef("ERROR: get download url error: %s\n", der.fileInfo.FileId)
continue
}
logger.Verbosef("work id: %d, download url: %s\n", k, durl)
logger.Verbosef("work id: %d, download url: %v\n", k, panClientUrl.FileUrl)
client := requester.NewHTTPClient()
client.SetKeepAlive(true)
client.SetTimeout(10 * time.Minute)
realUrl := durl.Url
if der.config.UseInternalUrl {
realUrl = durl.InternalUrl
}
worker := NewWorker(k, der.driveId, der.fileInfo.FileId, realUrl, writer)
realUrl := panClientUrl.FileUrl
worker := NewWorker(k, panClientUrl.DriveId, panClientUrl.FileInfo.FileId, realUrl, writer, der.globalSpeedsStat)
worker.SetClient(client)
worker.SetPanClient(der.panClient)
worker.SetPanClient(panClientUrl.PanClient)
worker.SetWriteMutex(writeMu)
worker.SetTotalSize(der.fileInfo.FileSize)
@@ -406,8 +427,8 @@ func (der *Downloader) Execute() error {
der.monitor.SetStatus(status)
// 服务器不支持断点续传, 或者单线程下载, 都不重载worker
der.monitor.SetReloadWorker(parallel > 1)
// 阿里云盘支持断点续传,开启重载worker
der.monitor.SetReloadWorker(true)
moniterCtx, moniterCancelFunc := context.WithCancel(context.Background())
der.monitorCancelFunc = moniterCancelFunc
@@ -437,7 +458,166 @@ func (der *Downloader) Execute() error {
return err
}
//downloadStatusEvent 执行状态处理事件
// 获取对应网盘文件的下载链接
func (der *Downloader) getFileAllClientDownloadUrl() ([]*panClientDownloadUrlEntity, error) {
if der.filePanSource == global.AlbumSource {
// 相册源只支持主账号下载
return der.getAlbumSourceDownloadUrl()
} else {
// 文件源支持多账号分流下载
return der.getFileSourceDownloadUrl()
}
}
// getFileSourceDownloadUrl 获取文件源的文件下载链接
func (der *Downloader) getFileSourceDownloadUrl() ([]*panClientDownloadUrlEntity, error) {
result := []*panClientDownloadUrlEntity{}
// 主账号(必须存在)
// 获取下载链接
var apierr *apierror.ApiError
durl, apierr := der.panClient.OpenapiPanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
DriveId: der.driveId,
FileId: der.fileInfo.FileId,
})
time.Sleep(time.Duration(200) * time.Millisecond)
if apierr != nil {
logger.Verbosef("ERROR: get download url error: %s\n", der.fileInfo.FileId)
cmdutil.Trigger(der.onCancelEvent)
return nil, apierr
}
if durl == nil || durl.Url == "" || strings.HasPrefix(durl.Url, aliyunpan.IllegalDownloadUrlPrefix) {
logger.Verbosef("无法获取有效的下载链接: %+v\n", durl)
cmdutil.Trigger(der.onCancelEvent)
der.removeInstanceState() // 移除断点续传文件
cmdutil.Trigger(der.onFailedEvent)
return nil, ErrFileDownloadForbidden
}
result = append(result, &panClientDownloadUrlEntity{
PanClient: der.panClient,
FileInfo: der.fileInfo,
DriveId: der.driveId,
FileId: der.fileInfo.FileId,
FileUrl: durl.Url,
})
// 网盘名称
mainDriveName := ""
openUserInfo, err := der.panClient.OpenapiPanClient().GetUserInfo()
if err != nil || openUserInfo == nil {
return nil, err
}
if openUserInfo.FileDriveId == der.driveId {
mainDriveName = "File"
} else if openUserInfo.ResourceDriveId == der.driveId {
mainDriveName = "Resource"
}
// 网盘文件路径
mainFileFullPath := der.fileInfo.Path
// 铺助账号的下载链接,只支持文件源
if der.subPanClientList != nil {
for _, spc := range der.subPanClientList {
driveId := ""
userInfo, err1 := spc.OpenapiPanClient().GetUserInfo()
if err1 != nil {
continue
}
if mainDriveName == "File" {
driveId = userInfo.FileDriveId
} else if mainDriveName == "Resource" {
driveId = userInfo.ResourceDriveId
}
// 文件信息
panfileInfo, err2 := spc.OpenapiPanClient().FileInfoByPath(driveId, mainFileFullPath)
if err2 != nil || panfileInfo == nil {
continue
}
// 下载链接
durl2, apierr := spc.OpenapiPanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
DriveId: driveId,
FileId: panfileInfo.FileId,
})
time.Sleep(time.Duration(200) * time.Millisecond)
if apierr != nil {
logger.Verbosef("ERROR: get download url error: %s\n", der.fileInfo.FileId)
continue
}
if durl2 == nil || durl2.Url == "" || strings.HasPrefix(durl2.Url, aliyunpan.IllegalDownloadUrlPrefix) {
logger.Verbosef("无法获取有效的下载链接: %+v\n", durl2)
continue
}
result = append(result, &panClientDownloadUrlEntity{
PanClient: spc,
FileInfo: panfileInfo,
DriveId: driveId,
FileId: panfileInfo.FileId,
FileUrl: durl2.Url,
})
}
}
return result, nil
}
// getAlbumSourceDownloadUrl 获取相册源的文件下载链接
func (der *Downloader) getAlbumSourceDownloadUrl() ([]*panClientDownloadUrlEntity, error) {
result := []*panClientDownloadUrlEntity{}
// 相册源只能使用主账号
// 获取下载链接
var apierr *apierror.ApiError
durl, apierr := der.panClient.OpenapiPanClient().ShareAlbumGetFileDownloadUrl(&aliyunpan.ShareAlbumGetFileUrlParam{
AlbumId: der.fileInfo.AlbumId,
DriveId: der.fileInfo.DriveId,
FileId: der.fileInfo.FileId,
})
time.Sleep(time.Duration(200) * time.Millisecond)
if apierr != nil {
logger.Verbosef("ERROR: get album file download url error: %s\n", der.fileInfo.FileId)
cmdutil.Trigger(der.onCancelEvent)
return nil, apierr
}
realUrl := ""
if durl.StreamsUrl != nil { // 实况图片
if strings.ToLower(der.fileInfo.FileExtension) == "mov" {
// 视频文件
if durl.StreamsUrl.Mov != "" {
realUrl = durl.StreamsUrl.Mov
}
} else {
// 照片文件
if durl.StreamsUrl.Heic != "" {
realUrl = durl.StreamsUrl.Heic
} else if durl.StreamsUrl.Jpeg != "" {
realUrl = durl.StreamsUrl.Jpeg
}
}
} else if durl.Url != "" { // 普通图片
realUrl = durl.Url
}
if realUrl == "" {
logger.Verbosef("无法获取有效的相册文件下载链接: %+v\n", durl)
cmdutil.Trigger(der.onCancelEvent)
der.removeInstanceState() // 移除断点续传文件
cmdutil.Trigger(der.onFailedEvent)
return nil, ErrFileDownloadForbidden
}
// 返回下载链接
result = append(result, &panClientDownloadUrlEntity{
PanClient: der.panClient,
FileInfo: der.fileInfo,
DriveId: der.driveId,
FileId: der.fileInfo.FileId,
FileUrl: realUrl,
})
return result, nil
}
// downloadStatusEvent 执行状态处理事件
func (der *Downloader) downloadStatusEvent() {
if der.onDownloadStatusEvent == nil {
return
@@ -452,13 +632,14 @@ func (der *Downloader) downloadStatusEvent() {
case <-der.monitor.completed:
return
case <-ticker.C:
time.Sleep(500 * time.Millisecond)
der.onDownloadStatusEvent(status, der.monitor.RangeWorker)
}
}
}()
}
//Pause 暂停
// Pause 暂停
func (der *Downloader) Pause() {
if der.monitor == nil {
return
@@ -467,7 +648,7 @@ func (der *Downloader) Pause() {
der.monitor.Pause()
}
//Resume 恢复
// Resume 恢复
func (der *Downloader) Resume() {
if der.monitor == nil {
return
@@ -476,7 +657,7 @@ func (der *Downloader) Resume() {
der.monitor.Resume()
}
//Cancel 取消
// Cancel 取消
func (der *Downloader) Cancel() {
if der.monitor == nil {
return
@@ -485,37 +666,51 @@ func (der *Downloader) Cancel() {
cmdutil.Trigger(der.monitorCancelFunc)
}
//OnExecute 设置开始下载事件
// Failed 失败
func (der *Downloader) Failed() {
if der.monitor == nil {
return
}
cmdutil.Trigger(der.onFailedEvent)
cmdutil.Trigger(der.monitorCancelFunc)
}
// OnExecute 设置开始下载事件
func (der *Downloader) OnExecute(onExecuteEvent requester.Event) {
der.onExecuteEvent = onExecuteEvent
}
//OnSuccess 设置成功下载事件
// OnSuccess 设置成功下载事件
func (der *Downloader) OnSuccess(onSuccessEvent requester.Event) {
der.onSuccessEvent = onSuccessEvent
}
//OnFinish 设置结束下载事件
// OnFailed 设置失败事件
func (der *Downloader) OnFailed(onFailedEvent requester.Event) {
der.onFailedEvent = onFailedEvent
}
// OnFinish 设置结束下载事件
func (der *Downloader) OnFinish(onFinishEvent requester.Event) {
der.onFinishEvent = onFinishEvent
}
//OnPause 设置暂停下载事件
// OnPause 设置暂停下载事件
func (der *Downloader) OnPause(onPauseEvent requester.Event) {
der.onPauseEvent = onPauseEvent
}
//OnResume 设置恢复下载事件
// OnResume 设置恢复下载事件
func (der *Downloader) OnResume(onResumeEvent requester.Event) {
der.onResumeEvent = onResumeEvent
}
//OnCancel 设置取消下载事件
// OnCancel 设置取消下载事件
func (der *Downloader) OnCancel(onCancelEvent requester.Event) {
der.onCancelEvent = onCancelEvent
}
//OnDownloadStatusEvent 设置状态处理函数
// OnDownloadStatusEvent 设置状态处理函数
func (der *Downloader) OnDownloadStatusEvent(f DownloadStatusFunc) {
der.onDownloadStatusEvent = f
}
+29 -26
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,8 +16,8 @@ package downloader
import (
"context"
"errors"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/logger"
"sort"
"time"
)
@@ -36,7 +36,7 @@ type (
completed chan struct{}
err error
resetController *ResetController
isReloadWorker bool //是否重载worker, 单线程模式不重载
isReloadWorker bool //是否重载worker
// 临时变量
lastAvaliableIndex int
@@ -46,7 +46,7 @@ type (
RangeWorkerFunc func(key int, worker *Worker) bool
)
//NewMonitor 初始化Monitor
// NewMonitor 初始化Monitor
func NewMonitor() *Monitor {
monitor := &Monitor{}
return monitor
@@ -60,16 +60,16 @@ func (mt *Monitor) lazyInit() {
mt.status = transfer.NewDownloadStatus()
}
if mt.resetController == nil {
mt.resetController = NewResetController(80)
mt.resetController = NewResetController(1000)
}
}
//InitMonitorCapacity 初始化workers, 用于Append
// InitMonitorCapacity 初始化workers, 用于Append
func (mt *Monitor) InitMonitorCapacity(capacity int) {
mt.workers = make(WorkerList, 0, capacity)
}
//Append 增加Worker
// Append 增加Worker
func (mt *Monitor) Append(worker *Worker) {
if worker == nil {
return
@@ -77,37 +77,37 @@ func (mt *Monitor) Append(worker *Worker) {
mt.workers = append(mt.workers, worker)
}
//SetWorkers 设置workers, 此操作会覆盖原有的workers
// SetWorkers 设置workers, 此操作会覆盖原有的workers
func (mt *Monitor) SetWorkers(workers WorkerList) {
mt.workers = workers
}
//SetStatus 设置DownloadStatus
// SetStatus 设置DownloadStatus
func (mt *Monitor) SetStatus(status *transfer.DownloadStatus) {
mt.status = status
}
//SetInstanceState 设置状态
// SetInstanceState 设置状态
func (mt *Monitor) SetInstanceState(instanceState *InstanceState) {
mt.instanceState = instanceState
}
//Status 返回DownloadStatus
// Status 返回DownloadStatus
func (mt *Monitor) Status() *transfer.DownloadStatus {
return mt.status
}
//Err 返回遇到的错误
// Err 返回遇到的错误
func (mt *Monitor) Err() error {
return mt.err
}
//CompletedChan 获取completed chan
// CompletedChan 获取completed chan
func (mt *Monitor) CompletedChan() <-chan struct{} {
return mt.completed
}
//GetAvailableWorker 获取空闲的worker
// GetAvailableWorker 获取空闲的worker
func (mt *Monitor) GetAvailableWorker() *Worker {
workerCount := len(mt.workers)
for i := mt.lastAvaliableIndex; i < mt.lastAvaliableIndex+workerCount; i++ {
@@ -121,7 +121,7 @@ func (mt *Monitor) GetAvailableWorker() *Worker {
return nil
}
//GetAllWorkersRange 获取所有worker的范围
// GetAllWorkersRange 获取所有worker的范围
func (mt *Monitor) GetAllWorkersRange() transfer.RangeList {
allWorkerRanges := make(transfer.RangeList, 0, len(mt.workers))
for _, worker := range mt.workers {
@@ -130,7 +130,7 @@ func (mt *Monitor) GetAllWorkersRange() transfer.RangeList {
return allWorkerRanges
}
//NumLeftWorkers 剩余的worker数量
// NumLeftWorkers 剩余的worker数量
func (mt *Monitor) NumLeftWorkers() (num int) {
for _, worker := range mt.workers {
if !worker.Completed() {
@@ -140,12 +140,12 @@ func (mt *Monitor) NumLeftWorkers() (num int) {
return
}
//SetReloadWorker 是否重载worker
// SetReloadWorker 是否重载worker
func (mt *Monitor) SetReloadWorker(b bool) {
mt.isReloadWorker = b
}
//IsLeftWorkersAllFailed 剩下的线程是否全部失败
// IsLeftWorkersAllFailed 剩下的线程是否全部失败
func (mt *Monitor) IsLeftWorkersAllFailed() bool {
failedNum := 0
for _, worker := range mt.workers {
@@ -161,7 +161,7 @@ func (mt *Monitor) IsLeftWorkersAllFailed() bool {
return failedNum != 0
}
//registerAllCompleted 全部完成则发送消息
// registerAllCompleted 全部完成则发送消息
func (mt *Monitor) registerAllCompleted() {
mt.completed = make(chan struct{}, 0)
var (
@@ -197,7 +197,7 @@ func (mt *Monitor) registerAllCompleted() {
}()
}
//ResetFailedAndNetErrorWorkers 重设部分网络错误的worker
// ResetFailedAndNetErrorWorkers 重设部分网络错误的worker
func (mt *Monitor) ResetFailedAndNetErrorWorkers() {
for k := range mt.workers {
if !mt.resetController.CanReset() {
@@ -221,7 +221,7 @@ func (mt *Monitor) ResetFailedAndNetErrorWorkers() {
}
}
//RangeWorker 遍历worker
// RangeWorker 遍历worker
func (mt *Monitor) RangeWorker(f RangeWorkerFunc) {
for k := range mt.workers {
if !f(k, mt.workers[k]) {
@@ -230,14 +230,14 @@ func (mt *Monitor) RangeWorker(f RangeWorkerFunc) {
}
}
//Pause 暂停所有的下载
// Pause 暂停所有的下载
func (mt *Monitor) Pause() {
for k := range mt.workers {
mt.workers[k].Pause()
}
}
//Resume 恢复所有的下载
// Resume 恢复所有的下载
func (mt *Monitor) Resume() {
for k := range mt.workers {
mt.workers[k].Resume()
@@ -346,7 +346,10 @@ func (mt *Monitor) ResetWorker(worker *Worker) {
// 忽略, 返回
return
case StatusCodeDownloadUrlExpired: // 下载链接已经过期
worker.RefreshDownloadUrl()
logger.Verbosef("download url expired, reset worker: %d\n", worker.ID())
worker.RefreshDownloadUrl()
break
case StatusCodeDownloadUrlExceedMaxConcurrency: // 下载遇到限流报错
break
}
@@ -357,7 +360,7 @@ func (mt *Monitor) ResetWorker(worker *Worker) {
worker.Reset()
}
//Execute 执行任务
// Execute 执行任务
func (mt *Monitor) Execute(cancelCtx context.Context) {
if len(mt.workers) == 0 {
mt.err = ErrNoWokers
@@ -406,7 +409,7 @@ func (mt *Monitor) Execute(cancelCtx context.Context) {
// 是否有失败的worker
for _, w := range mt.workers {
if w.status.statusCode == StatusCodeDownloadUrlExpired {
if w.status.statusCode == StatusCodeDownloadUrlExpired || w.status.statusCode == StatusCodeDownloadUrlExceedMaxConcurrency {
mt.ResetWorker(w)
}
}
+14 -6
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -63,9 +63,13 @@ const (
StatusCodeCanceled
//StatusCodeDownloadUrlExpired 下载链接已过期
StatusCodeDownloadUrlExpired
// StatusCodeDownloadUrlExceedMaxConcurrency 限流报错,下载链接超过最大并发数
StatusCodeDownloadUrlExceedMaxConcurrency
//StatusCodeIllegalDownloadFile 文件非法,不允许下载
StatusCodeIllegalDownloadFile
)
//GetStatusText 根据状态码获取状态信息
// GetStatusText 根据状态码获取状态信息
func GetStatusText(sc StatusCode) string {
switch sc {
case StatusCodeInit:
@@ -92,29 +96,33 @@ func GetStatusText(sc StatusCode) string {
return "已重设连接"
case StatusCodeCanceled:
return "已取消"
case StatusCodeDownloadUrlExpired:
return "链接已过期"
case StatusCodeDownloadUrlExceedMaxConcurrency:
return "遇到限流报错"
default:
return "未知状态码"
}
}
//NewWorkerStatus 初始化WorkerStatus
// NewWorkerStatus 初始化WorkerStatus
func NewWorkerStatus() *WorkerStatus {
return &WorkerStatus{
statusCode: StatusCodeInit,
}
}
//SetStatusCode 设置worker状态码
// SetStatusCode 设置worker状态码
func (ws *WorkerStatus) SetStatusCode(sc StatusCode) {
ws.statusCode = sc
}
//StatusCode 返回状态码
// StatusCode 返回状态码
func (ws *WorkerStatus) StatusCode() StatusCode {
return ws.statusCode
}
//StatusText 返回状态信息
// StatusText 返回状态信息
func (ws *WorkerStatus) StatusText() string {
return GetStatusText(ws.statusCode)
}
+24 -3
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,8 +14,7 @@
package downloader
import (
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"errors"
mathrand "math/rand"
"mime"
"net/url"
@@ -23,6 +22,10 @@ import (
"regexp"
"strconv"
"time"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
)
var (
@@ -34,6 +37,9 @@ var (
// ran 一个随机数实例
ran = mathrand.New(ranSource)
// 文件被禁止下载
ErrFileDownloadForbidden = errors.New("文件被禁止下载")
)
// RandomNumber 生成指定区间随机数
@@ -95,3 +101,18 @@ func fixCacheSize(size *int) {
*size = 1024
}
}
// IsUrlExpired 下载链接是否已过期。过期返回True
func IsUrlExpired(urlStr string) bool {
expiredTimeSec, err := utils.GetExpiredTimeSecFromOSSUrl(urlStr)
if err != nil {
// 解析错误,默认过期
return true
}
if (expiredTimeSec - time.Now().Unix()) <= 5 { // 小于5秒钟
// expired
return true
}
// 返回未过期
return false
}
+102 -51
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,32 +19,37 @@ import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/cachepool"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"github.com/tickstep/library-go/requester/rio/speeds"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"io"
"io/ioutil"
"net/http"
"strings"
"sync"
"time"
)
type (
//Worker 工作单元
Worker struct {
totalSize int64 // 整个文件的大小, worker请求range时会获取尝试获取该值, 如果不匹配, 则返回错误
wrange *transfer.Range
speedsStat *speeds.Speeds
id int // work id
fileId string // 文件ID
driveId string
url string // 下载地址
acceptRanges string
panClient *aliyunpan.PanClient
client *requester.HTTPClient
writerAt io.WriterAt
writeMu *sync.Mutex
execMu sync.Mutex
totalSize int64 // 整个文件的大小, worker请求range时会获取尝试获取该值, 如果不匹配, 则返回错误
wrange *transfer.Range
speedsStat *speeds.Speeds
globalSpeedsStat *speeds.Speeds // 全局速度统计
id int // work id
fileId string // 文件ID
driveId string
url string // 下载地址
acceptRanges string
panClient *config.PanClient
client *requester.HTTPClient
writerAt io.WriterAt
writeMu *sync.Mutex
execMu sync.Mutex
pauseChan chan struct{}
workerCancelFunc context.CancelFunc
@@ -66,18 +71,19 @@ func (wl WorkerList) Duplicate() WorkerList {
return n
}
//NewWorker 初始化Worker
func NewWorker(id int, driveId string, fileId, durl string, writerAt io.WriterAt) *Worker {
// NewWorker 初始化Worker
func NewWorker(id int, driveId string, fileId, durl string, writerAt io.WriterAt, globalSpeedsStat *speeds.Speeds) *Worker {
return &Worker{
id: id,
url: durl,
writerAt: writerAt,
fileId: fileId,
driveId: driveId,
id: id,
url: durl,
writerAt: writerAt,
fileId: fileId,
driveId: driveId,
globalSpeedsStat: globalSpeedsStat,
}
}
//ID 返回worker ID
// ID 返回worker ID
func (wer *Worker) ID() int {
return wer.id
}
@@ -107,21 +113,21 @@ func (wer *Worker) SetTotalSize(size int64) {
wer.totalSize = size
}
//SetClient 设置http客户端
// SetClient 设置http客户端
func (wer *Worker) SetClient(c *requester.HTTPClient) {
wer.client = c
}
func (wer *Worker) SetPanClient(p *aliyunpan.PanClient) {
func (wer *Worker) SetPanClient(p *config.PanClient) {
wer.panClient = p
}
//SetAcceptRange 设置AcceptRange
// SetAcceptRange 设置AcceptRange
func (wer *Worker) SetAcceptRange(acceptRanges string) {
wer.acceptRanges = acceptRanges
}
//SetRange 设置请求范围
// SetRange 设置请求范围
func (wer *Worker) SetRange(r *transfer.Range) {
if wer.wrange == nil {
wer.wrange = r
@@ -131,33 +137,38 @@ func (wer *Worker) SetRange(r *transfer.Range) {
wer.wrange.StoreEnd(r.LoadEnd())
}
//SetWriteMutex 设置数据写锁
// SetWriteMutex 设置数据写锁
func (wer *Worker) SetWriteMutex(mu *sync.Mutex) {
wer.writeMu = mu
}
//SetDownloadStatus 增加其他需要统计的数据
// SetDownloadStatus 增加其他需要统计的数据
func (wer *Worker) SetDownloadStatus(downloadStatus *transfer.DownloadStatus) {
wer.downloadStatus = downloadStatus
}
//GetStatus 返回下载状态
// SetUrl 更新新的下载路径
func (wer *Worker) SetUrl(newUrl string) {
wer.url = newUrl
}
// GetStatus 返回下载状态
func (wer *Worker) GetStatus() WorkerStatuser {
// 空接口与空指针不等价
return &wer.status
}
//GetRange 返回worker范围
// GetRange 返回worker范围
func (wer *Worker) GetRange() *transfer.Range {
return wer.wrange
}
//GetSpeedsPerSecond 获取每秒的速度
// GetSpeedsPerSecond 获取每秒的速度
func (wer *Worker) GetSpeedsPerSecond() int64 {
return wer.speedsStat.GetSpeeds()
}
//Pause 暂停下载
// Pause 暂停下载
func (wer *Worker) Pause() {
wer.lazyInit()
if wer.acceptRanges == "" {
@@ -172,7 +183,7 @@ func (wer *Worker) Pause() {
wer.status.statusCode = StatusCodePaused
}
//Resume 恢复下载
// Resume 恢复下载
func (wer *Worker) Resume() {
if wer.status.statusCode != StatusCodePaused {
return
@@ -180,7 +191,7 @@ func (wer *Worker) Resume() {
go wer.Execute()
}
//Cancel 取消下载
// Cancel 取消下载
func (wer *Worker) Cancel() error {
if wer.workerCancelFunc == nil {
return errors.New("cancelFunc not set")
@@ -192,8 +203,9 @@ func (wer *Worker) Cancel() error {
return nil
}
//Reset 重设连接
// Reset 重设连接
func (wer *Worker) Reset() {
logger.Verbosef("Reset worker: %d\n", wer.ID())
if wer.resetFunc == nil {
logger.Verbosef("DEBUG: worker: resetFunc not set")
return
@@ -209,13 +221,15 @@ func (wer *Worker) Reset() {
// RefreshDownloadUrl 重新刷新下载链接
func (wer *Worker) RefreshDownloadUrl() {
var apierr *apierror.ApiError
durl, apierr := wer.panClient.GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{DriveId: wer.driveId, FileId: wer.fileId})
logger.Verbosef("get new download url for worker: %d\n", wer.ID())
durl, apierr := wer.panClient.OpenapiPanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{DriveId: wer.driveId, FileId: wer.fileId})
if apierr != nil {
logger.Verbosef("get new download url for worker: %d, error: %+v\n", wer.ID(), apierr)
wer.status.statusCode = StatusCodeTooManyConnections
return
}
wer.url = durl.Url
logger.Verbosef("get new download url for worker: %d, new url: %s\n", wer.ID(), wer.url)
}
// Canceled 是否已经取消
@@ -223,7 +237,7 @@ func (wer *Worker) Canceled() bool {
return wer.status.statusCode == StatusCodeCanceled
}
//Completed 是否已经完成
// Completed 是否已经完成
func (wer *Worker) Completed() bool {
switch wer.status.statusCode {
case StatusCodeSuccessed, StatusCodeCanceled:
@@ -233,7 +247,7 @@ func (wer *Worker) Completed() bool {
}
}
//Failed 是否失败
// Failed 是否失败
func (wer *Worker) Failed() bool {
switch wer.status.statusCode {
case StatusCodeFailed, StatusCodeInternalError, StatusCodeTooManyConnections, StatusCodeNetError:
@@ -243,18 +257,19 @@ func (wer *Worker) Failed() bool {
}
}
//ClearStatus 清空状态
// ClearStatus 清空状态
func (wer *Worker) ClearStatus() {
wer.status.statusCode = StatusCodeInit
}
//Err 返回worker错误
// Err 返回worker错误
func (wer *Worker) Err() error {
return wer.err
}
//Execute 执行任务
// Execute 执行任务
func (wer *Worker) Execute() {
logger.Verbosef("Execute worker: %d\n", wer.ID())
wer.lazyInit()
wer.execMu.Lock()
@@ -292,11 +307,19 @@ func (wer *Worker) Execute() {
wer.status.statusCode = StatusCodePending
var resp *http.Response
// check url expired or not
if IsUrlExpired(wer.url) {
logger.Verbosef("download url expired, renew url and reset worker: %d\n", wer.ID())
wer.status.statusCode = StatusCodeDownloadUrlExpired
wer.err = errors.New("403")
return
}
apierr := wer.panClient.DownloadFileData(wer.url, aliyunpan.FileDownloadRange{
// do download data
var resp *http.Response
apierr := wer.panClient.OpenapiPanClient().DownloadFileData(wer.url, aliyunpan.FileDownloadRange{
Offset: wer.wrange.Begin,
End: wer.wrange.End - 1,
End: wer.wrange.End - 1,
}, func(httpMethod, fullUrl string, headers map[string]string) (*http.Response, error) {
resp, wer.err = wer.client.Req(httpMethod, fullUrl, nil, headers)
if wer.err != nil {
@@ -326,16 +349,41 @@ func (wer *Worker) Execute() {
break
case 416: //Requested Range Not Satisfiable
fallthrough
case 403: // Forbidden
fallthrough
case 403: // 链接过期也会返回403
if respBody, e := ioutil.ReadAll(resp.Body); e == nil {
respBodyStr := string(respBody)
if strings.Contains(respBodyStr, "Request has expired") { // 链接已过期
logger.Verboseln("download url return 403 error and expired url response")
wer.status.statusCode = StatusCodeDownloadUrlExpired
wer.err = errors.New(resp.Status)
} else if strings.Contains(respBodyStr, "ExceedMaxConcurrency") { // 遇到限流报错
// 普通应用:文件分片下载的并发数为 3,即某用户使用 App 时,可以同时下载 1 个文件的 3 个分片,或者同时下载 3 个文件的各 1 个分片。
// 超过并发,再次调用接口,报错 http status:403。示例报错信息如下:
// <?xml version="1.0" encoding="UTF-8"?>
//<Error>
// <Code>RequestDeniedByCallback</Code>
// <Message>Callback deny this request reason: ExceedMaxConcurrency</Message>
// <RequestId>66B5720887CECD32333EB8C1</RequestId>
// <HostId>cn-beijing-data.aliyundrive.net</HostId>
// <EC>0007-00000209</EC>
// <RecommendDoc>https://api.aliyun.com/troubleshoot?q=0007-00000209</RecommendDoc>
//</Error>
logger.Verboseln("download url return 403 error and exceed max concurrency response")
wer.status.statusCode = StatusCodeDownloadUrlExceedMaxConcurrency
wer.err = errors.New(resp.Status)
// 遇到限流,本线程延迟后,再重试
time.Sleep(10 * time.Second)
}
}
return
case 406: // Not Acceptable
wer.status.statusCode = StatusCodeNetError
wer.err = errors.New(resp.Status)
return
case 404:
wer.status.statusCode = StatusCodeDownloadUrlExpired
wer.err = errors.New(resp.Status)
return
logger.Verboseln("request download url 404 error")
fallthrough
case 429, 509: // Too Many Requests
wer.status.SetStatusCode(StatusCodeTooManyConnections)
wer.err = errors.New(resp.Status)
@@ -405,6 +453,9 @@ func (wer *Worker) Execute() {
wer.downloadStatus.AddSpeedsDownloaded(nn64) // 限速在这里阻塞
}
wer.speedsStat.Add(nn64)
if wer.globalSpeedsStat != nil {
wer.globalSpeedsStat.Add(nn64)
}
n += nn
}
+4
View File
@@ -16,6 +16,7 @@ package downloader
import (
"io"
"os"
"runtime"
)
type (
@@ -32,6 +33,9 @@ type (
// NewDownloaderWriterByFilename 创建下载器数据输出接口, 类似于os.OpenFile
func NewDownloaderWriterByFilename(name string, flag int, perm os.FileMode) (writer Writer, file *os.File, err error) {
if runtime.GOOS == "windows" {
// TODO: name进行特殊字符转换
}
file, err = os.OpenFile(name, flag, perm)
if err != nil {
return
+24 -13
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,8 +16,8 @@ package uploader
import (
"bufio"
"fmt"
"github.com/tickstep/library-go/requester/rio/speeds"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/requester/rio/speeds"
"io"
"os"
"sync"
@@ -30,15 +30,17 @@ type (
io.Seeker
Range() transfer.Range
Left() int64
ResetReader(readerAt io.ReaderAt)
}
fileBlock struct {
readRange transfer.Range
readed int64
readerAt io.ReaderAt
speedsStatRef *speeds.Speeds
rateLimit *speeds.RateLimit
mu sync.Mutex
readRange transfer.Range
readed int64
readerAt io.ReaderAt
speedsStatRef *speeds.Speeds
globalSpeedsStatRef *speeds.Speeds
rateLimit *speeds.RateLimit
mu sync.Mutex
}
bufioFileBlock struct {
@@ -63,12 +65,13 @@ func SplitBlock(fileSize, blockSize int64) (blockList []*BlockState) {
}
// NewBufioSplitUnit io.ReaderAt实现SplitUnit接口, 有Buffer支持
func NewBufioSplitUnit(readerAt io.ReaderAt, readRange transfer.Range, speedsStat *speeds.Speeds, rateLimit *speeds.RateLimit) SplitUnit {
func NewBufioSplitUnit(readerAt io.ReaderAt, readRange transfer.Range, speedsStat *speeds.Speeds, rateLimit *speeds.RateLimit, globalSpeedsStat *speeds.Speeds) SplitUnit {
su := &fileBlock{
readerAt: readerAt,
readRange: readRange,
speedsStatRef: speedsStat,
rateLimit: rateLimit,
readerAt: readerAt,
readRange: readRange,
speedsStatRef: speedsStat,
globalSpeedsStatRef: globalSpeedsStat,
rateLimit: rateLimit,
}
return &bufioFileBlock{
fileBlock: su,
@@ -104,6 +107,9 @@ func (fb *fileBlock) Read(b []byte) (n int, err error) {
if fb.speedsStatRef != nil {
fb.speedsStatRef.Add(n64)
}
if fb.globalSpeedsStatRef != nil {
fb.globalSpeedsStatRef.Add(n64)
}
return
}
@@ -142,3 +148,8 @@ func (fb *fileBlock) Range() transfer.Range {
func (fb *fileBlock) Readed() int64 {
return fb.readed
}
func (fb *fileBlock) ResetReader(readerAt io.ReaderAt) {
fb.readerAt = readerAt
fb.readed = 0
}
+1 -1
View File
@@ -36,7 +36,7 @@ func TestSplitBlock(t *testing.T) {
func TestSplitUnitRead(t *testing.T) {
var size int64 = 65536*2+3432
buffer := rio.NewBuffer(cachepool.RawMallocByteSlice(int(size)))
unit := uploader.NewBufioSplitUnit(buffer, transfer.Range{Begin: 2, End: size}, nil, nil)
unit := uploader.NewBufioSplitUnit(buffer, transfer.Range{Begin: 2, End: size}, nil, nil, nil)
buf := cachepool.RawMallocByteSlice(1022)
for {
+18 -3
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,16 +13,31 @@
// limitations under the License.
package uploader
import "fmt"
var (
UploadUrlExpired = fmt.Errorf("UrlExpired")
UploadPartNotSeq = fmt.Errorf("PartNotSequential")
UploadNoSuchUpload = fmt.Errorf("NoSuchUpload")
UploadTerminate = fmt.Errorf("UploadErrorTerminate")
UploadPartAlreadyExist = fmt.Errorf("PartAlreadyExist")
UploadHttpError = fmt.Errorf("HttpError")
UploadLocalFileAlreadyClosedError = fmt.Errorf("LocalFileAlreadyClosedError")
)
type (
// MultiError 多线程上传的错误
MultiError struct {
Err error
// IsRetry 是否重试,
Terminated bool
Terminated bool
NeedStartOver bool // 是否从头开始上传
}
)
func (me *MultiError) Error() string {
return me.Err.Error()
if me.Err != nil {
return me.Err.Error()
}
return ""
}

Some files were not shown because too many files have changed in this diff Show More