Compare commits

..

670 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
tickstep 127b7f0662 update version to v0.0.6 2021-11-13 17:34:44 +08:00
feny 04a1548d5d update readme file 2021-11-12 10:50:52 +08:00
tickstep 3d22751c7a format file tag 2021-11-11 23:30:27 +08:00
tickstep a28ed463e7 support use the internal download upload url for aliyun ECS 2021-11-11 23:24:56 +08:00
tickstep 25b51dd763 update version to v0.0.5 2021-10-31 16:13:42 +08:00
tickstep bf26202290 fix file name auto complete issue 2021-10-31 15:53:27 +08:00
tickstep 53bdf34691 update version to v0.0.4 2021-10-30 23:27:50 +08:00
tickstep 6fdebc88a5 update readme 2021-10-30 23:04:06 +08:00
tickstep c1cb3a60dd support file name auto complete input 2021-10-30 22:53:08 +08:00
feny 4782cec115 update gitignore file 2021-10-28 09:00:27 +08:00
tickstep f15de22ce2 add token command 2021-10-27 22:13:29 +08:00
tickstep 50cc159cc6 update version to v0.0.3 2021-10-24 23:52:54 +08:00
tickstep 4ed3da6097 pretty format output 2021-10-24 23:38:19 +08:00
tickstep fe60915147 remove unsupport commands 2021-10-24 23:06:35 +08:00
tickstep 267cf937be format time string 2021-10-24 22:25:40 +08:00
tickstep 971369e968 add proof code for upload to support rapid upload 2021-10-24 22:03:47 +08:00
tickstep 60f3e1a0a4 init project 2021-10-10 10:48:53 +08:00
tickstep 66742754aa update rename file 2021-09-05 22:08:09 +08:00
198 changed files with 29976 additions and 511 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功能。" -->
### Ⅲ. 请描述新功能的使用场景
<!-- 可以讲讲你对新功能的看法,为什么需要增加这个功能,这个新功能在哪些场景下会使用。 -->
+5 -2
View File
@@ -40,5 +40,8 @@ tests.sh
main_test.go
license_header.sh
account.txt
main_test.go
git_push.sh
git_push.sh
git_pull.sh
docker/sync/data
clear_docker.sh
upload.sh
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
+331 -509
View File
@@ -1,539 +1,351 @@
# 关于
阿里云盘CLI。仿 Linux shell 文件处理命令的阿里云盘命令行客户端。
阿里云盘CLI。仿 Linux shell 文件处理命令的阿里云盘命令行客户端,支持JavaScript插件,支持同步备份功能,支持相册批量下载。
# 相关说明
1. 本项目还处于开发阶段,未经过充分的测试,如有bug或者好的建议欢迎提交issue
2. 由于阿里云盘还在内测中,后面功能和接口随时会被修改,相对应的,本工具相关功能也会被影响
3. 目前阶段优先处理Bug,功能增强的开发会相对往后安排
# 特色
1. 多平台支持, 支持 Windows, macOS, linux(x86/x64/arm), android, iOS 等
2. 阿里云盘多用户支持
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)
# 目录
- [关于](#关于)
- [特色](#特色)
- [下载/运行 说明](#下载运行说明)
* [Windows](#windows)
* [Linux / macOS](#linux--macos)
- [命令列表及说明](#命令列表及说明)
* [注意](#注意)
* [检测程序更新](#检测程序更新)
* [查看帮助](#查看帮助)
* [登录阿里云盘帐号](#登录阿里云盘帐号)
* [列出帐号列表](#列出帐号列表)
* [获取当前帐号](#获取当前帐号)
* [切换阿里云盘帐号](#切换阿里云盘帐号)
* [退出阿里云盘帐号](#退出阿里云盘帐号)
* [切换网盘(文件/相册)](#切换网盘)
* [获取网盘配额](#获取网盘配额)
* [切换工作目录](#切换工作目录)
* [输出工作目录](#输出工作目录)
* [列出目录](#列出目录)
* [下载文件/目录](#下载文件目录)
* [上传文件/目录](#上传文件目录)
* [备份本地文件/目录](#备份本地文件目录)
* [手动秒传文件](#手动秒传文件)
* [创建目录](#创建目录)
* [删除文件/目录](#删除文件目录)
* [移动文件/目录](#移动文件目录)
* [重命名文件/目录](#重命名文件目录)
* [导出文件](#导出文件)
* [导入文件](#导入文件)
* [分享文件/目录](#分享文件目录)
+ [设置分享文件/目录](#设置分享文件目录)
+ [列出已分享文件/目录](#列出已分享文件目录)
+ [取消分享文件/目录](#取消分享文件目录)
+ [分享秒传链接](#分享秒传链接)
* [显示和修改程序配置项](#显示和修改程序配置项)
- [常见问题Q&A](#常见问题Q&A)
* [1. 如何获取RefreshToken](#1-如何获取RefreshToken)
* [2. 如何开启Debug调试日志](#1-如何开启Debug调试日志)
- [目录](#目录)
- [如何安装](#如何安装)
- [直接下载安装](#直接下载安装)
- [apt安装](#apt安装)
- [yum安装](#yum安装)
- [brew安装](#brew安装)
- [winget安装](#winget安装)
- [docker安装](#docker安装)
- [sync同步盘](#sync同步盘)
- [如何使用](#如何使用)
- [基本使用](#基本使用)
- [修改配置目录](#修改配置目录)
- [启动程序](#启动程序)
- [查看帮助](#查看帮助)
- [登录](#登录)
- [切换网盘](#切换网盘)
- [查看文件列表](#查看文件列表)
- [下载文件](#下载文件)
- [上传文件](#上传文件)
- [同步备份文件](#同步备份文件)
- [更多命令](#更多命令)
- [常见问题](#常见问题)
- [如何开启Debug调试日志](#如何开启debug调试日志)
- [第一步](#第一步)
- [第二步](#第二步)
- [如何登出和下线客户端](#如何登出和下线客户端)
- [交流反馈](#交流反馈)
- [鸣谢](#鸣谢)
# 如何安装
## 直接下载安装
可以直接在本仓库 [发布页](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
# 特色
1. 多平台支持, 支持 Windows, macOS, linux(x86/x64/arm)等
2. 阿里云盘多用户支持
3. 支持文件网盘,相册网盘无缝切换
4. 支持导入/导出功能,快速备份(导出)和恢复(导入)网盘的文件
5. [下载](#下载文件目录)网盘内文件, 支持多个文件或目录下载, 支持断点续传和单文件并行下载
6. [上传](#上传文件目录)本地文件, 支持多个文件或目录上传,支持排除指定文件夹/文件(正则表达式)功能
7. [备份本地文件](#备份本地文件目录)支持备份本地文件夹到网盘中,保持本地文件和网盘文件同步。常用于嵌入式或者NAS等设备。
# 下载/运行说明
可以直接在[发布页](https://github.com/tickstep/aliyunpan/releases)下载使用.
也可以使用这个天翼云盘分享链接(阿里云盘还不支持zip压缩包分享)进行下载:[下载链接](https://cloud.189.cn/t/z2auuiFfYb2i)(访问码:5o9b)
如果程序运行时输出乱码, 请检查下终端的编码方式是否为 `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
```
程序会进一步确认退出帐号, 防止误操作.
## 切换网盘
程序默认工作在文件网盘下,如需切换到相册网盘,可以使用本命令进行切换。
```
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
```
# 常见问题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日志。步骤如下:
### 第一步
@@ -545,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)
+14
View File
@@ -0,0 +1,14 @@
<?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.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="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

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"
}
]
}
Executable
+163
View File
@@ -0,0 +1,163 @@
#!/bin/sh
# how to use
# for macOS & linux, run this command in shell
# ./build.sh v0.1.0
name="aliyunpan"
version=$1
if [ "$1" = "" ]; then
version=v1.0.0
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
}
Build() {
default_golang
goarm=$4
if [ "$4" = "" ]; then
goarm=7
fi
echo "Building $1..."
export GOOS=$2 GOARCH=$3 GO386=sse2 CGO_ENABLED=0 GOARM=$4
if [ $2 = "windows" ]; then
goversioninfo -o=resource_windows_386.syso
goversioninfo -64 -o=resource_windows_amd64.syso
$go build -ldflags "-X main.Version=$version -s -w" -o "$output/$1/$name.exe"
RicePack $1 $name.exe
else
$go build -ldflags "-X main.Version=$version -s -w" -o "$output/$1/$name"
RicePack $1 $name
fi
Pack $1 $2
}
AndroidBuild() {
default_golang
echo "Building $1..."
export GOOS=$2 GOARCH=$3 GOARM=$4 CGO_ENABLED=1
$go build -ldflags "-X main.Version=$version -s -w -linkmode=external -extldflags=-pie" -o "$output/$1/$name"
RicePack $1 $name
Pack $1 $2
}
IOSBuild() {
default_golang
echo "Building $1..."
mkdir -p "$output/$1"
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
jtool2 --sign --inplace --ent ../../entitlements.xml $name
cd ../..
RicePack $1 $name
Pack $1 "ios"
}
# zip 打包
Pack() {
if [ $2 != "windows" ]; then
chmod +x "$output/$1/$name"
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"
# 删除
rm -rf "$1"
cd ..
}
# rice 打包静态资源
RicePack() {
return # 已取消web功能
}
############### 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 ###############
IOSBuild $name-$version"-ios-arm64"
############### 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 ###############
Build $name-$version"-windows-x86" windows 386
Build $name-$version"-windows-x64" windows amd64
Build $name-$version"-windows-arm64" windows arm64
############### 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
# 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
# Build $name-$version"-openbsd-386" openbsd 386
# Build $name-$version"-openbsd-amd64" openbsd amd64
# Build $name-$version"-openbsd-arm" openbsd arm
# Build $name-$version"-plan9-386" plan9 386
# Build $name-$version"-plan9-amd64" plan9 amd64
# Build $name-$version"-plan9-arm" plan9 arm
# Build $name-$version"-nacl-386" nacl 386
# Build $name-$version"-nacl-amd64p32" nacl amd64p32
# Build $name-$version"-nacl-arm" nacl arm
# Build $name-$version"-dragonflybsd-amd64" dragonfly amd64
+17
View File
@@ -0,0 +1,17 @@
package cmder
import (
"github.com/urfave/cli"
)
var (
appInstance *cli.App
)
func SetApp(app *cli.App) {
appInstance = app
}
func App() *cli.App {
return appInstance
}
+97
View File
@@ -0,0 +1,97 @@
// 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 args
import (
"strings"
"unicode"
)
const (
CharEscape = '\\'
CharSingleQuote = '\''
CharDoubleQuote = '"'
CharBackQuote = '`'
)
// IsQuote 是否为引号
func IsQuote(r rune) bool {
return r == CharSingleQuote || r == CharDoubleQuote || r == CharBackQuote
}
// Parse 解析line, 忽略括号
func Parse(line string) (lineArgs []string) { // 在函数中定义的返回值变量,会自动赋为 zero-value,即相当于 var lineArgs string[]
var (
rl = []rune(line + " ")
buf = strings.Builder{}
quoteChar rune
nextChar rune
escaped bool
in bool
)
var (
isSpace bool
)
for k, r := range rl {
isSpace = unicode.IsSpace(r)
if !isSpace && !in {
in = true
}
switch {
case escaped: // 已转义, 跳过
escaped = false
//pass
case r == CharEscape: // 转义模式
if k+1+1 < len(rl) { // 不是最后一个字符, 多+1是因为最后一个空格
nextChar = rl[k+1]
// 仅支持转义这些字符, 否则原样输出反斜杠
if unicode.IsSpace(nextChar) || IsQuote(nextChar) || nextChar == CharEscape {
escaped = true
continue
}
}
// pass
case IsQuote(r):
if quoteChar == 0 { //未引
quoteChar = r
continue
}
if quoteChar == r { //取消引
quoteChar = 0
continue
}
case isSpace:
if !in { // 忽略多余的空格
continue
}
if quoteChar == 0 { // 未在引号内
lineArgs = append(lineArgs, buf.String())
buf.Reset()
in = false
continue
}
}
buf.WriteRune(r)
}
// Go 允许在定义函数时,命名返回值,当然这些变量可以在函数中使用。
// 在 return 语句中,无需显示的返回这些值,Go 会自动将其返回。当然 return 语句还是必须要写的,否则编译器会报错。
// 相当于 return lineArgs
return
}
+17
View File
@@ -0,0 +1,17 @@
// +build !windows
package cmdliner
import (
"fmt"
)
// ClearScreen 清空屏幕
func (pl *CmdLiner) ClearScreen() {
ClearScreen()
}
// ClearScreen 清空屏幕
func ClearScreen() {
fmt.Print("\x1b[H\x1b[2J")
}
+69
View File
@@ -0,0 +1,69 @@
// 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 cmdliner
import (
"syscall"
"unsafe"
)
const (
std_output_handle = uint32(-11 & 0xFFFFFFFF)
)
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procGetStdHandle = kernel32.NewProc("GetStdHandle")
procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
)
type (
coord struct {
x, y int16
}
smallRect struct {
left, top, right, bottom int16
}
consoleScreenBufferInfo struct {
dwSize coord
dwCursorPosition coord
wAttributes int16
srWindow smallRect
dwMaximumWindowSize coord
}
)
// ClearScreen 清空屏幕
func (pl *CmdLiner) ClearScreen() {
ClearScreen()
}
// ClearScreen 清空屏幕
func ClearScreen() {
out, _, _ := procGetStdHandle.Call(uintptr(std_output_handle))
hOut := syscall.Handle(out)
var sbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(hOut), uintptr(unsafe.Pointer(&sbi)))
var numWritten uint32
procFillConsoleOutputCharacter.Call(uintptr(hOut), uintptr(' '),
uintptr(sbi.dwSize.x)*uintptr(sbi.dwSize.y),
0,
uintptr(unsafe.Pointer(&numWritten)))
procSetConsoleCursorPosition.Call(uintptr(hOut), 0)
}
+82
View File
@@ -0,0 +1,82 @@
// 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 cmdliner
import (
"github.com/peterh/liner"
)
// CmdLiner 封装 *liner.State, 提供更简便的操作
type CmdLiner struct {
State *liner.State
History *LineHistory
tmode liner.ModeApplier
lmode liner.ModeApplier
paused bool
}
// NewLiner 返回 *CmdLiner, 默认设置允许 Ctrl+C 结束
func NewLiner() *CmdLiner {
pl := &CmdLiner{}
pl.tmode, _ = liner.TerminalMode()
line := liner.NewLiner()
pl.lmode, _ = liner.TerminalMode()
line.SetMultiLineMode(true)
line.SetCtrlCAborts(true)
pl.State = line
return pl
}
// Pause 暂停服务
func (pl *CmdLiner) Pause() error {
if pl.paused {
panic("CmdLiner already paused")
}
pl.paused = true
pl.DoWriteHistory()
return pl.tmode.ApplyMode()
}
// Resume 恢复服务
func (pl *CmdLiner) Resume() error {
if !pl.paused {
panic("CmdLiner is not paused")
}
pl.paused = false
return pl.lmode.ApplyMode()
}
// Close 关闭服务
func (pl *CmdLiner) Close() (err error) {
err = pl.State.Close()
if err != nil {
return err
}
if pl.History != nil && pl.History.historyFile != nil {
return pl.History.historyFile.Close()
}
return nil
}
+68
View File
@@ -0,0 +1,68 @@
// 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 cmdliner
import (
"fmt"
"os"
)
// LineHistory 命令行历史
type LineHistory struct {
historyFilePath string
historyFile *os.File
}
// NewLineHistory 设置历史
func NewLineHistory(filePath string) (lh *LineHistory, err error) {
lh = &LineHistory{
historyFilePath: filePath,
}
lh.historyFile, err = os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return nil, err
}
return lh, nil
}
// DoWriteHistory 执行写入历史
func (pl *CmdLiner) DoWriteHistory() (err error) {
if pl.History == nil {
return fmt.Errorf("history not set")
}
pl.History.historyFile, err = os.Create(pl.History.historyFilePath)
if err != nil {
return fmt.Errorf("写入历史错误, %s", err)
}
_, err = pl.State.WriteHistory(pl.History.historyFile)
if err != nil {
return fmt.Errorf("写入历史错误: %s", err)
}
return nil
}
// ReadHistory 读取历史
func (pl *CmdLiner) ReadHistory() (err error) {
if pl.History == nil {
return fmt.Errorf("history not set")
}
_, err = pl.State.ReadHistory(pl.History.historyFile)
return err
}
+33
View File
@@ -0,0 +1,33 @@
// 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 cmdtable
import (
"github.com/olekukonko/tablewriter"
"io"
)
type CmdTable struct {
*tablewriter.Table
}
// NewTable 预设了一些配置
func NewTable(wt io.Writer) CmdTable {
tb := tablewriter.NewWriter(wt)
tb.SetAutoWrapText(false)
tb.SetBorder(false)
tb.SetHeaderLine(false)
tb.SetColumnSeparator("")
return CmdTable{tb}
}
+45
View File
@@ -0,0 +1,45 @@
// 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 cmdutil
import (
"net"
)
// ListAddresses 列出本地可用的 IP 地址
func ListAddresses() (addresses []string) {
iFaces, _ := net.Interfaces()
addresses = make([]string, 0, len(iFaces))
for k := range iFaces {
iFaceAddrs, _ := iFaces[k].Addrs()
for l := range iFaceAddrs {
switch v := iFaceAddrs[l].(type) {
case *net.IPNet:
addresses = append(addresses, v.IP.String())
case *net.IPAddr:
addresses = append(addresses, v.IP.String())
}
}
}
return
}
// ParseHost 解析地址中的host
func ParseHost(address string) string {
h, _, err := net.SplitHostPort(address)
if err != nil {
return address
}
return h
}
+98
View File
@@ -0,0 +1,98 @@
// 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 cmdutil
import (
"compress/gzip"
"flag"
"io"
"io/ioutil"
"net/http/cookiejar"
"net/url"
"strings"
)
// TrimPathPrefix 去除目录的前缀
func TrimPathPrefix(path, prefixPath string) string {
if prefixPath == "/" {
return path
}
return strings.TrimPrefix(path, prefixPath)
}
// ContainsString 检测字符串是否在字符串数组里
func ContainsString(ss []string, s string) bool {
for k := range ss {
if ss[k] == s {
return true
}
}
return false
}
// GetURLCookieString 返回cookie字串
func GetURLCookieString(urlString string, jar *cookiejar.Jar) string {
u, _ := url.Parse(urlString)
cookies := jar.Cookies(u)
cookieString := ""
for _, v := range cookies {
cookieString += v.String() + "; "
}
cookieString = strings.TrimRight(cookieString, "; ")
return cookieString
}
// DecompressGZIP 对 io.Reader 数据, 进行 gzip 解压
func DecompressGZIP(r io.Reader) ([]byte, error) {
gzipReader, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
gzipReader.Close()
return ioutil.ReadAll(gzipReader)
}
// FlagProvided 检测命令行是否提供名为 name 的 flag, 支持多个name(names)
func FlagProvided(names ...string) bool {
if len(names) == 0 {
return false
}
var targetFlag *flag.Flag
for _, name := range names {
targetFlag = flag.Lookup(name)
if targetFlag == nil {
return false
}
if targetFlag.DefValue == targetFlag.Value.String() {
return false
}
}
return true
}
// Trigger 用于触发事件
func Trigger(f func()) {
if f == nil {
return
}
go f()
}
// TriggerOnSync 用于触发事件, 同步触发
func TriggerOnSync(f func()) {
if f == nil {
return
}
f()
}
+76
View File
@@ -0,0 +1,76 @@
// 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 escaper
import (
"strings"
)
type (
// RuneFunc 判断指定rune
RuneFunc func(r rune) bool
)
// EscapeByRuneFunc 通过runeFunc转义, runeFunc返回真, 则转义
func EscapeByRuneFunc(s string, runeFunc RuneFunc) string {
if runeFunc == nil {
return s
}
var (
builder = &strings.Builder{}
rs = []rune(s)
)
for k := range rs {
if !runeFunc(rs[k]) {
builder.WriteRune(rs[k])
continue
}
if k >= 1 && rs[k-1] == '\\' {
builder.WriteRune(rs[k])
continue
}
builder.WriteString(`\`)
builder.WriteRune(rs[k])
}
return builder.String()
}
// Escape 转义指定的escapeRunes, 在escapeRunes的前面加上一个反斜杠
func Escape(s string, escapeRunes []rune) string {
return EscapeByRuneFunc(s, func(r rune) bool {
for k := range escapeRunes {
if escapeRunes[k] == r {
return true
}
}
return false
})
}
// EscapeStrings 转义字符串数组
func EscapeStrings(ss []string, escapeRunes []rune) {
for k := range ss {
ss[k] = Escape(ss[k], escapeRunes)
}
}
// EscapeStringsByRuneFunc 转义字符串数组, 通过runeFunc
func EscapeStringsByRuneFunc(ss []string, runeFunc RuneFunc) {
for k := range ss {
ss[k] = EscapeByRuneFunc(ss[k], runeFunc)
}
}
+130
View File
@@ -0,0 +1,130 @@
// 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 cmdutil
import (
"github.com/kardianos/osext"
"github.com/tickstep/library-go/logger"
"os"
"path"
"path/filepath"
"runtime"
"strings"
)
func IsPipeInput() bool {
fileInfo, err := os.Stdin.Stat()
if err != nil {
return false
}
return (fileInfo.Mode() & os.ModeNamedPipe) == os.ModeNamedPipe
}
// IsIPhoneOS 是否为苹果移动设备
func IsIPhoneOS() bool {
if runtime.GOOS == "darwin" && (runtime.GOARCH == "arm" || runtime.GOARCH == "arm64") {
_, err := os.Stat("Info.plist")
return err == nil
}
return false
}
// ChWorkDir 切换回工作目录
func ChWorkDir() {
if !IsIPhoneOS() {
return
}
dir, err := filepath.Abs("")
if err != nil {
return
}
subPath := filepath.Dir(os.Args[0])
os.Chdir(strings.TrimSuffix(dir, subPath))
}
// Executable 获取程序所在的真实目录或真实相对路径
func Executable() string {
executablePath, err := osext.Executable()
if err != nil {
logger.Verbosef("DEBUG: osext.Executable: %s\n", err)
executablePath, err = filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
logger.Verbosef("DEBUG: filepath.Abs: %s\n", err)
executablePath = filepath.Dir(os.Args[0])
}
}
if IsIPhoneOS() {
executablePath = filepath.Join(strings.TrimSuffix(executablePath, os.Args[0]), filepath.Base(os.Args[0]))
}
// 读取链接
linkedExecutablePath, err := filepath.EvalSymlinks(executablePath)
if err != nil {
logger.Verbosef("DEBUG: filepath.EvalSymlinks: %s\n", err)
return executablePath
}
return linkedExecutablePath
}
// ExecutablePath 获取程序所在目录
func ExecutablePath() string {
return filepath.Dir(Executable())
}
// ExecutablePathJoin 返回程序所在目录的子目录
func ExecutablePathJoin(subPath string) string {
return filepath.Join(ExecutablePath(), subPath)
}
// WalkDir 获取指定目录及所有子目录下的所有文件,可以匹配后缀过滤。
// 支持 Linux/macOS 软链接
func WalkDir(dirPth, suffix string) (files []string, err error) {
files = make([]string, 0, 32)
suffix = strings.ToUpper(suffix) //忽略后缀匹配的大小写
var walkFunc filepath.WalkFunc
walkFunc = func(filename string, fi os.FileInfo, err error) error { //遍历目录
if err != nil {
return err
}
if fi.IsDir() { // 忽略目录
return nil
}
if fi.Mode()&os.ModeSymlink != 0 { // 读取 symbol link
err = filepath.Walk(filename+string(os.PathSeparator), walkFunc)
return err
}
if strings.HasSuffix(strings.ToUpper(fi.Name()), suffix) {
files = append(files, path.Clean(filename))
}
return nil
}
err = filepath.Walk(dirPth, walkFunc)
return files, err
}
// ConvertToUnixPathSeparator 将 windows 目录分隔符转换为 Unix 的
func ConvertToUnixPathSeparator(p string) string {
return strings.Replace(p, "\\", "/", -1)
}
// ConvertToWindowsPathSeparator 将路径中的所有分隔符转换成windows格式
func ConvertToWindowsPathSeparator(p string) string {
return strings.Replace(p, "/", "\\", -1)
}
+31
View File
@@ -0,0 +1,31 @@
// 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 jsonhelper
import (
"github.com/json-iterator/go"
"io"
)
// UnmarshalData 将 r 中的 json 格式的数据, 解析到 data
func UnmarshalData(r io.Reader, data interface{}) error {
d := jsoniter.NewDecoder(r)
return d.Decode(data)
}
// MarshalData 将 data, 生成 json 格式的数据, 写入 w 中
func MarshalData(w io.Writer, data interface{}) error {
e := jsoniter.NewEncoder(w)
return e.Encode(data)
}
+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
+16
View File
@@ -0,0 +1,16 @@
# 关于 Windows EXE ICO 和应用信息编译
为了编译出来的windows的exe文件带有ico和应用程序信息,需要使用 github.com/josephspurrier/goversioninfo/cmd/goversioninfo 工具
工具安装,运行下面的命令即可生成工具。也可以直接用 bin/ 文件夹下面的编译好的
```
go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo
go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo
```
versioninfo.json - 里面有exe程序信息以及ico的配置
使用 goversioninfo 工具运行以下命令
```
goversioninfo -o=resource_windows_386.syso
goversioninfo -64 -o=resource_windows_amd64.syso
```
即可编译出.syso资源库,再使用 go build 编译之后,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");
}
```
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>application-identifier</key>
<string>com.tickstep.aliyunpan</string>
<key>get-task-allow</key>
<true/>
<key>platform-application</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>com.tickstep.aliyunpan</string>
</array>
</dict>
</plist>
+42
View File
@@ -0,0 +1,42 @@
module github.com/tickstep/aliyunpan
go 1.22
require (
github.com/GeertJohan/go.incremental v1.0.0
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/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/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.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
+146
View File
@@ -0,0 +1,146 @@
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/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/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/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/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/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/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/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=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
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/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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=
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/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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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=
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()
}
}
+146
View File
@@ -0,0 +1,146 @@
// 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/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
)
func CmdCd() cli.Command {
return cli.Command{
Name: "cd",
Category: "阿里云盘",
Usage: "切换工作目录",
Description: `
aliyunpan cd <目录, 绝对路径或相对路径>
示例:
切换 /我的资源 工作目录:
aliyunpan cd /我的资源
切换 /我的资源 工作目录,使用通配符:
aliyunpan cd /我的*
切换上级目录:
aliyunpan cd ..
切换根目录:
aliyunpan cd /
`,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
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
}
RunChangeDirectory(parseDriveId(c), c.Args().Get(0))
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
},
}
}
func CmdPwd() cli.Command {
return cli.Command{
Name: "pwd",
Usage: "输出工作目录",
UsageText: cmder.App().Name + " pwd",
Category: "阿里云盘",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
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)
}
return nil
},
}
}
func RunChangeDirectory(driveId, targetPath string) {
user := config.Config.ActiveUser()
targetPath = user.PathJoin(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", targetPathInfo.Path)
return
}
if user.IsFileDriveActive() {
user.Workdir = targetPathInfo.Path
user.WorkdirFileEntity = *targetPathInfo
} else if user.IsResourceDriveActive() {
user.ResourceWorkdir = targetPathInfo.Path
user.ResourceWorkdirFileEntity = *targetPathInfo
} else if user.IsAlbumDriveActive() {
user.AlbumWorkdir = targetPathInfo.Path
user.AlbumWorkdirFileEntity = *targetPathInfo
}
fmt.Printf("改变工作目录: %s\n", targetPathInfo.Path)
}
+316
View File
@@ -0,0 +1,316 @@
// 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 (
"errors"
"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"
)
type ()
const (
cryptoDescription = `
可用的方法 <method>:
aes-128-ctr, aes-192-ctr, aes-256-ctr,
aes-128-cfb, aes-192-cfb, aes-256-cfb,
aes-128-ofb, aes-192-ofb, aes-256-ofb.
密钥 <key>:
aes-128 对应key长度为16, aes-192 对应key长度为24, aes-256 对应key长度为32,
如果key长度不符合, 则自动修剪key, 舍弃超出长度的部分, 长度不足的部分用'\0'填充.
GZIP <disable-gzip>:
在文件加密之前, 启用GZIP压缩文件; 文件解密之后启用GZIP解压缩文件, 默认启用,
如果不启用, 则无法检测文件是否解密成功, 解密文件时会保留源文件, 避免解密失败造成文件数据丢失.`
)
var ErrBadArgs = errors.New("参数错误")
var ErrNotLogined = errors.New("未登录账号")
func GetActivePanClient() *config.PanClient {
return config.Config.ActiveUser().PanClient()
}
func GetActiveUser() *config.PanUser {
return config.Config.ActiveUser()
}
func parseDriveId(c *cli.Context) string {
driveId := config.Config.ActiveUser().ActiveDriveId
if c.IsSet("driveId") {
driveId = c.String("driveId")
}
return driveId
}
func CmdConfig() cli.Command {
return cli.Command{
Name: "config",
Usage: "显示和修改程序配置项",
Description: "显示和修改程序配置项",
Category: "配置",
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
fmt.Printf("----\n当前配置目录: %s\n运行 %s config set 可进行设置配置\n\n当前配置:\n", config.GetConfigDir(), cmder.App().Name)
config.Config.PrintTable()
return nil
},
Subcommands: []cli.Command{
{
Name: "set",
Usage: "修改程序配置项",
UsageText: cmder.App().Name + " config set [arguments...]",
Description: `
注意:
可通过设置环境变量 ALIYUNPAN_CONFIG_DIR, 指定配置文件存放的目录.
cache_size 的值支持可选设置单位, 单位不区分大小写, b 和 B 均表示字节的意思, 如 64KB, 1MB, 32kb, 65536b, 65536
max_download_rate, max_upload_rate 的值支持可选设置单位, 单位为每秒的传输速率, 后缀'/s' 可省略, 如 2MB/s, 2MB, 2m, 2mb 均为一个意思
例子:
aliyunpan config set -cache_size 64KB
aliyunpan config set -cache_size 16384 -max_download_parallel 200 -savedir D:/download`,
Action: func(c *cli.Context) error {
if c.NumFlags() <= 0 || c.NArg() > 0 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
if c.IsSet("cache_size") {
err := config.Config.SetCacheSizeByStr(c.String("cache_size"))
if err != nil {
fmt.Printf("设置 cache_size 错误: %s\n", err)
return nil
}
}
if c.IsSet("max_download_parallel") {
config.Config.MaxDownloadParallel = c.Int("max_download_parallel")
}
if c.IsSet("max_upload_parallel") {
config.Config.MaxUploadParallel = c.Int("max_upload_parallel")
}
if c.IsSet("max_download_rate") {
err := config.Config.SetMaxDownloadRateByStr(c.String("max_download_rate"))
if err != nil {
fmt.Printf("设置 max_download_rate 错误: %s\n", err)
return nil
}
}
if c.IsSet("max_upload_rate") {
err := config.Config.SetMaxUploadRateByStr(c.String("max_upload_rate"))
if err != nil {
fmt.Printf("设置 max_upload_rate 错误: %s\n", err)
return nil
}
}
if c.IsSet("savedir") {
config.Config.SaveDir = c.String("savedir")
}
if c.IsSet("proxy") {
config.Config.SetProxy(c.String("proxy"))
}
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 {
fmt.Println(err)
return err
}
config.Config.PrintTable()
fmt.Printf("\n保存配置成功!\n\n")
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "cache_size",
Usage: "下载缓存,单位:KB",
},
cli.IntFlag{
Name: "max_download_parallel",
Usage: "下载文件最大并发量",
},
cli.IntFlag{
Name: "max_upload_parallel",
Usage: "上传文件最大并发量",
},
cli.StringFlag{
Name: "max_download_rate",
Usage: "限制最大下载速度, 0代表不限制",
},
cli.StringFlag{
Name: "max_upload_rate",
Usage: "限制最大上传速度, 0代表不限制",
},
cli.StringFlag{
Name: "savedir",
Usage: "下载文件的储存目录",
},
cli.StringFlag{
Name: "proxy",
Usage: "设置代理, 支持 http/socks5 代理",
},
cli.StringFlag{
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",
Usage: "工具箱",
Action: func(c *cli.Context) error {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
},
Subcommands: []cli.Command{
{
Name: "getip",
Usage: "获取IP地址",
Action: func(c *cli.Context) error {
fmt.Printf("内网IP地址: \n")
for _, address := range cmdutil.ListAddresses() {
fmt.Printf("%s\n", address)
}
fmt.Printf("\n")
ipAddr, err := getip.IPInfoFromTechainBaiduByClient(config.Config.HTTPClient(""))
if err != nil {
fmt.Printf("获取公网IP错误: %s\n", err)
return nil
}
fmt.Printf("公网IP地址: %s\n", ipAddr)
return nil
},
},
{
Name: "enc",
Usage: "加密文件",
UsageText: cmder.App().Name + " enc -method=<method> -key=<key> [files...]",
Description: cryptoDescription,
Action: func(c *cli.Context) error {
if c.NArg() <= 0 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
for _, filePath := range c.Args() {
encryptedFilePath, err := crypto.EncryptFile(c.String("method"), []byte(c.String("key")), filePath, !c.Bool("disable-gzip"))
if err != nil {
fmt.Printf("%s\n", err)
continue
}
fmt.Printf("加密成功, %s -> %s\n", filePath, encryptedFilePath)
}
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "method",
Usage: "加密方法",
Value: "aes-128-ctr",
},
cli.StringFlag{
Name: "key",
Usage: "加密密钥",
Value: cmder.App().Name,
},
cli.BoolFlag{
Name: "disable-gzip",
Usage: "不启用GZIP",
},
},
},
{
Name: "dec",
Usage: "解密文件",
UsageText: cmder.App().Name + " dec -method=<method> -key=<key> [files...]",
Description: cryptoDescription,
Action: func(c *cli.Context) error {
if c.NArg() <= 0 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
for _, filePath := range c.Args() {
decryptedFilePath, err := crypto.DecryptFile(c.String("method"), []byte(c.String("key")), filePath, !c.Bool("disable-gzip"))
if err != nil {
fmt.Printf("%s\n", err)
continue
}
fmt.Printf("解密成功, %s -> %s\n", filePath, decryptedFilePath)
}
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "method",
Usage: "加密方法",
Value: "aes-128-ctr",
},
cli.StringFlag{
Name: "key",
Usage: "加密密钥",
Value: cmder.App().Name,
},
cli.BoolFlag{
Name: "disable-gzip",
Usage: "不启用GZIP",
},
},
},
},
}
}
+13
View File
@@ -0,0 +1,13 @@
package command
import (
"testing"
)
func TestRapidUploadItem_createRapidUploadLink(t *testing.T) {
}
func TestRapidUploadItem_newRapidUploadItem(t *testing.T) {
}
+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("无法复制文件,请稍后重试")
}
}
+501
View File
@@ -0,0 +1,501 @@
// 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"
"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"
)
type (
//DownloadOptions 下载可选参数
DownloadOptions struct {
// DownloadActionId 下载动作ID,唯一标识一次下载动作的ID。这个ID每次下载任务启动的时候会自动生成,同一次下载任务下载的文件,这个ID都是同一个值。
DownloadActionId string
IsPrintStatus bool
IsExecutedPermission bool
IsOverwrite bool
SaveTo string
Parallel int // 文件下载最大线程数,即同时进行下载文件的数量,阿里云盘限制最大为3
SliceParallel int // 单个文件分片下载最大线程数,阿里云盘限制最大为3
Load int
MaxRetry int
NoCheck bool
ShowProgress bool
DriveId string
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
IsMultiUserDownload bool // 是否启用多用户联合下载
IsUseUIDashboard bool // 是否使用UI下载面板显示下载进度
}
// LocateDownloadOption 获取下载链接可选参数
LocateDownloadOption struct {
FromPan bool
}
)
var (
// MaxDownloadRangeSize 文件片段最大值
MaxDownloadRangeSize = 55 * converter.MB
// DownloadCacheSize 默认每个线程下载缓存大小
DownloadCacheSize = 64 * converter.KB
)
func CmdDownload() cli.Command {
return cli.Command{
Name: "download",
Aliases: []string{"d"},
Usage: "下载文件/目录",
UsageText: cmder.App().Name + " download <文件/目录路径1> <文件/目录2> <文件/目录3> ...",
Description: `
下载的文件默认保存到, 程序所在目录的 download/ 目录。支持软链接文件,包括Linux/macOS(ln命令)和Windows(mklink命令)创建的符号链接文件。
通过 aliyunpan config set -savedir <savedir>, 自定义保存的目录。支持多个文件或目录下载,支持自动跳过下载重名的文件!
示例:
设置保存目录, 保存到 D:\Downloads
注意区别反斜杠 "\" 和 斜杠 "/" !!!
aliyunpan config set -savedir D:\\Downloads
或者
aliyunpan config set -savedir D:/Downloads
下载 /我的资源/1.mp4
aliyunpan download /我的资源/1.mp4
下载 /我的资源 目录下面所有的mp4文件,使用通配符
aliyunpan download /我的资源/*.mp4
下载 /我的资源 整个目录!!
aliyunpan download /我的资源
下载 /我的资源 整个目录,但是排除所有的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: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
// 处理saveTo
var (
saveTo string
)
if c.Bool("save") {
// 使用当前工作目录
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"),
SliceParallel: c.Int("sp"),
Load: 0,
MaxRetry: c.Int("retry"),
NoCheck: c.Bool("nocheck"),
ShowProgress: !c.Bool("np"),
DriveId: parseDriveId(c),
ExcludeNames: c.StringSlice("exn"),
IsMultiUserDownload: c.Bool("md"),
IsUseUIDashboard: c.Bool("ui"),
}
RunDownload(c.Args(), do)
return nil
},
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ow",
Usage: "overwrite, 覆盖已存在的文件",
},
cli.BoolFlag{
Name: "status",
Usage: "输出所有线程的工作状态",
},
cli.BoolFlag{
Name: "save",
Usage: "将下载的文件直接保存到当前工作目录",
},
cli.StringFlag{
Name: "saveto",
Usage: "将下载的文件直接保存到指定的目录",
},
cli.BoolFlag{
Name: "x",
Usage: "为文件加上执行权限, (windows系统无效)",
},
cli.IntFlag{
Name: "p",
Usage: "parallel,指定同时进行下载文件的数量(取值范围:1 ~ 3)",
Value: 1,
},
cli.IntFlag{
Name: "sp",
Usage: "slice parallel,指定单个文件下载的最大线程(分片)数(取值范围:1 ~ 3)",
Value: 0,
},
cli.IntFlag{
Name: "retry",
Usage: "下载失败最大重试次数",
Value: pandownload.DefaultDownloadMaxRetry,
},
cli.BoolFlag{
Name: "nocheck",
Usage: "下载文件完成后不校验文件",
},
cli.BoolFlag{
Name: "np",
Usage: "no progress 不展示下载进度条",
},
cli.StringFlag{
Name: "driveId",
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面板显示下载详情和进度,更加直观和友好",
},
},
}
}
// 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.MaxRetry < 0 {
options.MaxRetry = pandownload.DefaultDownloadMaxRetry
}
if runtime.GOOS == "windows" {
// windows下不加执行权限
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
}
}
paths, err := makePathAbsolute(options.DriveId, paths...)
if err != nil {
fmt.Println(err)
return
}
// 多用户下载的辅助账号列表
var subPanClientList []*config.PanClient
if options.IsMultiUserDownload { // 多用户下载
c := config.Config
for _, u := range config.Config.UserList {
if u.UserId == activeUser.UserId {
// 当前登录用户,作为主用户,跳过
continue
}
// 初始化客户端
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
}
if subPanClientList == nil {
subPanClientList = []*config.PanClient{}
}
subPanClientList = append(subPanClientList, user.PanClient())
}
if subPanClientList == nil || len(subPanClientList) == 0 {
fmt.Printf("\n当前登录用户只有一个,无法启用多用户联合下载\n")
subPanClientList = nil
}
}
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 {
// 使用通配符匹配
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 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)
}
}
// 开始计时
statistic.StartTimer()
// 启动UI面板显示
if dashboard != nil {
dashboard.Start()
}
// 开始执行
executor.Execute()
// 关闭UI面板
if dashboard != nil {
dashboard.Close()
}
// 完成下载,输出统计结果
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()
}
}
+135
View File
@@ -0,0 +1,135 @@
// 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/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
"strconv"
"strings"
)
func CmdDrive() cli.Command {
return cli.Command{
Name: "drive",
Usage: "切换网盘(备份盘/资源库)",
Description: `
切换已登录账号的阿里云盘的工作网盘(备份盘/资源库)
如果运行该条命令没有提供参数, 程序将会列出所有的网盘列表, 供选择切换.
示例:
aliyunpan drive
aliyunpan drive <driveId>
`,
Category: "阿里云盘账号",
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
inputData := c.Args().Get(0)
targetDriveId := strings.TrimSpace(inputData)
RunSwitchDriveList(targetDriveId)
return nil
},
}
}
func RunSwitchDriveList(targetDriveId string) {
currentDriveId := config.Config.ActiveUser().ActiveDriveId
var activeDriveInfo *config.DriveInfo = nil
driveList, renderStr := getDriveOptionList()
if driveList == nil || len(driveList) == 0 {
fmt.Println("切换网盘失败")
return
}
if targetDriveId == "" {
// show option list
fmt.Println(renderStr)
// 提示输入 index
var index string
fmt.Printf("输入要切换的网盘 # 值 > ")
_, err := fmt.Scanln(&index)
if err != nil {
return
}
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 {
if driveInfo.DriveId == targetDriveId {
activeDriveInfo = driveInfo
break
}
}
}
if activeDriveInfo == nil {
fmt.Printf("切换网盘失败\n")
return
}
config.Config.ActiveUser().ActiveDriveId = activeDriveInfo.DriveId
activeUser := config.Config.ActiveUser()
if currentDriveId != config.Config.ActiveUser().ActiveDriveId {
// clear the drive work path
if activeUser.IsFileDriveActive() {
if activeUser.Workdir == "" {
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 = "/"
config.Config.ActiveUser().AlbumWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
}
}
}
fmt.Printf("切换到网盘:%s\n", activeDriveInfo.DriveName)
}
func getDriveOptionList() (config.DriveInfoList, string) {
activeUser := config.Config.ActiveUser()
driveList := activeUser.DriveList
builder := &strings.Builder{}
tb := cmdtable.NewTable(builder)
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_CENTER})
tb.SetHeader([]string{"#", "drive_id", "网盘名称"})
for k, info := range driveList {
if info.IsAlbumDrive() {
continue
}
tb.Append([]string{strconv.Itoa(k + 1), info.DriveId, info.DriveName})
}
tb.Render()
return driveList, builder.String()
}
+164
View File
@@ -0,0 +1,164 @@
// 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/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
`,
Category: "阿里云盘账号",
Before: ReloadConfigFunc, // 每次进行登录动作的时候需要调用刷新配置
After: SaveConfigFunc, // 登录完成需要调用保存配置
Action: func(c *cli.Context) error {
ticketId := ""
openToken := &config.PanClientToken{}
webToken := &config.PanClientToken{}
var err error
ticketId, openToken, webToken, err = RunLogin()
if err != nil {
fmt.Println(err)
return err
}
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.TicketId = ticketId
config.Config.SetActiveUser(cloudUser)
fmt.Println("阿里云盘登录成功: ", cloudUser.Nickname)
return nil
},
// 命令的附加options参数说明,使用 help panlogin 命令即可查看
Flags: []cli.Flag{},
}
}
func CmdLogout() cli.Command {
return cli.Command{
Name: "logout",
Usage: "退出阿里帐号",
Description: "退出当前登录的帐号",
Category: "阿里云盘账号",
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.NumLogins() == 0 {
fmt.Println("未设置任何帐号, 不能退出")
return nil
}
var (
confirm string
activeUser = config.Config.ActiveUser()
)
if activeUser == nil {
return nil
}
if !c.Bool("y") {
fmt.Printf("确认退出当前帐号: %s ? (y/n) > ", activeUser.Nickname)
_, err := fmt.Scanln(&confirm)
if err != nil || (confirm != "y" && confirm != "Y") {
return err
}
}
// 云端注销登录
//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)
}
fmt.Printf("退出用户成功: %s\n", deletedUser.Nickname)
return nil
},
Flags: []cli.Flag{
cli.BoolFlag{
Name: "y",
Usage: "确认退出帐号",
},
},
}
}
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
}
+254
View File
@@ -0,0 +1,254 @@
// 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/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/converter"
"github.com/urfave/cli"
"os"
"strconv"
)
type (
// LsOptions 列目录可选项
LsOptions struct {
Total bool
}
// SearchOptions 搜索可选项
SearchOptions struct {
Total bool
Recurse bool
}
)
const (
opLs int = iota
opSearch
)
func CmdLs() cli.Command {
return cli.Command{
Name: "ls",
Aliases: []string{"l", "ll"},
Usage: "列出目录",
UsageText: cmder.App().Name + " ls <目录>",
Description: `
列出当前工作目录内的文件和目录, 或指定目录内的文件和目录
示例:
列出 我的资源 内的文件和目录
aliyunpan ls 我的资源
绝对路径
aliyunpan ls /我的资源
列出 我的资源 内的文件和目录,使用通配符
aliyunpan ls /我的*
详细列出 我的资源 内的文件和目录
aliyunpan ll /我的资源
`,
Category: "阿里云盘",
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
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "driveId",
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,
orderBy aliyunpan.FileOrderBy, orderDirection aliyunpan.FileOrderDirection) {
activeUser := config.Config.ActiveUser()
targetPath = activeUser.PathJoin(driveId, targetPath)
// 获取目标路径文件信息
targetPathInfo, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, targetPath)
if err != nil {
if err.Code == apierror.ApiCodeFileNotFoundCode {
fmt.Println("指定目录不存在: " + targetPath)
} else {
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
}
fileList := aliyunpan.FileList{}
fileListParam := &aliyunpan.FileListParam{}
fileListParam.ParentFileId = targetPathInfo.FileId
fileListParam.DriveId = driveId
fileListParam.OrderBy = orderBy
fileListParam.OrderDirection = orderDirection
if targetPathInfo.IsFolder() {
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, targetPathInfo.Path, fileList)
}
func renderTable(op int, isTotal bool, path string, files aliyunpan.FileList) {
tb := cmdtable.NewTable(os.Stdout)
var (
fN, dN int64
showPath string
)
switch op {
case opLs:
showPath = "文件(目录)"
case opSearch:
showPath = "路径"
}
if isTotal {
tb.SetHeader([]string{"#", "file_id", "文件大小", "文件SHA1", "文件大小(原始)", "创建日期", "修改日期", showPath})
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 + 1), file.FileId, "-", "-", "-", file.CreatedAt, file.UpdatedAt, file.FileName + aliyunpan.PathSeparator})
continue
}
switch op {
case opLs:
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 + 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()
tb.Append([]string{"", "", "总: " + converter.ConvertFileSize(files.TotalSize(), 2), "", "", "", fmt.Sprintf("文件总数: %d, 目录总数: %d", fN, dN)})
} else {
tb.SetHeader([]string{"#", "文件大小", "修改日期", showPath})
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 + 1), "-", file.UpdatedAt, file.FileName + aliyunpan.PathSeparator})
continue
}
switch op {
case opLs:
tb.Append([]string{strconv.Itoa(k + 1), converter.ConvertFileSize(file.FileSize, 2), file.UpdatedAt, file.FileName})
case opSearch:
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()
fmt.Printf("----\n")
}
+74
View File
@@ -0,0 +1,74 @@
// 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/urfave/cli"
)
func CmdMkdir() cli.Command {
return cli.Command{
Name: "mkdir",
Usage: "创建目录",
UsageText: cmder.App().Name + " mkdir <目录>",
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
}
RunMkdir(parseDriveId(c), c.Args().Get(0))
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
},
}
}
func RunMkdir(driveId, name string) {
activeUser := GetActiveUser()
fullpath := activeUser.PathJoin(driveId, name)
rs := &aliyunpan.MkdirResult{}
err := apierror.NewFailedApiError("")
rs, err = activeUser.PanClient().OpenapiPanClient().MkdirByFullPath(driveId, fullpath)
if err != nil {
fmt.Println("创建文件夹失败:" + err.Error())
return
}
if rs.FileId != "" {
fmt.Println("创建文件夹成功: ", fullpath)
// cache
activeUser.DeleteCache(GetAllPathFolderByPath(fullpath))
} else {
fmt.Println("创建文件夹失败: ", fullpath)
}
}
+159
View File
@@ -0,0 +1,159 @@
// 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"
"path"
"strconv"
)
func CmdMv() cli.Command {
return cli.Command{
Name: "mv",
Usage: "移动文件/目录",
UsageText: `
aliyunpan mv <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>`,
Description: `
注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败。支持通配符匹配移动文件,通配符当前只能匹配文件名,不能匹配文件路径。
示例:
将 /我的资源/1.mp4 移动到 根目录 /
aliyunpan mv /我的资源/1.mp4 /
将 /我的资源 目录下所有的.png文件 移动到 /我的图片 目录下面,使用通配符匹配
aliyunpan mv /我的资源/*.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
}
RunMove(parseDriveId(c), c.Args()...)
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
},
}
}
// RunMove 执行移动文件/目录
func RunMove(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)
failedMoveFiles := []*aliyunpan.FileEntity{}
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.Path)
}
fmt.Println("")
}
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) {
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(driveId, targetFilePath)
targetFile, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, absolutePath)
if err != nil || !targetFile.IsFolder() {
return nil, nil, nil, fmt.Errorf("指定目标文件夹不存在")
}
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
}
+63
View File
@@ -0,0 +1,63 @@
// 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/internal/config"
"github.com/tickstep/library-go/converter"
"github.com/urfave/cli"
)
type QuotaInfo struct {
// 已使用个人空间大小
UsedSize int64
// 个人空间总大小
Quota int64
}
func CmdQuota() cli.Command {
return cli.Command{
Name: "quota",
Usage: "获取当前帐号空间配额",
Description: "获取网盘的总储存空间, 和已使用的储存空间",
Category: "阿里云盘账号",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
q, err := RunGetQuotaInfo()
if err == nil {
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))
}
return nil
},
}
}
func RunGetQuotaInfo() (quotaInfo *QuotaInfo, error error) {
user, err := GetActivePanClient().OpenapiPanClient().GetUserInfo()
if err != nil {
return nil, err
}
return &QuotaInfo{
UsedSize: int64(user.UsedSize),
Quota: int64(user.TotalSize),
}, nil
}
+274
View File
@@ -0,0 +1,274 @@
// 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_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 {
return cli.Command{
Name: "recycle",
Usage: "回收站",
Description: `
回收站操作.
示例:
1. 从回收站还原两个文件, 其中的两个文件的 file_id 分别为 1013792297798440 和 643596340463870
aliyunpan recycle restore 1013792297798440 643596340463870
2. 从回收站删除两个文件, 其中的两个文件的 file_id 分别为 1013792297798440 和 643596340463870
aliyunpan recycle delete 1013792297798440 643596340463870
3. 清空回收站, 程序不会进行二次确认, 谨慎操作!!!
aliyunpan recycle delete -all
`,
Category: "阿里云盘",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NumFlags() <= 0 || c.NArg() <= 0 {
cli.ShowCommandHelp(c, c.Command.Name)
}
return nil
},
Subcommands: []cli.Command{
{
Name: "list",
Aliases: []string{"ls", "l"},
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
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
},
},
{
Name: "restore",
Aliases: []string{"r"},
Usage: "还原回收站文件或目录",
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
}
RunRecycleRestore(parseDriveId(c), c.Args()...)
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
},
},
{
Name: "delete",
Aliases: []string{"d"},
Usage: "删除回收站文件或目录 / 清空回收站",
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))
return nil
}
if c.NArg() <= 0 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
RunRecycleDelete(parseDriveId(c), c.Args()...)
return nil
},
Flags: []cli.Flag{
cli.BoolFlag{
Name: "all",
Usage: "清空回收站, 程序不会进行二次确认, 谨慎操作!!!",
},
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
},
},
},
}
}
// RunRecycleList 执行列出回收站文件列表
func RunRecycleList(driveId string) {
panClient := GetActivePanClient()
fdl, err := panClient.WebapiPanClient().RecycleBinFileListGetAll(&aliyunpan_web.RecycleBinFileListParam{
DriveId: driveId,
Limit: 100,
})
if err != nil {
fmt.Println(err)
return
}
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "file_id", "文件/目录名", "文件大小", "创建日期", "修改日期"})
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
for k, file := range fdl {
fn := file.FileName
fs := converter.ConvertFileSize(file.FileSize, 2)
if file.IsFolder() {
fn = fn + "/"
fs = "-"
}
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, fn, fs, file.CreatedAt, file.UpdatedAt})
}
tb.Render()
}
// RunRecycleRestore 执行还原回收站文件或目录
func RunRecycleRestore(driveId string, fidStrList ...string) {
panClient := GetActivePanClient()
restoreFileList := []*aliyunpan.FileBatchActionParam{}
for _, fid := range fidStrList {
restoreFileList = append(restoreFileList, &aliyunpan.FileBatchActionParam{
DriveId: driveId,
FileId: fid,
})
}
if len(restoreFileList) == 0 {
fmt.Printf("没有需要还原的文件")
return
}
rbfr, err := panClient.WebapiPanClient().RecycleBinFileRestore(restoreFileList)
if rbfr != nil && len(rbfr) > 0 {
fmt.Printf("还原文件成功\n")
return
}
if len(rbfr) == 0 && err != nil {
fmt.Printf("还原文件失败:%s\n", err)
return
}
}
// RunRecycleDelete 执行删除回收站文件或目录
func RunRecycleDelete(driveId string, fidStrList ...string) {
panClient := GetActivePanClient()
deleteFileList := []*aliyunpan.FileBatchActionParam{}
for _, fid := range fidStrList {
deleteFileList = append(deleteFileList, &aliyunpan.FileBatchActionParam{
DriveId: driveId,
FileId: fid,
})
}
if len(deleteFileList) == 0 {
fmt.Printf("没有需要删除的文件")
return
}
rbfr, err := panClient.WebapiPanClient().RecycleBinFileDelete(deleteFileList)
if rbfr != nil && len(rbfr) > 0 {
fmt.Printf("彻底删除文件成功\n")
return
}
if len(rbfr) == 0 && err != nil {
fmt.Printf("彻底删除文件失败:%s\n", err)
return
}
}
// RunRecycleClear 清空回收站
func RunRecycleClear(driveId string) {
panClient := GetActivePanClient()
// 提交清空回收站异步任务
r, err := panClient.WebapiPanClient().RecycleBinFileClear(&aliyunpan_web.RecycleBinFileClearParam{
DriveId: driveId,
})
if err != nil {
logger.Verboseln(err)
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")
}
+275
View File
@@ -0,0 +1,275 @@
// 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/apiutil"
"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",
Usage: "重命名文件",
UsageText: `重命名文件:
aliyunpan rename <旧文件/目录名> <新文件/目录名>`,
Description: `
示例:
1. 将文件 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
批量重命名,规则: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: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() != 2 && c.NArg() != 3 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
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{
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
cli.BoolFlag{
Name: "y",
Usage: "跳过人工确认,对批量操作有效",
},
},
}
}
func RunRename(driveId string, oldName string, newName string) {
if oldName == "" {
fmt.Println("请指定命名文件")
return
}
if newName == "" {
fmt.Println("请指定文件新名称")
return
}
activeUser := GetActiveUser()
oldName = activeUser.PathJoin(driveId, strings.TrimSpace(oldName))
newName = activeUser.PathJoin(driveId, strings.TrimSpace(newName))
if path.Dir(oldName) != path.Dir(newName) {
fmt.Println("只能命名同一个目录的文件")
return
}
if !apiutil.CheckFileNameValid(path.Base(newName)) {
fmt.Println("文件名不能包含特殊字符:" + apiutil.FileNameSpecialChars)
return
}
fileId := ""
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().OpenapiPanClient().FileRename(driveId, fileId, path.Base(newName))
if e != nil {
fmt.Println(e.Err)
return
}
if !b {
fmt.Println("重命名文件失败")
return
}
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))
}
+197
View File
@@ -0,0 +1,197 @@
// 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/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 {
return cli.Command{
Name: "rm",
Usage: "删除文件/目录",
UsageText: cmder.App().Name + " rm <文件/目录的路径1> <文件/目录2> <文件/目录3> ...",
Description: `
注意: 删除多个文件和目录时, 请确保每一个文件和目录都存在, 否则删除操作会失败.
被删除的文件或目录可在网盘文件回收站找回。支持通配符匹配删除文件,通配符当前只能匹配文件名,不能匹配文件路径。
示例:
删除 /我的资源/1.mp4
aliyunpan rm /我的资源/1.mp4
删除 /我的资源/1.mp4 和 /我的资源/2.mp4
aliyunpan rm /我的资源/1.mp4 /我的资源/2.mp4
删除 /我的资源 整个目录 !!
aliyunpan rm /我的资源
删除 /我的资源 目录下面的所有.zip文件,使用通配符匹配
aliyunpan rm /我的资源/*.zip
`,
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
}
RunRemove(parseDriveId(c), c.Args()...)
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
},
}
}
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))
successDelFileEntity := []*aliyunpan.FileEntity{}
for _, p := range paths {
absolutePath := path.Clean(activeUser.PathJoin(driveId, p))
// 查询路径对应的文件信息
fileList, err1 := queryFileListByPath(activeUser, driveId, absolutePath)
if err1 != nil {
failedRmPaths = append(failedRmPaths, absolutePath)
continue
}
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 + 1), successDelFileEntity[k].Path})
}
tb.Render()
}
if len(successDelFileEntity) > 0 {
fmt.Println("操作成功, 以下文件/目录已删除, 可在云盘文件回收站找回: ")
pnt()
activeUser.DeleteCache(cacheCleanDirs)
} 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)
}
+230
View File
@@ -0,0 +1,230 @@
// 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/urfave/cli"
"path"
"strings"
"time"
)
func CmdShare() cli.Command {
return cli.Command{
Name: "share",
Usage: "分享文件/目录",
UsageText: cmder.App().Name + " share",
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
}
// 有效期
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")
}
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" {
if sharePwd == "" {
sharePwd = RandomStr(4)
}
} else {
sharePwd = ""
}
RunOpenShareSet(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: "",
},
},
},
},
}
}
// 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 allFileList {
fidList = append(fidList, f.FileId)
}
if len(fidList) == 0 {
fmt.Printf("没有指定有效的文件\n")
return
}
// 创建分享类型
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)
}
}
}
+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()
}
+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))
}
+483
View File
@@ -0,0 +1,483 @@
// 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"
"os"
"path"
"path/filepath"
"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"
"github.com/tickstep/aliyunpan/cmder/cmdutil"
"github.com/oleiade/lane"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/functions/panupload"
"github.com/tickstep/aliyunpan/internal/localfile"
"github.com/tickstep/aliyunpan/internal/taskframework"
"github.com/tickstep/library-go/converter"
)
const (
// DefaultUploadMaxAllParallel 默认所有文件并发上传数量,即可以同时并发上传多少个文件
DefaultUploadMaxAllParallel = 1
// DefaultUploadMaxRetry 默认上传失败最大重试次数
DefaultUploadMaxRetry = 3
)
type (
// UploadOptions 上传可选项
UploadOptions struct {
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代表跟从配置文件设置(取值范围:1 ~ 20)",
Value: 0,
},
cli.IntFlag{
Name: "retry",
Usage: "上传失败最大重试次数",
Value: DefaultUploadMaxRetry,
},
cli.IntFlag{
Name: "timeout",
Usage: "上传请求超时时间,单位为秒。当遇到网络不好导致上传超时可以尝试调大该值,建议设置30秒以上",
},
cli.BoolFlag{
Name: "np",
Usage: "no progress 不展示上传进度条",
},
cli.BoolFlag{
Name: "ow",
Usage: "overwrite, 覆盖已存在的同名文件,注意已存在的文件会被移到回收站",
},
cli.BoolFlag{
Name: "skip",
Usage: "skip same name, 跳过已存在的同名文件,即使文件内容不一致(不检查SHA1)",
},
cli.BoolFlag{
Name: "norapid",
Usage: "不检测秒传。跳过费时的SHA1计算直接上传",
},
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
cli.StringSliceFlag{
Name: "exn",
Usage: "exclude name,指定排除的文件夹或者文件的名称,只支持正则表达式。支持同时排除多个名称,每一个名称就是一个exn参数",
Value: nil,
},
cli.IntFlag{
Name: "bs",
Usage: "block size,上传分片大小,单位KB。推荐值:1024 ~ 10240。当上传极大单文件时候请适当调高该值",
Value: 10240,
},
cli.BoolFlag{
Name: "ui",
Usage: "(BETA) 使用UI面板显示上传详情和进度,更加直观和友好",
},
}
func CmdUpload() cli.Command {
return cli.Command{
Name: "upload",
Aliases: []string{"u"},
Usage: "上传文件/目录",
UsageText: cmder.App().Name + " upload <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>",
Description: `
上传指定的文件夹或者文件,上传的文件将会保存到 <目标目录>。支持软链接文件,包括Linux/macOS(ln命令)和Windows(mklink命令)创建的符号链接文件。
示例:
1. 将本地的 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录
注意区别反斜杠 "\" 和 斜杠 "/" !!!
aliyunpan upload C:/Users/Administrator/Desktop/1.mp4 /视频
2. 将本地的 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 /视频
3. 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录
aliyunpan upload C:/Users/Administrator/Desktop /视频
4. 将本地 200GB 极大文件 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录,需要调高上传分片大小
aliyunpan upload -bs 30720 C:/Users/Administrator/Desktop/1.mp4 /视频
5. 使用相对路径
aliyunpan upload 1.mp4 /视频
6. 覆盖上传,已存在的同名文件会被移到回收站
aliyunpan upload -ow 1.mp4 /视频
7. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
aliyunpan upload -exn "\.jpg$" C:/Users/Administrator/Video /视频
8. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
aliyunpan upload -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视频
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$"
2)排除.jpg文件:-exn "\.jpg$"
3)排除.号开头的文件:-exn "^\."
4)排除~号开头的文件:-exn "^~"
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
`,
Category: "阿里云盘",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() < 2 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
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"),
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,
}
}
// RunUpload 执行文件上传
func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
activeUser := GetActiveUser()
activeUser.PanClient().OpenapiPanClient().EnableCache()
activeUser.PanClient().OpenapiPanClient().ClearCache()
defer activeUser.PanClient().OpenapiPanClient().DisableCache()
if opt == nil {
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
}
// 超时时间
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().OpenapiPanClient().FileInfoByPath(opt.DriveId, savePath)
if err1 != nil {
fmt.Printf("警告: 上传文件, 获取云盘路径 %s 错误, %s\n", savePath, err1)
}
switch len(localPaths) {
case 0:
fmt.Printf("本地路径为空\n")
return
}
// 打开上传状态数据库
uploadDatabase, err := panupload.NewUploadingDatabase()
if err != nil {
fmt.Printf("打开上传未完成数据库错误: %s\n", err)
return
}
defer uploadDatabase.Close()
var (
// 使用 task framework
executor = &taskframework.TaskExecutor{
IsFailedDeque: true, // 失败统计
}
// 统计
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 localfile.MyWalkFunc
curPath = filepath.Clean(curPath)
localPathDir := filepath.Dir(curPath)
// 是否排除上传
if utils.IsExcludeFile(curPath, &opt.ExcludeNames) {
logf("排除文件: %s\n", curPath)
continue
}
// 避免去除文件名开头的"."
if localPathDir == "." {
localPathDir = ""
}
walkFunc = func(file localfile.SymlinkFile, fi os.FileInfo, err error) error {
if err != nil {
// skip this error file and continue recurse
logger.Verboseln("upload process file: ", file, " error: ", err)
return nil
}
if os.PathSeparator == '\\' {
file.LogicPath = cmdutil.ConvertToWindowsPathSeparator(file.LogicPath)
file.RealPath = cmdutil.ConvertToWindowsPathSeparator(file.RealPath)
}
// 是否排除上传
if utils.IsExcludeFile(file.LogicPath, &opt.ExcludeNames) {
logf("排除文件: %s\n", file.LogicPath)
return filepath.SkipDir
}
subSavePath := strings.TrimPrefix(file.LogicPath, localPathDir)
// 针对 windows 的目录处理
if os.PathSeparator == '\\' {
subSavePath = cmdutil.ConvertToUnixPathSeparator(subSavePath)
}
subSavePath = path.Clean(savePath + aliyunpan.PathSeparator + subSavePath)
// 插件回调
ft := "file"
if fi.IsDir() {
ft = "folder"
}
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 uploadFilePrepareResult.DriveFilePath != "" {
targetSavePanRelativePath := strings.TrimPrefix(uploadFilePrepareResult.DriveFilePath, "/")
subSavePath = path.Clean(savePath + aliyunpan.PathSeparator + targetSavePanRelativePath)
logf("插件修改文件网盘保存路径为: %s\n", subSavePath)
}
}
// 创建对应的文件上传任务
// 上传里面的文件会创建对应的缺失文件夹
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
}
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(), 2))
// 输出上传失败的文件列表
for _, failed := range failedList {
if failed.Size() != 0 {
fmt.Printf("以下文件上传失败: \n")
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.LogicPath})
}
tb.Render()
}
}
activeUser.DeleteCache(GetAllPathFolderByPath(savePath))
}
+141
View File
@@ -0,0 +1,141 @@
// 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/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
"os"
"strconv"
)
func CmdLoglist() cli.Command {
return cli.Command{
Name: "loglist",
Usage: "列出帐号列表",
Description: "列出所有已登录的阿里账号",
Category: "阿里云盘账号",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
fmt.Println(config.Config.UserList.String())
return nil
},
}
}
func CmdSu() cli.Command {
return cli.Command{
Name: "su",
Usage: "切换阿里账号",
Description: `
切换已登录的阿里账号:
如果运行该条命令没有提供参数, 程序将会列出所有的帐号, 供选择切换.
示例:
aliyunpan su
aliyunpan su <uid>
`,
Category: "阿里云盘账号",
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() >= 2 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
numLogins := config.Config.NumLogins()
if numLogins == 0 {
fmt.Printf("未设置任何帐号, 不能切换\n")
return nil
}
var (
inputData = c.Args().Get(0)
uid string
)
if c.NArg() == 1 {
// 直接切换
uid = inputData
} else if c.NArg() == 0 {
// 输出所有帐号供选择切换
cli.HandleAction(cmder.App().Command("loglist").Action, c)
// 提示输入 index
var index string
fmt.Printf("输入要切换帐号的 # 值 > ")
_, err := fmt.Scanln(&index)
if err != nil {
return nil
}
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
}
} else {
cli.ShowCommandHelp(c, c.Command.Name)
}
switchedUser, err := config.Config.SwitchUser(uid)
if err != nil {
fmt.Printf("切换用户失败, %s\n", err)
return nil
}
if switchedUser == nil {
switchedUser = TryLogin()
}
if switchedUser != nil {
fmt.Printf("切换用户: %s\n", switchedUser.Nickname)
} else {
fmt.Printf("切换用户失败\n")
}
return nil
},
}
}
func CmdWho() cli.Command {
return cli.Command{
Name: "who",
Usage: "获取当前帐号",
Description: "获取当前帐号的信息",
Category: "阿里云盘账号",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
os.Exit(1)
}
activeUser := config.Config.ActiveUser()
cloudName := activeUser.GetDriveById(activeUser.ActiveDriveId).DriveName
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
},
}
}
+427
View File
@@ -0,0 +1,427 @@
// 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/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)
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
}
)
// 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
}
// 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 {
return nil, err
}
files = append(files, *ps...)
}
return files, nil
}
// makePathAbsolute 拼接路径,确定返回路径为绝对路径
func makePathAbsolute(driveId string, patterns ...string) (panpaths []string, err error) {
acUser := GetActiveUser()
for k := range patterns {
ps := acUser.PathJoin(driveId, patterns[k])
panpaths = append(panpaths, ps)
}
return panpaths, nil
}
func RandomStr(count int) string {
//STR_SET := "abcdefjhijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ1234567890"
STR_SET := "abcdefjhijklmnopqrstuvwxyz1234567890"
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()
}
func GetAllPathFolderByPath(pathStr string) []string {
dirNames := strings.Split(pathStr, "/")
dirs := []string{}
p := "/"
dirs = append(dirs, p)
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
}
+55
View File
@@ -0,0 +1,55 @@
package config
import (
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/library-go/expires"
"path"
"time"
)
// DeleteCache 删除含有 dirs 的缓存
func (pu *PanUser) DeleteCache(dirs []string) {
cache := pu.cacheOpMap.LazyInitCachePoolOp(pu.ActiveDriveId)
for _, v := range dirs {
key := v + "_" + "OrderByName"
_, ok := cache.Load(key)
if ok {
cache.Delete(key)
}
}
}
// DeleteOneCache 删除缓存
func (pu *PanUser) DeleteOneCache(dirPath string) {
ps := []string{dirPath}
pu.DeleteCache(ps)
}
// CacheFilesDirectoriesList 缓存获取
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.OpenapiPanClient().FileInfoByPath(pu.ActiveDriveId, pathStr)
if apiError != nil {
return nil
}
fileListParam := &aliyunpan.FileListParam{
DriveId: pu.ActiveDriveId,
ParentFileId: fi.FileId,
}
fdl, apiError = pu.panClient.OpenapiPanClient().FileListGetAll(fileListParam, 200)
if apiError != nil {
return nil
}
// construct full path
for _, f := range fdl {
f.Path = path.Join(pathStr, f.FileName)
}
return expires.NewDataExpires(fdl, 10*time.Minute)
})
if apiError != nil {
return
}
return data.Data().(aliyunpan.FileList), nil
}
+31
View File
@@ -0,0 +1,31 @@
// 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 config
import (
"errors"
)
var (
//ErrNotLogin 未登录帐号错误
ErrNotLogin = errors.New("user not login")
//ErrConfigFilePathNotSet 未设置配置文件
ErrConfigFilePathNotSet = errors.New("config file not set")
//ErrConfigFileNotExist 未设置Config, 未初始化
ErrConfigFileNotExist = errors.New("config file not exist")
//ErrConfigFileNoPermission Config文件无权限访问
ErrConfigFileNoPermission = errors.New("config file permission denied")
//ErrConfigContentsParseError 解析Config数据错误
ErrConfigContentsParseError = errors.New("config contents parse error")
)
+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
}
+595
View File
@@ -0,0 +1,595 @@
// 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 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"
"sync"
)
const (
// EnvDownloadDir 配置下载环境变量
EnvDownloadDir = "ALIYUNPAN_DOWNLOAD_DIR"
// EnvVerbose 启用调试环境变量
EnvVerbose = "ALIYUNPAN_VERBOSE"
// EnvConfigDir 配置路径环境变量
EnvConfigDir = "ALIYUNPAN_CONFIG_DIR"
// ConfigName 配置文件名
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 (
CmdConfigVerbose = logger.New("CONFIG", EnvVerbose)
configFilePath = filepath.Join(GetConfigDir(), ConfigName)
// Config 配置信息, 由外部调用
Config = NewConfig(configFilePath)
)
type UpdateCheckInfo struct {
PreferUpdateSrv string `json:"preferUpdateSrv"` // 优先更新服务器,github | tickstep
LatestVer string `json:"latestVer"` // 最后检测到的版本
CheckTime int64 `json:"checkTime"` // 最后检测的时间戳,单位为秒
}
// PanConfig 配置详情
type PanConfig struct {
ConfigVer string `json:"configVer"`
ActiveUID string `json:"activeUID"`
UserList PanUserList `json:"userList"`
CacheSize int `json:"cacheSize"` // 下载缓存
MaxDownloadParallel int `json:"maxDownloadParallel"` // 最大下载并发量,即同时下载文件最大数量
MaxUploadParallel int `json:"maxUploadParallel"` // 最大上传并发量,即同时上传文件最大数量
MaxDownloadRate int64 `json:"maxDownloadRate"` // 限制最大下载速度,单位 B/s, 即字节/每秒
MaxUploadRate int64 `json:"maxUploadRate"` // 限制最大上传速度,单位 B/s, 即字节/每秒
SaveDir string `json:"saveDir"` // 下载储存路径
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
activeUser *PanUser
}
// NewConfig 返回 PanConfig 指针对象
func NewConfig(configFilePath string) *PanConfig {
c := &PanConfig{
configFilePath: configFilePath,
}
return c
}
// Init 初始化配置
func (c *PanConfig) Init() error {
return c.init()
}
// Reload 从文件重载配置
func (c *PanConfig) Reload() error {
return c.init()
}
// Close 关闭配置文件
func (c *PanConfig) Close() error {
if c.configFile != nil {
err := c.configFile.Close()
c.configFile = nil
return err
}
return nil
}
// Save 保存配置信息到配置文件
func (c *PanConfig) Save() error {
// 检测配置项是否合法, 不合法则自动修复
c.fix()
err := c.lazyOpenConfigFile()
if err != nil {
return err
}
c.fileMu.Lock()
defer c.fileMu.Unlock()
data, err := jsoniter.MarshalIndent(c, "", " ")
if err != nil {
// json数据生成失败
panic(err)
}
// 减掉多余的部分
err = c.configFile.Truncate(int64(len(data)))
if err != nil {
return err
}
_, err = c.configFile.Seek(0, os.SEEK_SET)
if err != nil {
return err
}
_, err = c.configFile.Write(data)
if err != nil {
return err
}
return nil
}
func (c *PanConfig) init() error {
if c.configFilePath == "" {
return ErrConfigFileNotExist
}
c.initDefaultConfig()
err := c.loadConfigFromFile()
if err != nil {
return err
}
// 设置全局代理
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 != "" {
ips := ParseLocalAddress(c.LocalAddrs, strings.ToLower(c.PreferIPType))
if len(ips) > 0 {
logger.Verboseln("bind local address list: ", ips)
requester.SetLocalTCPAddrList(ips...)
}
}
return nil
}
// lazyOpenConfigFile 打开配置文件
func (c *PanConfig) lazyOpenConfigFile() (err error) {
if c.configFile != nil {
return nil
}
c.fileMu.Lock()
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 {
if os.IsPermission(err) {
return ErrConfigFileNoPermission
}
if os.IsExist(err) {
return ErrConfigFileNotExist
}
return err
}
return nil
}
// loadConfigFromFile 载入配置
func (c *PanConfig) loadConfigFromFile() (err error) {
err = c.lazyOpenConfigFile()
if err != nil {
return err
}
// 未初始化
info, err := c.configFile.Stat()
if err != nil {
return err
}
if info.Size() == 0 {
err = c.Save()
return err
}
c.fileMu.Lock()
defer c.fileMu.Unlock()
_, err = c.configFile.Seek(0, os.SEEK_SET)
if err != nil {
return err
}
err = jsonhelper.UnmarshalData(c.configFile, c)
if err != nil {
return ErrConfigContentsParseError
}
if c.DeviceName == "" {
c.DeviceName = DefaultDeviceName
}
if c.ClientId == "" {
c.ClientId = DefaultClientId
}
return nil
}
func (c *PanConfig) initDefaultConfig() {
//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
}
// 如果不是绝对路径, 从程序目录寻找
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
}
}
}
}
// 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 {
if c.activeUser == nil {
if c.UserList == nil {
return nil
}
if c.ActiveUID == "" {
return nil
}
for _, u := range c.UserList {
if u.UserId == c.ActiveUID {
if u.PanClient() == nil {
// restore client
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
}
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().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().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 = "/"
}
}
}
}
c.activeUser = u
return u
}
}
return nil
}
return c.activeUser
}
func (c *PanConfig) SetActiveUser(user *PanUser) *PanUser {
needToInsert := true
for _, u := range c.UserList {
if u.UserId == user.UserId {
// update user info
u.Nickname = user.Nickname
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
}
}
if needToInsert {
// insert
c.UserList = append(c.UserList, user)
}
// setup active user
c.ActiveUID = user.UserId
// clear active user cache
c.activeUser = nil
// reload
return c.ActiveUser()
}
func (c *PanConfig) fix() {
}
// NumLogins 获取登录的用户数量
func (c *PanConfig) NumLogins() int {
return len(c.UserList)
}
// SwitchUser 切换登录用户
func (c *PanConfig) SwitchUser(uid string) (*PanUser, error) {
for _, u := range c.UserList {
if u.UserId == uid {
return c.SetActiveUser(u), nil
}
}
return nil, fmt.Errorf("未找到指定的账号")
}
// DeleteUser 删除用户,并自动切换登录用户为用户列表第一个
func (c *PanConfig) DeleteUser(uid string) (*PanUser, error) {
for idx, u := range c.UserList {
if u.UserId == uid {
// delete user from user list
c.UserList = append(c.UserList[:idx], c.UserList[idx+1:]...)
c.ActiveUID = ""
c.activeUser = nil
if len(c.UserList) > 0 {
c.SwitchUser(c.UserList[0].UserId)
}
return u, nil
}
}
return nil, fmt.Errorf("未找到指定的账号")
}
// HTTPClient 返回设置好的 HTTPClient
func (c *PanConfig) HTTPClient(ua string) *requester.HTTPClient {
client := requester.NewHTTPClient()
if ua != "" {
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
}
+124
View File
@@ -0,0 +1,124 @@
// 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 config
import (
"os"
"strconv"
"strings"
"github.com/olekukonko/tablewriter"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/requester"
)
// SetProxy 设置代理
func (c *PanConfig) SetProxy(proxy string) {
c.Proxy = proxy
requester.SetGlobalProxy(proxy)
}
// SetLocalAddrs 设置localAddrs
func (c *PanConfig) SetLocalAddrs(localAddrs string) {
c.LocalAddrs = 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
func (c *PanConfig) SetCacheSizeByStr(sizeStr string) error {
size, err := converter.ParseFileSizeStr(sizeStr)
if err != nil {
return err
}
c.CacheSize = int(size)
return nil
}
// SetMaxDownloadRateByStr 设置 max_download_rate
func (c *PanConfig) SetMaxDownloadRateByStr(sizeStr string) error {
size, err := converter.ParseFileSizeStr(stripPerSecond(sizeStr))
if err != nil {
return err
}
c.MaxDownloadRate = size
return nil
}
// SetMaxUploadRateByStr 设置 max_upload_rate
func (c *PanConfig) SetMaxUploadRateByStr(sizeStr string) error {
size, err := converter.ParseFileSizeStr(stripPerSecond(sizeStr))
if err != nil {
return err
}
c.MaxUploadRate = size
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 ~ 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()
}
+425
View File
@@ -0,0 +1,425 @@
// 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 config
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"`
}
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"`
AccountName string `json:"accountName"`
// 文件(备份盘)
Workdir string `json:"workdir"`
WorkdirFileEntity aliyunpan.FileEntity `json:"workdirFileEntity"`
// 资源库
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"`
// 授权Token信息
TicketId string `json:"ticketId"`
WebapiToken *PanClientToken `json:"webapiToken"`
OpenapiToken *PanClientToken `json:"openapiToken"`
// API客户端
panClient *PanClient `json:"-"`
cacheOpMap cachemap.CacheOpMap `json:"-"`
}
type PanUserList []*PanUser
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 openapiToken == nil {
return nil, apierror.NewFailedApiError("openapi token is empty")
}
//if webapiToken == nil {
// return nil, apierror.NewFailedApiError("webapi token is empty")
//}
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
})
// open api token maybe expired
// check & refresh new one
openUserInfo, err := openPanClient.GetUserInfo()
if err != nil {
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 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
if openUserInfo.Nickname != "" {
name = openUserInfo.Nickname
}
u.UserId = openUserInfo.UserId
u.Nickname = name
u.AccountName = openUserInfo.UserName
// default file drive
if u.ActiveDriveId == "" {
u.ActiveDriveId = openUserInfo.FileDriveId
}
// drive list
u.DriveList = DriveInfoList{
{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. please login again")
}
// return user
return u, nil
}
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) {
return p
}
wd := "/"
di := pu.GetDriveById(driveId)
if di != nil {
if di.IsFileDrive() {
wd = pu.Workdir
} else if di.IsResourceDrive() {
wd = pu.ResourceWorkdir
} else if di.IsAlbumDrive() {
wd = pu.AlbumWorkdir
}
}
return path.Join(wd, p)
}
func (pu *PanUser) FreshWorkdirInfo() {
if pu.IsFileDriveActive() {
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().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(GetDownloadDir(), fmt.Sprintf("%s", pu.UserId), filePanPath)
dir, err := filepath.Abs(dirStr)
if err != nil {
dir = filepath.Clean(dirStr)
}
return dir
}
func (pu *PanUser) GetDriveByTag(tag string) *DriveInfo {
for _, item := range pu.DriveList {
if item.DriveTag == tag {
return item
}
}
return nil
}
func (pu *PanUser) GetDriveById(id string) *DriveInfo {
for _, item := range pu.DriveList {
if item.DriveId == id {
return item
}
}
return nil
}
func (pu *PanUser) GetActiveDriveInfo() *DriveInfo {
for _, item := range pu.DriveList {
if item.DriveId == pu.ActiveDriveId {
return item
}
}
return nil
}
func (pu *PanUser) IsFileDriveActive() bool {
d := pu.GetActiveDriveInfo()
return d != nil && d.IsFileDrive()
}
func (pu *PanUser) IsAlbumDriveActive() bool {
d := pu.GetActiveDriveInfo()
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"
}
+188
View File
@@ -0,0 +1,188 @@
// 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 config
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 {
builder := &strings.Builder{}
tb := cmdtable.NewTable(builder)
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER})
tb.SetHeader([]string{"#", "uid", "用户名", "昵称"})
for k, userInfo := range *pl {
tb.Append([]string{strconv.Itoa(k + 1), userInfo.UserId, userInfo.AccountName, userInfo.Nickname})
}
tb.Render()
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 {
return 1
}
p := parallel / downloadLoad
if p < 1 {
return 1
}
return p
}
func stripPerSecond(sizeStr string) string {
i := strings.LastIndex(sizeStr, "/")
if i < 0 {
return sizeStr
}
return sizeStr[:i]
}
func showMaxRate(size int64) string {
if size <= 0 {
return "不限制"
}
return converter.ConvertFileSize(size, 2) + "/s"
}
// EncryptString 加密
func EncryptString(text string) string {
if text == "" {
return ""
}
d := []byte(text)
key := []byte(ids.GetUniqueId("aliyunpan", 16))
r, e := crypto.EncryptAES(d, key)
if e != nil {
return text
}
return hex.EncodeToString(r)
}
// DecryptString 解密
func DecryptString(text string) string {
defer func() {
if err := recover(); err != nil {
logger.Verboseln("decrypt string failed, maybe the key has been changed")
}
}()
if text == "" {
return ""
}
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
key := []byte(ids.GetUniqueId("aliyunpan", 16))
r, e := crypto.DecryptAES(d, key)
if e != nil {
return text
}
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
}

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