Compare commits

...

446 Commits

Author SHA1 Message Date
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
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
188 changed files with 28520 additions and 511 deletions
+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.
+304 -509
View File
@@ -1,539 +1,325 @@
# 关于
阿里云盘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. 支持阿里云ECS环境下使用内网链接上传/下载,速度更快(只支持阿里经典网络,最高可达100MB/s),还可以节省公网带宽流量(配置transfer_url_type=2即可)
9. 支持[JavaScript插件](docs/manual.md#JavaScript插件),你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求
10. 支持个人相册的相关操作,支持批量下载相册所有照片、视频文件到本地
# 目录
- [关于](#关于)
- [特色](#特色)
- [下载/运行 说明](#下载运行说明)
* [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同步盘)
- [如何使用](#如何使用)
- [基本使用](#基本使用)
- [修改配置目录](#修改配置目录)
- [启动程序](#启动程序)
- [查看帮助](#查看帮助)
- [登录](#登录)
- [切换网盘](#切换网盘)
- [查看文件列表](#查看文件列表)
- [下载文件](#下载文件)
- [上传文件](#上传文件)
- [同步备份文件](#同步备份文件)
- [更多命令](#更多命令)
- [常见问题](#常见问题)
- [如何获取RefreshToken](#如何获取refreshtoken)
- [如何开启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,例如国产龙芯CPU
6. macOS amd64适用Intel CPU的机器,macOS arm64目前主要是适用苹果M1芯片的机器
7. iOS arm64适用iPhone手机,并且必须是越狱的手机才能正常运行
# 特色
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.2.9/aliyunpan-v0.2.9-linux-amd64.zip
unzip aliyunpan-v0.2.9-linux-amd64.zip
cd aliyunpan-v0.2.9-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 --silent
```
## docker安装
### sync同步盘
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份三种模式。支持JavaScript插件对备份文件进行过滤。
备份功能支持以下三种模式:
1. 备份本地文件,即上传本地文件到网盘,始终保持本地文件有一个完整的备份在网盘
2. 备份云盘文件,即下载网盘文件到本地,始终保持网盘的文件有一个完整的备份在本地
3. 双向备份,保持网盘文件和本地文件严格一致
```
docker run -d --name=aliyunpan-sync --restart=always -v "<your local dir>:/home/app/data" -e TZ="Asia/Shanghai" -e ALIYUNPAN_REFRESH_TOKEN="<your refreshToken>" -e ALIYUNPAN_PAN_DIR="<your drive pan dir>" -e ALIYUNPAN_SYNC_MODE="upload" -e ALIYUNPAN_TASK_STEP="sync" tickstep/aliyunpan-sync:v0.2.9
<your local dir>:本地目录绝对路径,例如:/tickstep/Documents/设计文档
ALIYUNPAN_PAN_DIR:云盘目录
ALIYUNPAN_REFRESH_TOKEN:RefreshToken
ALIYUNPAN_SYNC_MODE:备份模式,支持三种: upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)
ALIYUNPAN_TASK_STEP:任务步骤, 支持两种: scan(只扫描并建立同步数据库),sync(正常启动同步任务)。如果你同步目录文件非常多,首次运行最好先跑一次scan步骤,然后再正常启动文件同步任务
```
更详情文档请参考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|
```
需要先登录,已经登录过的可以跳过此步。
RefreshToken获取教程请查看:[如何获取RefreshToken](#如何获取RefreshToken)
```shell
aliyunpan > login -RefreshToken=32994cd2c43...4d505fa79
阿里云盘登录成功: tickstep
aliyunpan:/ tickstep$
```
### 切换网盘
支持切换备份盘、资源库,默认工作在备份盘下。
```shell
aliyunpan:/ tickstep(备份盘)$ drive
# DRIVE ID 网盘名称
1 11519221 备份盘
2 1311893110 资源库
3 61104421 相册
### 例子
```
导入文件 /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
----
```
### 设置分享文件/目录
阿里目前之支持少数文件类型的分享,不支持的文件分享会提示分享失败
### 下载文件
```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:/ tickstep$
```
aliyunpan share set <文件/目录1> <文件/目录2> ...
aliyunpan share s <文件/目录1> <文件/目录2> ...
下载支持两种链接类型:1-默认类型 2-阿里ECS环境类型
在普通网络下,下载速度可以达到10MB/s,在阿里ECS(必须是"经典网络"类型的机器)环境下,下载速度单文件可以轻松达到20MB/s,多文件可以达到100MB/s
![](./assets/images/download_file_ecs_speed_screenshot.gif)
![](./assets/images/download_file_speed_screenshot.gif)
### 上传文件
```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
```
上传支持两种链接类型:1-默认类型 2-阿里ECS环境类型
在阿里ECS(必须是"经典网络"类型的机器)环境下,上传速度单文件可以轻松达到30MB/s,多文件可以达到100MB/s
![](./assets/images/upload_file_speed_screenshot.gif)
### 同步备份文件
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份三种模式。支持JavaScript插件对备份文件进行过滤。
***注意:如果同步目录下有非常多的文件,最好在首次备份前先运行一次scan任务,等scan任务完成并建立起同步数据库后,再正常启动同步任务。这样同步任务可以更加快速同步并且能有效避免同步重复文件。***
例如:将本地目录 `/tickstep/Documents/设计文档` 中的文件备份上传到云盘目录 `/备份盘/我的文档`
```shell
首次运行建议先扫描并构建同步数据库,如下:
aliyunpan:/ tickstep$ sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload" -step scan
然后再正常启动同步任务,如下:
aliyunpan:/ tickstep$ sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload"
启动同步备份进程
备份配置文件:(使用命令行配置)
链接类型:默认链接
下载并发:2
上传并发:2
下载分片大小:1.00MB
上传分片大小:10.00MB
启动同步任务
任务: 设计文档(de3d6b69a607497b73624bcca0845f19)
同步模式: 备份本地文件(只上传)
本地目录: /tickstep/Documents/设计文档
云盘目录: /备份盘/我的文档
```
### 列出已分享文件/目录
```
aliyunpan share list
aliyunpan share l
```
## 更多命令
更多更详细的命令请查看手册:[命令手册](docs/manual.md)。
### 取消分享文件/目录
```
aliyunpan share cancel <shareid_1> <shareid_2> ...
aliyunpan share c <shareid_1> <shareid_2> ...
```
目前只支持通过分享id (shareid) 来取消分享.
### 分享秒传链接
秒传链接支持所有类型的文件分享,可以突破阿里的分享限制。得到的链接可以使用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/
```
## 显示和修改程序配置项
```
# 显示配置
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
# 常见问题
## 如何获取RefreshToken
需要通过浏览器获取refresh_token。这里以Chrome浏览器为例,其他浏览器类似。
打开 [阿里云盘网页](https://www.aliyundrive.com/drive) 并进行登录,然后F12按键打开浏览器调试菜单,按照下面步骤进行
![](./assets/images/how-to-get-refresh-token.png)
## 2 如何开启Debug调试日志
或者直接在控制台输入以下命令获取
```
JSON.parse(localStorage.getItem("token")).refresh_token
```
![](./assets/images/how-to-get-refresh-token-cmd.png)
## 如何开启Debug调试日志
当需要定位问题,或者提交issue的时候抓取log,则需要开启debug日志。步骤如下:
### 第一步
@@ -552,12 +338,21 @@ Windows
打开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)
> [hacdias/webdav](https://github.com/hacdias/webdav)
> [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.2.9.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: 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: 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

+127
View File
@@ -0,0 +1,127 @@
// ==========================================================================================
// aliyunpan JS插件回调处理函数
// 支持 JavaScript ECMAScript 5.1 语言规范
//
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
// ==========================================================================================
// ------------------------------------------------------------------------------------------
// 函数说明:下载文件前的回调函数
//
// 参数说明
// context - 当前调用的上下文信息
// {
// "appName": "aliyunpan",
// "version": "v0.1.3",
// "userId": "11001d48564f43b3bc5662874f04bb11",
// "nickname": "tickstep",
// "fileDriveId": "19519111",
// "albumDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户文件网盘ID
// albumDriveId - 用户相册网盘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",
// "localFilePath": "aliyunpan\\Downloads\\token.bat"
// }
// 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",
// "albumDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户文件网盘ID
// albumDriveId - 用户相册网盘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",
// "downloadResult": "success",
// "localFilePath": "aliyunpan\\Downloads\\token.bat"
// }
// driveId - 网盘ID
// driveFileName - 网盘文件名
// driveFilePath - 网盘文件绝对完整路径
// driveFileSize - 网盘文件大小,单位B
// driveFileSha1 - 网盘文件SHA1
// driveFileType - 网盘文件类型,file-文件,folder-文件夹
// driveFileUpdatedAt - 网盘文件修改时间
// downloadResult - 下载结果,success-成功,fail-失败
// localFilePath - 下载文件到本地保存的路径,这个是相对路径,相对指定下载的目标文件夹
//
// 返回值说明
// (没有返回值)
// ------------------------------------------------------------------------------------------
function downloadFileFinishCallback(context, params) {
console.log(params)
}
+209
View File
@@ -0,0 +1,209 @@
// ==========================================================================================
// aliyunpan JS插件回调处理函数
// 支持 JavaScript ECMAScript 5.1 语言规范
//
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
// ==========================================================================================
// ------------------------------------------------------------------------------------------
// 函数说明:同步备份-扫描本地文件前的回调函数
//
// 参数说明
// context - 当前调用的上下文信息
// {
// "appName": "aliyunpan",
// "version": "v0.1.3",
// "userId": "11001d48564f43b3bc5662874f04bb11",
// "nickname": "tickstep",
// "fileDriveId": "19519111",
// "albumDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户文件网盘ID
// albumDriveId - 用户相册网盘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",
// "albumDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户文件网盘ID
// albumDriveId - 用户相册网盘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",
// "albumDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户文件网盘ID
// albumDriveId - 用户相册网盘ID
//
// params - 同步文件后的参数
// {
// "action": "upload",
// "actionResult": "success",
// "driveId": "19519221",
// "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-上传文件,
// delete_local-删除本地文件,
// delete_pan-删除云盘文件,
// create_local_folder-创建本地文件夹,
// create_pan_folder-创建云盘文件夹
// actionResult - 同步结果,success-成功,fail-失败
// driveId - 网盘ID
// fileName - 文件名称
// filePath - 文件完整路径
// fileSha1 - 文件SHA1
// fileSize - 文件大小,单位B
// fileType - 文件类型,file-文件,folder-文件夹
// fileUpdatedAt - 文件修改时间
//
// 返回值说明
// (无)
// ------------------------------------------------------------------------------------------
function syncFileFinishCallback(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",
// "albumDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户文件网盘ID
// albumDriveId - 用户相册网盘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",
// "albumDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户文件网盘ID
// albumDriveId - 用户相册网盘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",
// "albumDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户文件网盘ID
// albumDriveId - 用户相册网盘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
+162
View File
@@ -0,0 +1,162 @@
#!/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
jtool --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 -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-arm" windows arm
############### 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)
}
+68
View File
@@ -0,0 +1,68 @@
# alpine:3.15.0
# 参数
ARG DOCKER_IMAGE_HASH
FROM alpine@sha256:$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
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_REFRESH_TOKEN=""
ENV ALIYUNPAN_TRANSFER_URL_TYPE=0
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_PRIORITY=time
ENV ALIYUNPAN_SYNC_LOG=false
ENV ALIYUNPAN_LOCAL_DELAY_TIME=3
ENV ALIYUNPAN_TASK_STEP=sync
ENV ALIYUNPAN_DEVICE_ID=""
# 运行
ENTRYPOINT ./app.sh
+60
View File
@@ -0,0 +1,60 @@
#!/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-5abb3bb04814",
"localFolderPath": "$ALIYUNPAN_LOCAL_DIR",
"panFolderPath": "$ALIYUNPAN_PAN_DIR",
"mode": "$ALIYUNPAN_SYNC_MODE",
"priority": "$ALIYUNPAN_SYNC_PRIORITY",
"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
# check login already or not
./aliyunpan who
if [ $? -eq 0 ]
then
echo "cache token is valid, not need to re-login"
else
echo "login use refresh token: ${ALIYUNPAN_REFRESH_TOKEN}"
./aliyunpan login -RefreshToken=${ALIYUNPAN_REFRESH_TOKEN}
fi
./aliyunpan config set -transfer_url_type ${ALIYUNPAN_TRANSFER_URL_TYPE}
if [ "$ALIYUNPAN_SYNC_LOG" = "true" ]
then
./aliyunpan config set -file_record_config 1
else
./aliyunpan config set -file_record_config 2
fi
./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} -step ${ALIYUNPAN_TASK_STEP}
+41
View File
@@ -0,0 +1,41 @@
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
environment:
- TZ=Asia/Shanghai
# refresh token
- ALIYUNPAN_REFRESH_TOKEN=8bf31a...7628a8ad5a0975bf2d9
# 上传下载链接类型:1-默认 2-阿里ECS环境
- ALIYUNPAN_TRANSFER_URL_TYPE=1
# 下载文件并发数
- 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(备份云盘文件到本地), sync(双向同步备份,慎用!!!)
- ALIYUNPAN_SYNC_MODE=upload
# 优先级,只对双向同步备份模式有效。选项支持三种: time-时间优先,local-本地优先,pan-网盘优先
- ALIYUNPAN_SYNC_PRIORITY=time
# 是否显示文件备份过程日志,true-显示,false-不显示
- ALIYUNPAN_SYNC_LOG=true
# 本地文件修改检测延迟间隔,单位秒。如果本地文件会被频繁修改,例如录制视频文件,配置好该时间可以避免上传未录制好的文件
- ALIYUNPAN_LOCAL_DELAY_TIME=3
# 任务步骤, 支持两种: scan(只扫描并建立同步数据库),sync(正常启动同步任务)
- ALIYUNPAN_TASK_STEP=sync
# 登录设备的ID,默认为空,程序会自动生成一个ID
- ALIYUNPAN_DEVICE_ID=
+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
+59
View File
@@ -0,0 +1,59 @@
# alpine:3.15.0
# 参数
ARG DOCKER_IMAGE_HASH
FROM alpine@sha256:$DOCKER_IMAGE_HASH
LABEL author="tickstep"
LABEL email="tickstep@outlook.com"
LABEL version="1.0"
LABEL description="webdav file server for aliyun cloud drive"
# 时区
ENV TZ=Asia/Shanghai
# 手动下载tzdata安装包,注意要下载对应架构的: https://dl-cdn.alpinelinux.org/alpine/v3.15/community/
RUN apk add -U 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
# 复制文件
COPY ./docker/webdav/app.sh /home/app/app.sh
RUN chmod +x /home/app/app.sh
COPY ./docker/webdav/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
# 端口
EXPOSE 23077
# 环境变量
ENV ALIYUNPAN_DOCKER=1
ENV ALIYUNPAN_CONFIG_DIR=/home/app/config
ENV ALIYUNPAN_AUTH_USER=admin
ENV ALIYUNPAN_AUTH_PASSWORD=admin
ENV ALIYUNPAN_WEBDAV_MODE=rw
ENV ALIYUNPAN_PAN_DRIVE=File
ENV ALIYUNPAN_PAN_DIR=/
ENV ALIYUNPAN_REFRESH_TOKEN=""
ENV ALIYUNPAN_TRANSFER_URL_TYPE=0
ENV ALIYUNPAN_BLOCK_SIZE=10240
ENV ALIYUNPAN_DEVICE_ID=""
ENTRYPOINT ./app.sh
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
cd /home/app
chmod +x ./aliyunpan
# 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
# check login already or not
./aliyunpan who
if [ $? -eq 0 ]
then
echo "cache token is valid, not need to re-login"
else
echo "login use refresh token: ${ALIYUNPAN_REFRESH_TOKEN}"
./aliyunpan login -RefreshToken=${ALIYUNPAN_REFRESH_TOKEN}
fi
./aliyunpan config set -transfer_url_type ${ALIYUNPAN_TRANSFER_URL_TYPE}
./aliyunpan webdav start -ip "0.0.0.0" -port 23077 -webdav_user "${ALIYUNPAN_AUTH_USER}" -webdav_password "${ALIYUNPAN_AUTH_PASSWORD}" -webdav_mode "${ALIYUNPAN_WEBDAV_MODE}" -pan_drive "${ALIYUNPAN_PAN_DRIVE}" -pan_dir_path "${ALIYUNPAN_PAN_DIR}" -bs ${ALIYUNPAN_BLOCK_SIZE}
+33
View File
@@ -0,0 +1,33 @@
version: '3'
services:
webdav:
image: tickstep/aliyunpan-webdav:v0.1.6
container_name: aliyunpan-webdav
restart: always
ports:
- 23077:23077
environment:
- TZ=Asia/Shanghai
# refresh token
- ALIYUNPAN_REFRESH_TOKEN=4dd40e8b165...42b8833e4a507b6
# webdav 登录用户名
- ALIYUNPAN_AUTH_USER=admin
# webdav 登录密码
- ALIYUNPAN_AUTH_PASSWORD=admin
# webdav 网盘模式,支持:rw-读写,ro-只读
- ALIYUNPAN_WEBDAV_MODE=rw
# 指定网盘类型为文件,可选: File-文件 Album-相册
- ALIYUNPAN_PAN_DRIVE=File
# 指定网盘文件夹作为webdav服务根目录
- ALIYUNPAN_PAN_DIR=/
# 上传下载链接类型:1-默认 2-阿里ECS环境
- ALIYUNPAN_TRANSFER_URL_TYPE=1
# 上传数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
- ALIYUNPAN_BLOCK_SIZE=10240
# 登录设备的ID,默认为空,程序会自动生成一个ID
- ALIYUNPAN_DEVICE_ID=
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
# 检查webdav的服务端口是否存在
netstat -luntp | awk '{print $4}' | grep 23077
if [ $? == 0 ]
then
echo $?
exit 0
else
echo $?
exit 1
fi
+15
View File
@@ -0,0 +1,15 @@
# 关于 Windows EXE ICO 和应用信息编译
为了编译出来的windows的exe文件带有ico和应用程序信息,需要使用 github.com/josephspurrier/goversioninfo/cmd/goversioninfo 工具
工具安装,运行下面的命令即可生成工具。也可以直接用 bin/ 文件夹下面的编译好的
```
go get 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图标
+1253
View File
File diff suppressed because it is too large Load Diff
+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.20
require (
github.com/GeertJohan/go.incremental v1.0.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.1.8
github.com/tickstep/bolt v1.3.4
github.com/tickstep/library-go v0.1.1
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
)
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/sys v0.0.0-20210423082822-04245dca01da // 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
+145
View File
@@ -0,0 +1,145 @@
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/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 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tickstep/aliyunpan-api v0.1.8 h1:heK04T2F1K5eDSHDqHJ2yHA6RSBqyN/4yqYjHClbJvI=
github.com/tickstep/aliyunpan-api v0.1.8/go.mod h1:SN+N5Vh3lLq3dVimlxKxviCl50gLGXZO96nHHc3qd2o=
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.0/go.mod h1:uAHeNOIpoywCzlaeLrWmmRSupn03m9kJVZKOEmuarmA=
github.com/tickstep/library-go v0.1.1 h1:xPAyddCJkZy5S3qUxCg0hoEG9PR8/rdIQiLMrt2OtWI=
github.com/tickstep/library-go v0.1.1/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/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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=
+753
View File
@@ -0,0 +1,753 @@
// 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/aliyunpan/internal/file/downloader"
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
"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 CmdAlbum() cli.Command {
return cli.Command{
Name: "album",
Aliases: []string{"abm"},
Usage: "个人相册(Beta)",
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
}
RunAlbumList()
return nil
},
Flags: []cli.Flag{},
},
{
Name: "new",
Aliases: []string{""},
Usage: "创建相簿",
UsageText: cmder.App().Name + " album new",
Description: `
示例:
新建相簿,名称为:我的相簿2022
aliyunpan album new "我的相簿2022"
新建相簿,名称为:我的相簿2022,描述为:存放2022所有文件
aliyunpan album new "我的相簿2022" "存放2022所有文件"
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
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 album rm "我的相簿2022"
删除名称为"我的相簿2022-1" 和 "我的相簿2022-2"的相簿
aliyunpan album rm "我的相簿2022-1" "我的相簿2022-2"
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
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 album rename "我的相簿2022" "我的相簿2022-new"
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
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 album list-file "我的相簿2022"
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
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 album rm-file 我的相簿2022 1.png 2.png
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
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 album add-file 我的相簿2022 1.png 2.png
增加当前目录下的 myFolder 文件夹下所有文件到相簿 "我的相簿2022" 中
aliyunpan album add-file 我的相簿2022 myFolder
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
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 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,
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().AlbumListGetAll(&aliyunpan.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().AlbumCreate(&aliyunpan.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().AlbumListGetAll(&aliyunpan.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().AlbumDelete(&aliyunpan.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().AlbumListGetAll(&aliyunpan.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().AlbumEdit(&aliyunpan.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().AlbumListFileGetAll(&aliyunpan.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().AlbumListFileGetAll(&aliyunpan.AlbumListFileParam{
AlbumId: album.AlbumId,
})
if er != nil {
fmt.Printf("获取相簿文件列表失败:%s\n", er)
return
}
param := &aliyunpan.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().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.AlbumAddFileParam{
AlbumId: album.AlbumId,
DriveFileList: []aliyunpan.FileBatchActionParam{},
}
for k := range paths {
filePath := paths[k]
fileInfo, apierr := activeUser.PanClient().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().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().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().EnableCache()
activeUser.PanClient().ClearCache()
defer activeUser.PanClient().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 RefreshTokenInNeed(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
}
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,
UseInternalUrl: config.Config.TransferUrlType == 2,
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().AlbumListFileGetAll(&aliyunpan.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,
PrintFormat: downloadPrintFormat(options.Load),
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,
}
// 设置相簿文件信息
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()
}
}
+139
View File
@@ -0,0 +1,139 @@
// Copyright (c) 2020 tickstep.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
)
func 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().FileInfoByPath(driveId, targetPath)
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)
}
+417
View File
@@ -0,0 +1,417 @@
// 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/library/crypto"
"github.com/tickstep/library-go/getip"
"github.com/urfave/cli"
"net/url"
"path"
"strconv"
"strings"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/internal/config"
)
type (
// 秒传数据项
RapidUploadItem struct {
FileSha1 string
FileSize int64
FilePath string // 绝对路径,包含文件名
}
)
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() *aliyunpan.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
}
// newRapidUploadItem 通过解析秒传链接创建秒传实体
func newRapidUploadItem(rapidUploadShareLink string) (*RapidUploadItem, error) {
if strings.IndexAny(rapidUploadShareLink, "aliyunpan://") != 0 {
return nil, fmt.Errorf("秒传链接格式错误: %s", rapidUploadShareLink)
}
// 格式:aliyunpan://文件名|sha1|文件大小|<相对路径>
rapidUploadShareLinkStr := strings.Replace(rapidUploadShareLink, "aliyunpan://", "", 1)
item := &RapidUploadItem{}
parts := strings.Split(rapidUploadShareLinkStr, "|")
if len(parts) < 4 {
return nil, fmt.Errorf("秒传链接格式错误: %s", rapidUploadShareLink)
}
// hash
if len(parts[1]) == 0 {
return nil, fmt.Errorf("文件sha1错误: %s", rapidUploadShareLink)
}
item.FileSha1 = strings.TrimSpace(parts[1])
// size
if size, e := strconv.ParseInt(parts[2], 10, 64); e == nil {
item.FileSize = size
} else {
return nil, fmt.Errorf("文件大小错误: %s", rapidUploadShareLink)
}
// path
relativePath, _ := url.QueryUnescape(parts[3])
item.FilePath = path.Join(relativePath, parts[0])
// result
return item, nil
}
func newRapidUploadItemFromFileEntity(fileEntity *aliyunpan.FileEntity) *RapidUploadItem {
if fileEntity == nil {
return nil
}
return &RapidUploadItem{
FileSha1: fileEntity.ContentHash,
FileSize: fileEntity.FileSize,
FilePath: fileEntity.Path,
}
}
// 创建秒传链接
// 链接格式说明:aliyunpan://文件名|sha1|文件大小|<相对路径>
// "相对路径" 可以为空,为空代表存储到网盘根目录
func (r *RapidUploadItem) createRapidUploadLink(hideRelativePath bool) string {
fullLink := &strings.Builder{}
p := r.FilePath
p = strings.ReplaceAll(p, "\\", "/")
fileName := path.Base(p)
dirPath := path.Dir(p)
// 去掉开头/
if strings.Index(dirPath, "/") == 0 {
dirPath = dirPath[1:]
}
// 相对路径编码
dirPath = url.QueryEscape(dirPath)
// 隐藏相对路径
if hideRelativePath {
dirPath = ""
}
// 拼接
fmt.Fprintf(fullLink, "aliyunpan://%s|%s|%d|%s",
fileName, strings.ToUpper(r.FileSha1), r.FileSize, dirPath)
return fullLink.String()
}
func CmdConfig() cli.Command {
return cli.Command{
Name: "config",
Usage: "显示和修改程序配置项",
Description: "显示和修改程序配置项",
Category: "配置",
Before: 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("transfer_url_type") {
config.Config.TransferUrlType = c.Int("transfer_url_type")
}
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.IntFlag{
Name: "transfer_url_type",
Usage: "上传下载URL类别,1-默认,2-阿里云ECS",
Value: 1,
},
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",
},
},
},
},
}
}
+21
View File
@@ -0,0 +1,21 @@
package command
import (
"fmt"
"testing"
)
func TestRapidUploadItem_createRapidUploadLink(t *testing.T) {
item := &RapidUploadItem{
FileSha1: "752FCCBFB2436A6FFCA3B287831D4FAA5654B07E",
FileSize: 7005440,
FilePath: "/dgsdg/rtt5/我的文件夹/file我的文件.dmg",
}
fmt.Println(item.createRapidUploadLink(false))
}
func TestRapidUploadItem_newRapidUploadItem(t *testing.T) {
link := "aliyunpan://file我的文件.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|dgsdg%2Frtt5%2F%E6%88%91%E7%9A%84%E6%96%87%E4%BB%B6%E5%A4%B9"
item,_ := newRapidUploadItem(link)
fmt.Println(item)
}
+398
View File
@@ -0,0 +1,398 @@
// 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/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/log"
"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"
"path/filepath"
"runtime"
"sync/atomic"
"time"
)
type (
//DownloadOptions 下载可选参数
DownloadOptions struct {
IsPrintStatus bool
IsExecutedPermission bool
IsOverwrite bool
SaveTo string
Parallel int
Load int
MaxRetry int
NoCheck bool
ShowProgress bool
DriveId string
UseInternalUrl bool // 是否使用内置链接
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
}
// 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)排除@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{
IsPrintStatus: c.Bool("status"),
IsExecutedPermission: c.Bool("x"),
IsOverwrite: c.Bool("ow"),
SaveTo: saveTo,
Parallel: c.Int("p"),
Load: 0,
MaxRetry: c.Int("retry"),
NoCheck: c.Bool("nocheck"),
ShowProgress: !c.Bool("np"),
DriveId: parseDriveId(c),
ExcludeNames: c.StringSlice("exn"),
}
// 获取下载文件锁,保证下载操作单实例
//locker := filelocker.NewFileLocker(config.GetLockerDir() + "/aliyunpan-download")
//if e := filelocker.LockFile(locker, 0755, true, 5*time.Second); e != nil {
// logger.Verboseln(e)
// fmt.Println("本应用其他实例正在执行下载,请先停止或者等待其完成")
// return nil
//}
RunDownload(c.Args(), do)
// 释放文件锁
//if locker != nil {
// filelocker.UnlockFile(locker)
//}
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: "指定同时进行下载文件的数量(取值范围:1 ~ 20)",
},
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,
},
},
}
}
func downloadPrintFormat(load int) string {
if load <= 1 {
return pandownload.DefaultPrintFormat
}
return "\r[%s] ↓ %s/%s %s/s in %s, left %s ..."
}
// RunDownload 执行下载网盘内文件
func RunDownload(paths []string, options *DownloadOptions) {
activeUser := GetActiveUser()
activeUser.PanClient().EnableCache()
activeUser.PanClient().ClearCache()
defer activeUser.PanClient().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 RefreshTokenInNeed(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,
UseInternalUrl: config.Config.TransferUrlType == 2,
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
}
}
paths, err := makePathAbsolute(options.DriveId, paths...)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("\n[*] 注意:由于阿里云盘接口的限制,当前不支持>100M单个文件的下载。")
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{}
// 下载记录器
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/download_file_records.csv")
// 处理队列
for k := range paths {
// 使用通配符匹配
fileList, err2 := matchPathByShellPattern(options.DriveId, paths[k])
if err2 != nil {
fmt.Printf("获取文件出错,请稍后重试: %s\n", paths[k])
continue
}
if fileList == nil || len(fileList) == 0 {
// 文件不存在
fmt.Printf("文件不存在: %s\n", paths[k])
continue
}
for _, f := range fileList {
newCfg := *cfg
// 是否排除下载
if utils.IsExcludeFile(f.Path, &newCfg.ExcludeNames) {
fmt.Printf("排除文件: %s\n", f.Path)
continue
}
// 匹配的文件
unit := pandownload.DownloadTaskUnit{
Cfg: &newCfg, // 复制一份新的cfg
PanClient: panClient,
VerbosePrinter: panCommandVerbose,
PrintFormat: downloadPrintFormat(options.Load),
ParentTaskExecutor: &executor,
DownloadStatistic: statistic,
IsPrintStatus: options.IsPrintStatus,
IsExecutedPermission: options.IsExecutedPermission,
IsOverwrite: options.IsOverwrite,
NoCheck: options.NoCheck,
FilePanPath: f.Path,
DriveId: options.DriveId,
GlobalSpeedsStat: globalSpeedsStat,
FileRecorder: fileRecorder,
}
// 设置储存的路径
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()
}
}
+132
View File
@@ -0,0 +1,132 @@
// 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 {
tb.Append([]string{strconv.Itoa(k + 1), info.DriveId, info.DriveName})
}
tb.Render()
return driveList, builder.String()
}
+150
View File
@@ -0,0 +1,150 @@
// Copyright (c) 2020 tickstep.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"log"
"os"
"path"
"strconv"
"time"
)
func CmdExport() cli.Command {
return cli.Command{
Name: "export",
Usage: "导出文件/目录元数据",
UsageText: cmder.App().Name + " export <网盘文件/目录的路径1> <文件/目录2> <文件/目录3> ... <本地保存文件路径>",
Description: `
导出指定文件/目录下面的所有文件的元数据信息,并保存到指定的本地文件里面。导出的文件元信息可以使用 import 命令(秒传文件功能)导入到网盘中。
支持多个文件或目录的导出.
示例:
导出 /我的资源/1.mp4 元数据到文件 /Users/tickstep/Downloads/export_files.txt
aliyunpan export /我的资源/1.mp4 /Users/tickstep/Downloads/export_files.txt
导出 /我的资源 整个目录 元数据到文件 /Users/tickstep/Downloads/export_files.txt
aliyunpan export /我的资源 /Users/tickstep/Downloads/export_files.txt
导出 网盘 整个目录 元数据到文件 /Users/tickstep/Downloads/export_files.txt
aliyunpan export / /Users/tickstep/Downloads/export_files.txt
`,
Category: "阿里云盘",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() < 2 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
subArgs := c.Args()
RunExportFiles(parseDriveId(c), c.Bool("ow"), subArgs[:len(subArgs)-1], subArgs[len(subArgs)-1])
return nil
},
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ow",
Usage: "overwrite, 覆盖已存在的导出文件",
},
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
},
}
}
func RunExportFiles(driveId string, overwrite bool, panPaths []string, saveLocalFilePath string) {
activeUser := config.Config.ActiveUser()
panClient := activeUser.PanClient()
lfi, _ := os.Stat(saveLocalFilePath)
realSaveFilePath := saveLocalFilePath
if lfi != nil {
if lfi.IsDir() {
realSaveFilePath = path.Join(saveLocalFilePath, "export_file_") + strconv.FormatInt(time.Now().Unix(), 10) + ".txt"
} else {
if !overwrite {
fmt.Println("导出文件已存在")
return
}
}
} else {
// create file
localDir := path.Dir(saveLocalFilePath)
dirFs, _ := os.Stat(localDir)
if dirFs != nil {
if !dirFs.IsDir() {
fmt.Println("指定的保存文件路径不合法")
return
}
} else {
er := os.MkdirAll(localDir, 0755)
if er != nil {
fmt.Println("创建本地文件夹出错")
return
}
}
realSaveFilePath = saveLocalFilePath
}
totalCount := 0
saveFile, err := os.OpenFile(realSaveFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
log.Fatal(err)
return
}
for _, panPath := range panPaths {
panPath = activeUser.PathJoin(driveId, panPath)
panClient.FilesDirectoriesRecurseList(driveId, panPath, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
if apiError != nil {
logger.Verbosef("%s\n", apiError)
return true
}
// 只需要存储文件即可
if !fd.IsFolder() {
item := newRapidUploadItemFromFileEntity(fd)
jstr := item.createRapidUploadLink(false)
if len(jstr) <= 0 {
logger.Verboseln("create rapid upload link err")
return false
}
saveFile.WriteString(jstr + "\n")
totalCount += 1
time.Sleep(time.Duration(100) * time.Millisecond)
fmt.Printf("\r导出文件数量: %d", totalCount)
}
return true
})
}
// close and save
if err := saveFile.Close(); err != nil {
log.Fatal(err)
}
fmt.Printf("\r导出文件总数量: %d\n", totalCount)
fmt.Printf("导出文件保存路径: %s\n", realSaveFilePath)
}
+277
View File
@@ -0,0 +1,277 @@
// Copyright (c) 2020 tickstep.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"strings"
"time"
)
type (
dirFileListData struct {
Dir *aliyunpan.MkdirResult
FileList aliyunpan.FileList
}
)
const (
DefaultSaveToPanPath = "/aliyunpan"
)
func CmdImport() cli.Command {
return cli.Command{
Name: "import",
Usage: "导入文件",
UsageText: cmder.App().Name + " export <本地元数据文件路径>",
Description: `
导入文件中记录的元数据文件到网盘。保存到网盘的文件会使用文件元数据记录的路径位置,如果没有指定云盘目录(saveto)则默认导入到目录 aliyunpan 中。
导入的文件可以使用 export 命令获得。
导入文件每一行是一个文件元数据,样例如下:
aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder
示例:
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项
aliyunpan import /Users/tickstep/Downloads/export_files.txt
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项并保存到目录 /my2021 中
aliyunpan import -saveto=/my2021 /Users/tickstep/Downloads/export_files.txt
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项并保存到网盘根目录 / 中
aliyunpan import -saveto=/ /Users/tickstep/Downloads/export_files.txt
`,
Category: "阿里云盘",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
saveTo := ""
if c.String("saveto") != "" {
saveTo = filepath.Clean(c.String("saveto"))
}
subArgs := c.Args()
RunImportFiles(parseDriveId(c), c.Bool("ow"), saveTo, subArgs[0])
return nil
},
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ow",
Usage: "overwrite, 覆盖已存在的网盘文件",
},
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
cli.StringFlag{
Name: "saveto",
Usage: "将文件保存到指定的目录",
},
},
}
}
func RunImportFiles(driveId string, overwrite bool, panSavePath, localFilePath string) {
lfi, _ := os.Stat(localFilePath)
if lfi != nil {
if lfi.IsDir() {
fmt.Println("请指定导入文件")
return
}
} else {
// create file
fmt.Println("导入文件不存在")
return
}
if panSavePath == "" {
// use default
panSavePath = DefaultSaveToPanPath
}
fmt.Println("导入的文件会存储到目录:" + panSavePath)
importFile, err := os.OpenFile(localFilePath, os.O_RDONLY, 0755)
if err != nil {
log.Fatal(err)
return
}
defer importFile.Close()
fileData, err := ioutil.ReadAll(importFile)
if err != nil {
fmt.Println("读取文件出错")
return
}
fileText := string(fileData)
if len(fileText) == 0 {
fmt.Println("文件为空")
return
}
fileText = strings.TrimSpace(fileText)
fileLines := strings.Split(fileText, "\n")
importFileItems := []RapidUploadItem{}
for _, line := range fileLines {
line = strings.TrimSpace(line)
if item, e := newRapidUploadItem(line); e != nil {
fmt.Println(e)
continue
} else {
item.FilePath = strings.ReplaceAll(path.Join(panSavePath, item.FilePath), "\\", "/")
importFileItems = append(importFileItems, *item)
}
}
if len(importFileItems) == 0 {
fmt.Println("没有可以导入的文件项目")
return
}
fmt.Println("正在准备导入...")
dirMap := prepareMkdir(driveId, importFileItems)
fmt.Println("正在导入...")
successImportFiles := []RapidUploadItem{}
failedImportFiles := []RapidUploadItem{}
for _, item := range importFileItems {
fmt.Printf("正在处理导入: %s\n", item.FilePath)
result, abort := processOneImport(driveId, overwrite, dirMap, item)
if abort {
fmt.Println("导入任务终止了")
break
}
if result {
successImportFiles = append(successImportFiles, item)
} else {
failedImportFiles = append(failedImportFiles, item)
}
time.Sleep(time.Duration(200) * time.Millisecond)
}
if len(failedImportFiles) > 0 {
fmt.Println("\n以下文件导入失败")
for _, f := range failedImportFiles {
fmt.Printf("%s %s\n", f.FileSha1, f.FilePath)
}
fmt.Println("")
}
fmt.Printf("导入结果, 成功 %d, 失败 %d\n", len(successImportFiles), len(failedImportFiles))
}
func processOneImport(driveId string, isOverwrite bool, dirMap map[string]*dirFileListData, item RapidUploadItem) (result, abort bool) {
panClient := config.Config.ActiveUser().PanClient()
panDir, fileName := path.Split(item.FilePath)
dataItem := dirMap[path.Dir(panDir)]
if isOverwrite {
// 标记覆盖旧同名文件
// 检查同名文件是否存在
var efi *aliyunpan.FileEntity = nil
for _, fileItem := range dataItem.FileList {
if !fileItem.IsFolder() && fileItem.FileName == fileName {
efi = fileItem
break
}
}
if efi != nil && efi.FileId != "" {
// existed, delete it
fdr, err := panClient.FileDelete([]*aliyunpan.FileBatchActionParam{
{
DriveId: driveId,
FileId: efi.FileId,
},
})
if err != nil || fdr == nil || !fdr[0].Success {
fmt.Println("无法删除文件,请稍后重试")
return false, false
}
time.Sleep(time.Duration(500) * time.Millisecond)
fmt.Println("检测到同名文件,已移动到回收站")
}
}
appCreateUploadFileParam := &aliyunpan.CreateFileUploadParam{
DriveId: driveId,
Name: fileName,
Size: item.FileSize,
ContentHash: item.FileSha1,
ParentFileId: dataItem.Dir.FileId,
}
uploadOpEntity, apierr := panClient.CreateUploadFile(appCreateUploadFileParam)
if apierr != nil {
fmt.Println("创建秒传任务失败:" + apierr.Error())
return false, true
}
if uploadOpEntity.RapidUpload {
logger.Verboseln("秒传成功, 保存到网盘路径: ", path.Join(panDir, uploadOpEntity.FileName))
} else {
fmt.Println("失败,文件未曾上传,无法秒传")
return false, false
}
return true, false
}
func prepareMkdir(driveId string, importFileItems []RapidUploadItem) map[string]*dirFileListData {
panClient := config.Config.ActiveUser().PanClient()
resultMap := map[string]*dirFileListData{}
for _, item := range importFileItems {
var apierr *apierror.ApiError
var rs *aliyunpan.MkdirResult
panDir := path.Dir(item.FilePath)
if resultMap[panDir] != nil {
continue
}
if panDir != "/" {
rs, apierr = panClient.MkdirRecursive(driveId, "", "", 0, strings.Split(path.Clean(panDir), "/"))
if apierr != nil || rs.FileId == "" {
logger.Verboseln("创建云盘文件夹失败")
continue
}
} else {
rs = &aliyunpan.MkdirResult{}
rs.FileId = aliyunpan.DefaultRootParentFileId
}
dataItem := &dirFileListData{}
dataItem.Dir = rs
// files
param := &aliyunpan.FileListParam{}
param.DriveId = driveId
param.ParentFileId = rs.FileId
allFileInfo, err1 := panClient.FileListGetAll(param, 0)
if err1 != nil {
logger.Verboseln("获取文件信息出错")
continue
}
dataItem.FileList = allFileInfo
resultMap[panDir] = dataItem
time.Sleep(time.Duration(500) * time.Millisecond)
}
return resultMap
}
+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/library/collection"
"github.com/urfave/cli"
"io/ioutil"
"os"
"strconv"
"strings"
)
func CmdLocateUrl() cli.Command {
return cli.Command{
Name: "locate",
Usage: "获取文件下载链接",
UsageText: cmder.App().Name + " locate <文件/目录1> <文件/目录2> <文件/目录3> ...",
Description: `
获取文件下载直链,支持文件和文件夹。下载链接有效时间为4个小时。
导出的下载链接可以使用任何支持http下载的工具进行下载,如果是视频文件还可以使用支持在线流播放的视频软件进行在线播放。
注意:由于阿里云盘网页端有防盗链设置所以不能直接使用网页Token登录,你必须使用手机扫码二维码登录(命令:login -QrCode),否则获取的直链无法正常下载,会提示 403 Forbidden 下载被禁止。
示例:
获取 /我的资源/1.mp4 下载直链
aliyunpan locate /我的资源/1.mp4
获取 /我的资源 目录下面所有mp4文件的下载直链,使用通配符
aliyunpan locate /我的资源/*.mp4
获取 /我的资源 目录下面所有文件的下载直链并保存到本地文件 /Volumes/Downloads/file_url.txt 中
aliyunpan locate -saveto "/Volumes/Downloads/file_url.txt" /我的资源
`,
Category: "阿里云盘",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
saveFilePath := ""
if c.IsSet("saveto") {
saveFilePath = c.String("saveto")
}
RunLocateUrl(parseDriveId(c), c.Args(), saveFilePath)
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
cli.StringFlag{
Name: "saveto",
Usage: "导出链接成文件并保存到指定的位置",
},
},
}
}
// RunLocateUrl 执行下载网盘内文件
func RunLocateUrl(driveId string, paths []string, saveFilePath string) {
useInternalUrl := config.Config.TransferUrlType == 2
activeUser := GetActiveUser()
activeUser.PanClient().EnableCache()
activeUser.PanClient().ClearCache()
defer activeUser.PanClient().DisableCache()
paths, err := makePathAbsolute(driveId, paths...)
if err != nil {
fmt.Println(err)
return
}
fileEntityQueue := collection.NewFifoQueue()
sb := &strings.Builder{}
failedList := []string{}
for _, p := range paths {
fileList, err1 := matchPathByShellPattern(driveId, p)
if err1 != nil {
failedList = append(failedList, p)
continue
}
if fileList == nil || len(fileList) == 0 {
// 文件不存在
failedList = append(failedList, p)
continue
}
for _, f := range fileList {
// 匹配的文件
fileEntityQueue.Push(f)
}
}
for {
item := fileEntityQueue.Pop()
if item == nil {
break
}
fileInfo := item.(*aliyunpan.FileEntity)
if fileInfo.IsFolder() { // 文件夹,获取下面所有文件
allFilesInFolder, er := activeUser.PanClient().FileListGetAll(&aliyunpan.FileListParam{
DriveId: driveId,
ParentFileId: fileInfo.FileId,
}, 300)
if er != nil {
failedList = append(failedList, fileInfo.Path)
continue
}
for _, f := range allFilesInFolder {
f.Path = fileInfo.Path + "/" + f.FileName
if f.IsFolder() {
// for next term
fileEntityQueue.Push(f)
continue
}
durl, apierr := activeUser.PanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
DriveId: driveId,
FileId: f.FileId,
})
if apierr != nil {
failedList = append(failedList, f.Path)
continue
}
url := durl.Url
if useInternalUrl {
url = durl.InternalUrl
}
if saveFilePath != "" {
fmt.Printf("获取文件下载链接:%s\n", f.Path)
fmt.Fprintf(sb, "\n文件:%s\n%s\n", f.Path, url)
} else {
fmt.Printf("\n文件:%s\n%s\n", f.Path, url)
}
}
} else { // 文件
durl, apierr := activeUser.PanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
DriveId: driveId,
FileId: fileInfo.FileId,
})
if apierr != nil {
failedList = append(failedList, fileInfo.Path)
continue
}
url := durl.Url
if useInternalUrl {
url = durl.InternalUrl
}
if saveFilePath != "" {
fmt.Printf("获取文件下载链接:%s\n", fileInfo.Path)
fmt.Fprintf(sb, "\n文件:%s\n%s\n", fileInfo.Path, url)
} else {
fmt.Printf("\n文件:%s\n%s\n", fileInfo.Path, url)
}
}
}
if saveFilePath != "" {
// save file
if e := ioutil.WriteFile(saveFilePath, []byte(sb.String()), 0777); e == nil {
fmt.Printf("保存文件成功:%s\n", saveFilePath)
} else {
fmt.Printf("保存文件失败:%s\n", saveFilePath)
}
}
// 输出失败的文件列表
if len(failedList) > 0 {
pnt := func() {
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "文件/目录"})
for k, f := range failedList {
tb.Append([]string{strconv.Itoa(k + 1), f})
}
tb.Render()
}
fmt.Printf("\n\n以下文件获取直链失败: \n")
pnt()
}
}
+193
View File
@@ -0,0 +1,193 @@
// 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/cmdliner"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
"github.com/tickstep/library-go/logger"
_ "github.com/tickstep/library-go/requester"
"github.com/urfave/cli"
"time"
)
func CmdLogin() cli.Command {
return cli.Command{
Name: "login",
Usage: "登录阿里云盘账号",
Description: `
示例:
1.常规登录,按提示一步一步来即可
aliyunpan login
2.直接指定RefreshToken进行登录
aliyunpan login -RefreshToken=8B12CBBCE89CA8DFC3445985B63B511B5E7EC7...
3.使用二维码方式进行登录
aliyunpan login -QrCode
`,
Category: "阿里云盘账号",
Before: ReloadConfigFunc, // 每次进行登录动作的时候需要调用刷新配置
After: SaveConfigFunc, // 登录完成需要调用保存配置
Action: func(c *cli.Context) error {
refreshTokenStr := ""
if refreshTokenStr == "" {
refreshTokenStr = c.String("RefreshToken")
}
useQrCode := c.Bool("QrCode")
tokenId := ""
webToken := aliyunpan.WebLoginToken{}
refreshToken := ""
var err error
tokenId, refreshToken, webToken, err = RunLogin(useQrCode, refreshTokenStr)
if err != nil {
fmt.Println(err)
return err
}
cloudUser, err := config.SetupUserByCookie(&webToken, config.Config.DeviceId, config.Config.DeviceName)
if cloudUser == nil {
fmt.Println("登录失败: ", err)
return nil
}
cloudUser.RefreshToken = refreshToken
cloudUser.TokenId = tokenId
config.Config.SetActiveUser(cloudUser)
fmt.Println("阿里云盘登录成功: ", cloudUser.Nickname)
return nil
},
// 命令的附加options参数说明,使用 help panlogin 命令即可查看
Flags: []cli.Flag{
// aliyunpan panlogin -RefreshToken=8B12CBBCE89CA8DFC3445985B63B511B5E7EC7...
cli.StringFlag{
Name: "RefreshToken",
Usage: "使用RefreshToken Cookie来登录帐号",
},
cli.BoolFlag{
Name: "QrCode",
Usage: "使用二维码登录",
},
},
}
}
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().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(useQrCodeLogin bool, refreshToken string) (tokenId, refreshTokenStr string, webToken aliyunpan.WebLoginToken, error error) {
if useQrCodeLogin {
h := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
qrCodeUrlResult, err := h.GetQRCodeLoginUrl("")
if err != nil {
fmt.Println("二维码登录错误:", err)
return "", "", aliyunpan.WebLoginToken{}, err
}
fmt.Printf("请在浏览器打开以下链接进行扫码登录,链接有效时间为5分钟\n%s\n\n", qrCodeUrlResult.TokenUrl+"&deviceId="+config.Config.DeviceId)
// handler waiting
line := cmdliner.NewLiner()
var qrCodeLoginResult *panlogin.QRCodeLoginResult
queryResult := true
defer line.Close()
go func() {
for queryResult {
time.Sleep(3 * time.Second)
qr, er := h.GetQRCodeLoginResult(qrCodeUrlResult.TokenId)
if er != nil {
continue
}
logger.Verboseln(qr)
if qr.QrCodeStatus == "CONFIRMED" {
// login successfully
qrCodeLoginResult = qr
break
} else if qr.QrCodeStatus == "EXPIRED" {
break
}
}
}()
line.State.Prompt("请在浏览器里面完成扫码登录,然后再按Enter键继续...")
if qrCodeLoginResult == nil {
queryResult = false
return "", "", aliyunpan.WebLoginToken{}, fmt.Errorf("二维码登录失败")
}
tokenStr, er := h.ParseSecureRefreshToken("", qrCodeLoginResult.SecureRefreshToken)
if er != nil {
fmt.Println("解析Token错误:", er)
return "", "", aliyunpan.WebLoginToken{}, er
}
refreshToken = tokenStr
tokenId = qrCodeUrlResult.TokenId
}
refreshTokenStr, webToken, error = DoLoginHelper(refreshToken)
return
}
+239
View File
@@ -0,0 +1,239 @@
// 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"
"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,
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)
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().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().Mkdir(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)
}
}
+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-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{}
moveFileParamList := []*aliyunpan.FileMoveParam{}
fileId2FileEntity := map[string]*aliyunpan.FileEntity{}
for _, mfi := range opFileList {
fileId2FileEntity[mfi.FileId] = mfi
moveFileParamList = append(moveFileParamList,
&aliyunpan.FileMoveParam{
DriveId: driveId,
FileId: mfi.FileId,
ToDriveId: driveId,
ToParentFileId: targetFile.FileId,
})
cacheCleanPaths = append(cacheCleanPaths, path.Dir(mfi.Path))
}
fmr, er := activeUser.PanClient().FileMove(moveFileParamList)
for _, rs := range fmr {
if !rs.Success {
failedMoveFiles = append(failedMoveFiles, fileId2FileEntity[rs.FileId])
}
}
if len(failedMoveFiles) > 0 {
fmt.Println("以下文件移动失败:")
for _, f := range failedMoveFiles {
fmt.Println(f.FileName)
}
fmt.Println("")
}
if er == nil {
pnt := func() {
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "文件/目录"})
for k, rs := range fmr {
tb.Append([]string{strconv.Itoa(k + 1), fileId2FileEntity[rs.FileId].Path})
}
tb.Render()
}
fmt.Println("操作成功, 以下文件已移动到目标目录: ", targetFile.Path)
pnt()
activeUser.DeleteCache(cacheCleanPaths)
} else {
fmt.Println("无法移动文件,请稍后重试")
}
}
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().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().GetUserInfo()
if err != nil {
return nil, err
}
return &QuotaInfo{
UsedSize: int64(user.UsedSize),
Quota: int64(user.TotalSize),
}, nil
}
+248
View File
@@ -0,0 +1,248 @@
// 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"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"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 {
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 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 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.RecycleBinFileListGetAll(&aliyunpan.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.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.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.RecycleBinFileClear(&aliyunpan.RecycleBinFileClearParam{
DriveId: driveId,
})
if err != nil {
logger.Verboseln(err)
fmt.Printf("当前无法清空回收站,请稍后重试\n")
}
for i := 0; i < 10; i++ {
ar, err1 := panClient.AsyncTaskQueryStatus(&aliyunpan.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().FileInfoByPath(driveId, activeUser.PathJoin(driveId, oldName))
if err != nil {
fmt.Printf("原文件不存在: %s, %s\n", oldName, err)
return
}
fileId = r.FileId
b, e := activeUser.PanClient().FileRename(driveId, fileId, path.Base(newName))
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().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))
}
+138
View File
@@ -0,0 +1,138 @@
// 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/urfave/cli"
"os"
"path"
"strconv"
)
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: "",
},
},
}
}
// RunRemove 执行 批量删除文件/目录
func RunRemove(driveId string, paths ...string) {
activeUser := GetActiveUser()
cacheCleanDirs := []string{}
failedRmPaths := make([]string, 0, len(paths))
delFileInfos := []*aliyunpan.FileBatchActionParam{}
fileId2FileEntity := map[string]*aliyunpan.FileEntity{}
for _, p := range paths {
absolutePath := path.Clean(activeUser.PathJoin(driveId, p))
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
if err1 != nil {
failedRmPaths = append(failedRmPaths, absolutePath)
continue
}
if fileList == nil || len(fileList) == 0 {
// 失败,文件不存在
failedRmPaths = append(failedRmPaths, absolutePath)
continue
}
for _, f := range fileList {
// 删除匹配的文件
delFileInfos = append(delFileInfos, &aliyunpan.FileBatchActionParam{
DriveId: driveId,
FileId: f.FileId,
})
fileId2FileEntity[f.FileId] = f
cacheCleanDirs = append(cacheCleanDirs, path.Dir(f.Path))
}
}
// delete
successDelFileEntity := []*aliyunpan.FileEntity{}
fdr, err := activeUser.PanClient().FileDelete(delFileInfos)
if fdr != nil {
for _, item := range fdr {
if !item.Success {
failedRmPaths = append(failedRmPaths, fileId2FileEntity[item.FileId].Path)
} else {
successDelFileEntity = append(successDelFileEntity, fileId2FileEntity[item.FileId])
}
}
}
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)
}
if len(successDelFileEntity) == 0 && err != nil {
fmt.Println("无法删除文件,请稍后重试")
return
}
}
+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"))
}
+493
View File
@@ -0,0 +1,493 @@
// 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/tickstep/library-go/logger"
"github.com/urfave/cli"
"os"
"path"
"path/filepath"
"strconv"
"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 1.mp4
创建 /我的视频/ 目录下所有mp4文件的分享链接,支持通配符
aliyunpan share set /我的视频/*.mp4
创建文件 1.mp4 的分享链接,并指定分享密码为2333
aliyunpan share set -sharePwd 2333 1.mp4
创建文件 1.mp4 的分享链接,并指定有效期为1天
aliyunpan share set -time 1 1.mp4
创建文件 1.mp4 的快传链接
aliyunpan share set -mode 3 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")
}
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 {
RunShareList()
return nil
},
Flags: []cli.Flag{},
},
{
Name: "cancel",
Aliases: []string{"c"},
Usage: "取消分享文件/目录",
UsageText: cmder.App().Name + " share cancel <shareid_1> <shareid_2> ...",
Description: `目前只支持通过分享id (shareid) 来取消分享.`,
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
RunShareCancel(c.Args())
return nil
},
},
{
Name: "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 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",
},
},
},
// {
// Name: "mc",
// Aliases: []string{},
// Usage: "创建秒传链接",
// UsageText: cmder.App().Name + " share mc <文件/目录1> <文件/目录2> ...",
// Description: `
//创建文件秒传链接,秒传链接只能是文件,如果是文件夹则会创建文件夹包含的所有文件的秒传链接。秒传链接可以通过RapidUpload命令或者Import命令进行导入到自己的网盘。
//示例:
// 创建文件 1.mp4 的秒传链接
// aliyunpan share mc 1.mp4
//
// 创建文件 1.mp4 的秒传链接,但链接隐藏相对路径
// aliyunpan share mc -hp 1.mp4
//
// 创建文件夹 share_folder 下面所有文件的秒传链接
// aliyunpan share mc share_folder/
//`,
// Action: func(c *cli.Context) error {
// if c.NArg() < 1 {
// cli.ShowCommandHelp(c, c.Command.Name)
// return nil
// }
// if config.Config.ActiveUser() == nil {
// fmt.Println("未登录账号")
// return nil
// }
// hp := false
// if c.IsSet("hp") {
// hp = c.Bool("hp")
// }
// RunShareMc(parseDriveId(c), hp, c.Args())
// return nil
// },
// Flags: []cli.Flag{
// cli.StringFlag{
// Name: "driveId",
// Usage: "网盘ID",
// Value: "",
// },
// cli.BoolFlag{
// Name: "hp",
// Usage: "hide path, 隐藏相对目录",
// },
// },
// },
},
}
}
// RunShareSet 执行分享
func RunShareSet(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.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.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().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().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().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
}
// 创建秒传链接
func RunShareMc(driveId string, hideRelativePath bool, panPaths []string) {
activeUser := config.Config.ActiveUser()
panClient := activeUser.PanClient()
totalCount := 0
for _, panPath := range panPaths {
panPath = activeUser.PathJoin(driveId, panPath)
panClient.FilesDirectoriesRecurseList(driveId, panPath, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
if apiError != nil {
logger.Verbosef("%s\n", apiError)
return true
}
// 只需要文件即可
if !fd.IsFolder() {
item := newRapidUploadItemFromFileEntity(fd)
jstr := item.createRapidUploadLink(hideRelativePath)
if len(jstr) <= 0 {
logger.Verboseln("create rapid upload link err")
return false
}
// print
fmt.Println(jstr)
totalCount += 1
time.Sleep(time.Duration(100) * time.Millisecond)
}
return true
})
}
fmt.Printf("\n秒传链接总数量: %d\n", totalCount)
}
+405
View File
@@ -0,0 +1,405 @@
// 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/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"
"sync/atomic"
"time"
)
func CmdSync() cli.Command {
return cli.Command{
Name: "sync",
Usage: "同步备份功能(Beta)",
UsageText: cmder.App().Name + " sync",
Description: `
备份功能。支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份三种模式。支持JavaScript插件对备份文件进行过滤。
指定本地目录和对应的一个网盘目录,以备份文件。网盘目录必须和本地目录独占使用,不要用作其他用途,不然备份可能会有问题。
备份功能支持以下三种模式:
1. upload
备份本地文件,即上传本地文件到网盘,始终保持本地文件有一个完整的备份在网盘
2. download
备份云盘文件,即下载网盘文件到本地,始终保持网盘的文件有一个完整的备份在本地
3. sync(慎用!!!双向备份过程会删除文件)
双向备份,保持网盘文件和本地文件严格一致
请输入以下命令查看如何配置和启动:
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",
"priority": "time"
}
]
}
相关字段说明如下:
name - 任务名称
localFolderPath - 本地目录
panFolderPath - 网盘目录
mode - 模式,支持三种: upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)
priority - 优先级,只对双向同步备份模式有效。选项支持三种: time-时间优先,local-本地优先,pan-网盘优先
例子:
1. 查看帮助
aliyunpan sync start -h
2. 使用命令行配置启动同步备份服务,将本地目录 D:\tickstep\Documents\设计文档 中的文件备份上传到云盘目录 /sync_drive/我的文档
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "upload"
3. 使用命令行配置启动同步备份服务,将云盘目录 /sync_drive/我的文档 中的文件备份下载到本地目录 D:\tickstep\Documents\设计文档
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "download"
4. 使用命令行配置启动同步备份服务,将云盘目录 /sync_drive/我的文档 和本地目录 D:\tickstep\Documents\设计文档 的文件进行双向同步
同时配置同步优先选项为本地文件优先,并显示同步过程的日志
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "sync" -pri "local" -log true
5. 使用命令行配置启动同步备份服务,将本地目录 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
6. 使用配置文件启动同步备份服务,使用配置文件可以支持同时启动多个备份任务。配置文件必须存在,否则启动失败。
aliyunpan sync start
7. 使用配置文件启动同步备份服务,并配置下载并发为2,上传并发为1,下载分片大小为256KB,上传分片大小为1MB
aliyunpan sync start -dp 2 -up 1 -dbs 256 -ubs 1024
8. 当你本地同步目录文件非常多,或者云盘同步目录文件非常多,为了后期更快更精准同步文件,可以先进行文件扫描并构建同步数据库,然后再正常启动同步任务。如下所示:
aliyunpan sync start -step scan
aliyunpan sync start
`,
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
}
opt := c.String("pri")
var syncOpt syncdrive.SyncPriorityOption = syncdrive.SyncPriorityTimestampFirst
if opt == "local" {
syncOpt = syncdrive.SyncPriorityLocalFirst
} else if opt == "pan" {
syncOpt = syncdrive.SyncPriorityPanFirst
} else {
syncOpt = syncdrive.SyncPriorityTimestampFirst
}
// 任务类型
step := syncdrive.StepSyncFile
stepVar := c.String("step")
if stepVar == "scan" {
step = syncdrive.StepScanFile
}
var task *syncdrive.SyncTask
localDir := c.String("ldir")
panDir := c.String("pdir")
mode := c.String("mode")
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.UploadOnly
if mode == string(syncdrive.UploadOnly) {
task.Mode = syncdrive.UploadOnly
} else if mode == string(syncdrive.DownloadOnly) {
task.Mode = syncdrive.DownloadOnly
} else if mode == string(syncdrive.SyncTwoWay) {
task.Mode = syncdrive.SyncTwoWay
} else {
task.Mode = syncdrive.UploadOnly
}
task.Name = path.Base(task.LocalFolderPath)
task.Id = utils.Md5Str(task.LocalFolderPath)
task.Priority = syncOpt
task.UserId = activeUser.UserId
}
// 获取同步文件锁,保证同步操作单实例
//locker := filelocker.NewFileLocker(config.GetLockerDir() + "/aliyunpan-sync")
//if e := filelocker.LockFile(locker, 0755, true, 5*time.Second); e != nil {
// logger.Verboseln(e)
// fmt.Println("本应用其他实例正在执行同步,请先停止或者等待其完成")
// return nil
//}
RunSync(task, dp, up, downloadBlockSize, uploadBlockSize, syncOpt, c.Int("ldt"), step)
// 释放文件锁
//if locker != nil {
// filelocker.UnlockFile(locker)
//}
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "ldir",
Usage: "local dir, 本地文件夹完整路径",
},
cli.StringFlag{
Name: "pdir",
Usage: "pan dir, 云盘文件夹完整路径",
},
cli.StringFlag{
Name: "mode",
Usage: "备份模式, 支持三种: upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)",
Value: "upload",
},
cli.StringFlag{
Name: "pri",
Usage: "优先级priority,只对双向同步备份模式有效。当网盘和本地存在同名文件,优先使用哪个,选项支持三种: time-时间优先,local-本地优先,pan-网盘优先",
Value: "time",
},
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.StringFlag{
Name: "step",
Usage: "task step 任务步骤, 支持两种: scan(只扫描并建立同步数据库),sync(正常启动同步任务)",
Value: "sync",
},
},
},
},
}
}
func RunSync(defaultTask *syncdrive.SyncTask, fileDownloadParallel, fileUploadParallel int, downloadBlockSize, uploadBlockSize int64,
flag syncdrive.SyncPriorityOption, localDelayTime int, taskStep syncdrive.TaskStep) {
useInternalUrl := config.Config.TransferUrlType == 2
maxDownloadRate := config.Config.MaxDownloadRate
maxUploadRate := config.Config.MaxUploadRate
activeUser := GetActiveUser()
panClient := activeUser.PanClient()
panClient.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 RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
logger.Verboseln("update access token for sync task")
panClient.UpdateToken(activeUser.WebToken)
}
}
}(&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("启动同步备份进程")
typeUrlStr := "默认链接"
if useInternalUrl {
typeUrlStr = "阿里ECS内部链接"
}
// 文件同步记录器
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/sync_file_records.csv")
option := syncdrive.SyncOption{
FileDownloadParallel: fileDownloadParallel,
FileUploadParallel: fileUploadParallel,
FileDownloadBlockSize: downloadBlockSize,
FileUploadBlockSize: uploadBlockSize,
UseInternalUrl: useInternalUrl,
MaxDownloadRate: maxDownloadRate,
MaxUploadRate: maxUploadRate,
SyncPriority: flag,
LocalFileModifiedCheckIntervalSec: localDelayTime,
FileRecorder: fileRecorder,
}
syncMgr := syncdrive.NewSyncTaskManager(activeUser, activeUser.DriveList.GetFileDriveId(), panClient, syncFolderRootPath, option)
syncConfigFile := syncMgr.ConfigFilePath()
if tasks != nil {
syncConfigFile = "(使用命令行配置)"
}
fmt.Printf("备份配置文件:%s\n链接类型:%s\n下载并发:%d\n上传并发:%d\n下载分片大小:%s\n上传分片大小:%s\n",
syncConfigFile, typeUrlStr, fileDownloadParallel, fileUploadParallel, converter.ConvertFileSize(downloadBlockSize, 2),
converter.ConvertFileSize(uploadBlockSize, 2))
if _, e := syncMgr.Start(tasks, taskStep); e != nil {
fmt.Println("启动任务失败:", e)
return
}
if taskStep != syncdrive.StepScanFile {
_, ok := os.LookupEnv("ALIYUNPAN_DOCKER")
if ok {
// in docker container
// 使用休眠以节省CPU资源
fmt.Println("本命令不会退出,程序正在以Docker的方式运行。如需退出请借助Docker提供的方式。")
for {
time.Sleep(60 * time.Second)
}
} else {
if config.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)
}
}
}
fmt.Println("正在停止同步备份任务,请稍等...")
}
// stop task
syncMgr.Stop(taskStep)
if taskStep == syncdrive.StepScanFile {
fmt.Println("\n已完成文件扫描和同步数据库的构建,可以启动任务同步了")
}
}
+129
View File
@@ -0,0 +1,129 @@
// 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/plugins"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
)
func CmdToken() cli.Command {
return cli.Command{
Name: "token",
Usage: "Token相关操作",
UsageText: cmder.App().Name + " token",
Category: "阿里云盘账号",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
},
Subcommands: []cli.Command{
{
Name: "update",
Usage: "更新Token",
UsageText: cmder.App().Name + " token update",
Description: `
示例:
更新当前登录用户的Token
aliyunpan token update -mode 1
更新全部登录用户的Token
aliyunpan token update -mode 2
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号,无需刷新Token")
return nil
}
modeFlag := "1"
if c.IsSet("mode") {
modeFlag = c.String("mode")
}
RunTokenUpdate(modeFlag)
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "mode",
Usage: "模式,1-登录用户,2-全部用户",
Value: "1",
},
},
},
},
}
}
// RunTokenUpdate 执行Token更新
func RunTokenUpdate(modeFlag string) {
ReloadConfigFunc(nil)
// 获取当前插件
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
plugin, _ := pluginManger.GetPlugin()
params := &plugins.UserTokenRefreshFinishParams{
Result: "success",
Message: "",
OldToken: "",
NewToken: "",
UpdatedAt: utils.NowTimeStr(),
}
userList := config.Config.UserList
if userList == nil || len(userList) == 0 {
fmt.Printf("没有登录用户,无需刷新Token\n")
return
}
for _, user := range userList {
params.Result = "success"
if modeFlag == "1" {
if user.UserId != config.Config.ActiveUID {
continue
}
}
newToken, e := aliyunpan.GetAccessTokenFromRefreshToken(user.RefreshToken)
if e != nil {
params.Result = "fail"
params.Message = e.Error()
fmt.Printf("无法为%s用户获取新的RefreshToken,可能需要重新登录\n", user.Nickname)
continue
}
if newToken != nil && newToken.RefreshToken != "" {
params.OldToken = user.RefreshToken
params.NewToken = newToken.RefreshToken
user.RefreshToken = newToken.RefreshToken
user.WebToken = *newToken
fmt.Printf("成功刷新%s用户的RefreshToken\n", user.Nickname)
} else {
params.Result = "fail"
}
// plugin callback
if er := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(user), params); er != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er.Error())
}
}
SaveConfigFunc(nil)
}
+234
View File
@@ -0,0 +1,234 @@
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)
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().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.Path, 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().ClearCache()
activeUser.PanClient().EnableCache()
defer activeUser.PanClient().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))
}
+618
View File
@@ -0,0 +1,618 @@
// 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/apierror"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/requester/rio/speeds"
"os"
"path"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"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 // 分片大小
UseInternalUrl bool // 是否使用内置链接
}
)
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,
},
}
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),
})
// 释放文件锁
//if locker != nil {
// filelocker.UnlockFile(locker)
//}
return nil
},
Flags: UploadFlags,
}
}
func CmdRapidUpload() cli.Command {
return cli.Command{
Name: "rapidupload",
Aliases: []string{"ru"},
Usage: "手动秒传文件",
UsageText: cmder.App().Name + " rapidupload \"aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder\"",
Description: `
使用此功能秒传文件, 前提是知道文件的大小, sha1, 且网盘中存在一模一样的文件.
上传的文件将会保存到网盘的目标目录。文件的秒传链接可以通过share或者export命令获取。
链接格式说明:aliyunpan://文件名|sha1|文件大小|<相对路径>
"相对路径" 可以为空,为空代表存储到网盘根目录
示例:
1. 如果秒传成功, 则保存到网盘路径 /pan_folder/file.dmg
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder"
2. 如果秒传成功, 则保存到网盘路径 /file.dmg
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|"
3. 同时秒传多个文件,如果秒传成功, 则保存到网盘路径 /pan_folder/file.dmg, /pan_folder/file1.dmg
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder" "aliyunpan://file1.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder"
`,
Category: "阿里云盘",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() <= 0 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
subArgs := c.Args()
RunRapidUpload(parseDriveId(c), c.Bool("ow"), subArgs, c.String("path"))
return nil
},
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ow",
Usage: "overwrite, 覆盖已存在的文件。已存在的文件会并移到回收站",
},
cli.StringFlag{
Name: "path",
Usage: "存储到网盘目录,绝对路径,例如:/myfolder",
Value: "",
},
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
},
}
}
// RunUpload 执行文件上传
func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
activeUser := GetActiveUser()
activeUser.PanClient().EnableCache()
activeUser.PanClient().ClearCache()
defer activeUser.PanClient().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 RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
logger.Verboseln("update access token for upload task")
}
}
}(&continueFlag)
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
}
opt.UseInternalUrl = config.Config.TransferUrlType == 2
// 超时时间
if opt.MaxTimeoutSec > 0 {
activeUser.PanClient().SetTimeout(time.Duration(opt.MaxTimeoutSec) * time.Second)
}
fmt.Printf("\n[0] 当前文件上传最大并发量为: %d, 上传分片大小为: %s\n", opt.AllParallel, converter.ConvertFileSize(opt.BlockSize, 2))
savePath = activeUser.PathJoin(opt.DriveId, savePath)
_, err1 := activeUser.PanClient().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{}
// 获取当前插件
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) {
fmt.Printf("排除文件: %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) {
fmt.Printf("排除文件: %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
fmt.Printf("插件禁止该文件上传: %s\n", file.LogicPath)
return filepath.SkipDir
}
if uploadFilePrepareResult.DriveFilePath != "" {
targetSavePanRelativePath := strings.TrimPrefix(uploadFilePrepareResult.DriveFilePath, "/")
subSavePath = path.Clean(savePath + aliyunpan.PathSeparator + targetSavePanRelativePath)
fmt.Printf("插件修改文件网盘保存路径为: %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,
UseInternalUrl: opt.UseInternalUrl,
GlobalSpeedsStat: globalSpeedsStat,
FileRecorder: fileRecorder,
}, opt.MaxRetry)
fmt.Printf("[%s] 加入上传队列: %s\n", taskinfo.Id(), file.LogicPath)
} else {
// 创建文件夹
// 这样空文件夹也可以正确上传
saveFilePath := subSavePath
if saveFilePath != "/" {
folderCreateMutex.Lock()
fmt.Printf("正在检测和创建云盘文件夹: %s\n", saveFilePath)
_, apierr1 := activeUser.PanClient().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().Mkdir(opt.DriveId, "root", saveFilePath)
if apierr != nil || rs.FileId == "" {
fmt.Printf("创建云盘文件夹失败: %s\n", saveFilePath)
}
}
folderCreateMutex.Unlock()
}
}
return nil
}
file := localfile.NewSymlinkFile(curPath)
if err = localfile.WalkAllFile(file, walkFunc); err != nil {
if err != filepath.SkipDir {
fmt.Printf("警告: 遍历错误: %s\n", err)
}
}
}
// 执行上传任务
var failedList []*lane.Deque
executor.Execute()
failed := executor.FailedDeque()
if failed.Size() > 0 {
failedList = append(failedList, failed)
}
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))
}
// RunRapidUpload 秒传
func RunRapidUpload(driveId string, isOverwrite bool, fileMetaList []string, savePanPath string) {
activeUser := GetActiveUser()
savePanPath = activeUser.PathJoin(driveId, savePanPath)
if len(fileMetaList) == 0 {
fmt.Println("秒传链接为空")
return
}
items := []*RapidUploadItem{}
// parse file meta strings
for _, fileMeta := range fileMetaList {
item, e := newRapidUploadItem(fileMeta)
if e != nil {
fmt.Println(e)
continue
}
if item == nil {
fmt.Println("秒传链接格式错误: ", fileMeta)
continue
}
// pan path
item.FilePath = path.Join(savePanPath, item.FilePath)
// append
items = append(items, item)
}
// upload one by one
for _, item := range items {
fmt.Println("准备秒传:", item.FilePath)
if ee := doRapidUpload(driveId, isOverwrite, item); ee != nil {
fmt.Println(ee)
} else {
fmt.Printf("秒传成功, 保存到网盘路径:%s\n", item.FilePath)
}
}
}
func doRapidUpload(driveId string, isOverwrite bool, item *RapidUploadItem) error {
activeUser := GetActiveUser()
panClient := activeUser.PanClient()
var apierr *apierror.ApiError
var rs *aliyunpan.MkdirResult
var appCreateUploadFileParam *aliyunpan.CreateFileUploadParam
var saveFilePath string
panDir, panFileName := path.Split(item.FilePath)
saveFilePath = item.FilePath
if panDir != "/" {
rs, apierr = panClient.MkdirRecursive(driveId, "", "", 0, strings.Split(path.Clean(panDir), "/"))
if apierr != nil || rs.FileId == "" {
return fmt.Errorf("创建云盘文件夹失败")
}
} else {
rs = &aliyunpan.MkdirResult{}
rs.FileId = aliyunpan.DefaultRootParentFileId
}
time.Sleep(time.Duration(2) * time.Second)
if isOverwrite {
// 标记覆盖旧同名文件
// 检查同名文件是否存在
efi, apierr := panClient.FileInfoByPath(driveId, saveFilePath)
if apierr != nil && apierr.Code != apierror.ApiCodeFileNotFoundCode {
return fmt.Errorf("检测同名文件失败,请稍后重试")
}
if efi != nil && efi.FileId != "" {
// existed, delete it
fileDeleteResult, err1 := panClient.FileDelete([]*aliyunpan.FileBatchActionParam{{DriveId: efi.DriveId, FileId: efi.FileId}})
if err1 != nil || len(fileDeleteResult) == 0 {
return fmt.Errorf("无法删除文件,请稍后重试")
}
time.Sleep(time.Duration(500) * time.Millisecond)
if fileDeleteResult[0].Success {
logger.Verboseln("检测到同名文件,已移动到回收站: ", saveFilePath)
} else {
return fmt.Errorf("无法删除文件,请稍后重试")
}
}
}
appCreateUploadFileParam = &aliyunpan.CreateFileUploadParam{
DriveId: driveId,
Name: panFileName,
Size: item.FileSize,
ContentHash: item.FileSha1,
ParentFileId: rs.FileId,
}
uploadOpEntity, apierr := panClient.CreateUploadFile(appCreateUploadFileParam)
if apierr != nil {
return fmt.Errorf("创建秒传任务失败:" + apierr.Error())
}
if uploadOpEntity.RapidUpload {
logger.Verboseln("秒传成功, 保存到网盘路径: ", path.Join(panDir, uploadOpEntity.FileName))
} else {
return fmt.Errorf("失败,文件未曾上传,无法秒传")
}
return nil
}
+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-api/aliyunpan"
"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 or name>
`,
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, inputData)
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
fmt.Printf("当前帐号UID: %s, 昵称: %s, 用户名: %s, 网盘:%s\n", activeUser.UserId, activeUser.Nickname, activeUser.AccountName, cloudName)
return nil
},
}
}
func RunGetUserInfo() (userInfo *aliyunpan.UserInfo, error error) {
return GetActivePanClient().GetUserInfo()
}
+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 (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder/cmdliner"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/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/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().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().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
}
// RefreshTokenInNeed 刷新refresh token
func RefreshTokenInNeed(activeUser *config.PanUser, deviceName string) bool {
if activeUser == nil {
return false
}
// refresh expired token
if activeUser.PanClient() != nil {
if len(activeUser.WebToken.RefreshToken) > 0 {
cz := time.FixedZone("CST", 8*3600) // 东8区
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", activeUser.WebToken.ExpireTime, cz)
now := time.Now()
if (expiredTime.Unix() - now.Unix()) <= (20 * 60) { // 20min
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
plugin, _ := pluginManger.GetPlugin()
params := &plugins.UserTokenRefreshFinishParams{
Result: "success",
Message: "",
OldToken: "",
NewToken: "",
UpdatedAt: utils.NowTimeStr(),
}
// need update refresh token
logger.Verboseln("access token expired, get new from refresh token")
if wt, er := aliyunpan.GetAccessTokenFromRefreshToken(activeUser.RefreshToken); er == nil {
params.Result = "success"
params.OldToken = activeUser.RefreshToken
params.NewToken = wt.RefreshToken
activeUser.RefreshToken = wt.RefreshToken
activeUser.WebToken = *wt
activeUser.PanClient().UpdateToken(*wt)
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
_, e := activeUser.PanClient().CreateSession(nil)
if e != nil {
logger.Verboseln("call CreateSession error in RefreshTokenInNeed: " + e.Error())
}
return true
} else {
// token refresh error
// if token has expired, callback plugin api for notify
if now.Unix() >= expiredTime.Unix() {
params.Result = "fail"
params.Message = er.Error()
params.OldToken = activeUser.RefreshToken
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
}
}
}
}
}
}
return false
}
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, "[")
}
// DoLoginHelper 登录助手,使用token进行登录
func DoLoginHelper(refreshToken string) (refreshTokenStr string, webToken aliyunpan.WebLoginToken, error error) {
line := cmdliner.NewLiner()
defer line.Close()
if refreshToken == "" {
refreshToken, error = line.State.Prompt("请输入RefreshToken, 回车键提交 > ")
if error != nil {
return
}
}
// app login
atoken, apperr := aliyunpan.GetAccessTokenFromRefreshToken(refreshToken)
if apperr != nil {
if apperr.Code == apierror.ApiCodeTokenExpiredCode || apperr.Code == apierror.ApiCodeRefreshTokenExpiredCode {
fmt.Println("Token过期,需要重新登录")
} else {
fmt.Println("Token登录失败:", apperr)
}
return "", webToken, fmt.Errorf("登录失败")
}
refreshTokenStr = refreshToken
return refreshTokenStr, *atoken, nil
}
// TryLogin 尝试登录,基础应用支持的各类登录方式进行尝试成功登录
func TryLogin() *config.PanUser {
// 获取当前插件
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
plugin, _ := pluginManger.GetPlugin()
params := &plugins.UserTokenRefreshFinishParams{
Result: "success",
Message: "",
OldToken: "",
NewToken: "",
UpdatedAt: utils.NowTimeStr(),
}
// can do automatically login?
for _, u := range config.Config.UserList {
if u.UserId == config.Config.ActiveUID {
// login
_, webToken, err := DoLoginHelper(u.RefreshToken)
if err != nil {
logger.Verboseln("automatically login use saved refresh token error ", err)
if u.TokenId != "" {
logger.Verboseln("try to login use tokenId")
h := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
r, e := h.GetRefreshToken(u.TokenId)
if e != nil {
logger.Verboseln("try to login use tokenId error", e)
// login fail plugin callback
params.Result = "fail"
params.OldToken = u.RefreshToken
params.NewToken = ""
if er := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(u), params); er != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er.Error())
}
break
}
refreshToken, e := h.ParseSecureRefreshToken("", r.SecureRefreshToken)
if e != nil {
logger.Verboseln("try to parse refresh token error", e)
break
}
_, webToken, err = DoLoginHelper(refreshToken)
if err != nil {
logger.Verboseln("try to use refresh token from tokenId error", e)
break
}
fmt.Println("Token重新自动登录成功")
// save new refresh token
u.RefreshToken = refreshToken
} else {
// login fail plugin callback
params.Result = "fail"
params.OldToken = u.RefreshToken
params.NewToken = ""
if er := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(u), params); er != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er.Error())
}
}
break
}
// plugin param
params.Result = "success"
params.OldToken = u.RefreshToken
params.NewToken = webToken.RefreshToken
// success login, save new token and access token
u.RefreshToken = webToken.RefreshToken
u.WebToken = webToken
// save
SaveConfigFunc(nil)
// reload
ReloadConfigFunc(nil)
// do plugin callback
if er := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(u), params); er != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er.Error())
}
return config.Config.ActiveUser()
}
}
return nil
}
+234
View File
@@ -0,0 +1,234 @@
// 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/tickstep/aliyunpan/internal/webdav"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"strings"
"sync/atomic"
"time"
)
func CmdWebdav() cli.Command {
return cli.Command{
Name: "webdav",
Usage: "在线网盘服务",
Description: "webdav在线网盘服务",
Category: "阿里云盘",
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
fmt.Print(`
本文命令可以让阿里云盘变身为webdav协议的文件服务器。这样你可以把阿里云盘挂载为Windows、Linux、Mac系统的磁盘,可以通过NAS系统做文件管理或文件同步等等。
当把阿里云盘作为webdav文件服务器进行使用的时候,上传文件是不支持秒传的,所以当你挂载为网络磁盘使用的时候,不建议在webdav挂载目录中上传、下载过大的文件,不然体验会非常差。
建议作为文档,图片等小文件的同步网盘。
请输入以下命令查看如何启动
aliyunpan webdav start -h
`)
cli.ShowCommandHelp(c, c.Command.Name)
return nil
},
Subcommands: []cli.Command{
{
Name: "start",
Usage: "启动webdav在线网盘服务",
UsageText: cmder.App().Name + " webdav start [arguments...]",
Description: `
启动webdav服务,让阿里云盘变身为webdav协议的文件服务器。这样你可以把阿里云盘挂载为Windows、Linux、Mac系统的磁盘,可以通过NAS系统做文件管理或文件同步等等。
当把阿里云盘作为webdav文件服务器进行使用的时候,上传文件是不支持秒传的,所以当你挂载为网络磁盘使用的时候,不建议在webdav挂载目录中上传、下载过大的文件,不然体验会非常差。
建议作为文档,图片等小文件的同步网盘。
例子:
1. 查看帮助
aliyunpan webdav start -h
2. 使用默认配置启动webdav服务
aliyunpan webdav start
3. 启动webdav服务,并配置IP为127.0.0.1,端口为23077,登录用户名为admin,登录密码为admin123,模式为读写,文件网盘目录 /webdav_folder 作为服务的根目录
aliyunpan webdav start -ip "127.0.0.1" -port 23077 -webdav_user "admin" -webdav_password "admin123" -webdav_mode "rw" -pan_drive "File" -pan_dir_path "/webdav_folder"
`,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号,请先登录")
return nil
}
activeUser := GetActiveUser()
// 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 {
// token刷新
time.Sleep(time.Duration(1) * time.Minute)
//time.Sleep(time.Duration(5) * time.Second)
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
logger.Verboseln("reload new access token for webdav")
}
}
}(&continueFlag)
webdavServ := &webdav.WebdavConfig{
PanDriveId: "",
PanUserId: "",
PanUser: nil,
UploadChunkSize: c.Int("bs") * 1024,
TransferUrlType: config.Config.TransferUrlType,
Address: "0.0.0.0",
Port: 23077,
Prefix: "/",
Users: []webdav.WebdavUser{{
Username: "admin",
Password: "admin",
Scope: "/",
Mode: "rw",
}},
}
// pan user
panUserId := activeUser.UserId
webdavServ.PanUserId = panUserId
webdavServ.PanUser = activeUser
// address
ip := "0.0.0.0"
if c.IsSet("ip") {
ip = c.String("ip")
}
webdavServ.Address = ip
// port
port := 23077
if c.IsSet("port") {
port = c.Int("port")
}
webdavServ.Port = port
// binding pan drive
panDriveName := "File"
panDriveNameStr := "文件"
if c.IsSet("pan_drive") {
panDriveName = c.String("pan_drive")
}
if strings.ToLower(panDriveName) == "album" {
webdavServ.PanDriveId = activeUser.DriveList.GetAlbumDriveId()
panDriveNameStr = "相册"
} else {
webdavServ.PanDriveId = activeUser.DriveList.GetFileDriveId()
panDriveNameStr = "文件"
}
// binding pan dir path
panDirPath := "/"
if c.IsSet("pan_dir_path") {
panDirPath = c.String("pan_dir_path")
}
webdavServ.Users[0].Scope = panDirPath
webdavUserName := "admin"
if c.IsSet("webdav_user") {
webdavUserName = c.String("webdav_user")
}
webdavServ.Users[0].Username = webdavUserName
webdavPassword := "admin"
if c.IsSet("webdav_password") {
webdavPassword = c.String("webdav_password")
}
webdavServ.Users[0].Password = webdavPassword
webdavMode := "rw"
if c.IsSet("webdav_mode") {
webdavMode = c.String("webdav_mode")
}
webdavServ.Users[0].Mode = webdavMode
if webdavServ.Users[0].Mode != "rw" && webdavServ.Users[0].Mode != "ro" {
webdavServ.Users[0].Mode = "rw"
}
err := config.Config.Save()
if err != nil {
fmt.Println(err)
return err
}
modeStr := "读写"
if webdavServ.Users[0].Mode == "rw" {
modeStr = "读写"
} else if webdavServ.Users[0].Mode == "ro" {
modeStr = "只读"
}
fmt.Println("----------------------------------------")
fmt.Printf("webdav网盘信息:\n链接:http://localhost:%d\n用户名:%s\n密码:%s\n网盘模式:%s\n网盘服务类型:%s\n网盘服务目录:%s\n",
webdavServ.Port, webdavServ.Users[0].Username, webdavServ.Users[0].Password, modeStr, panDriveNameStr, webdavServ.Users[0].Scope)
fmt.Println("----------------------------------------")
fmt.Println("webdav在线网盘服务运行中...")
webdavServ.StartServer()
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "ip",
Usage: "绑定的本地IP,多网卡的环境下建议指定绑定的IP。默认为0.0.0.0代表绑定全部网卡",
},
cli.IntFlag{
Name: "port",
Usage: "绑定的本地端口,默认为:23077",
Value: 23077,
},
cli.StringFlag{
Name: "webdav_user",
Usage: "Webdav登录用户名,默认为:admin",
},
cli.StringFlag{
Name: "webdav_password",
Usage: "Webdav登录密码,默认为:admin",
},
cli.StringFlag{
Name: "webdav_mode",
Usage: "Webdav模式,包括:rw-读写,ro-只读,默认为:rw",
Value: "rw",
},
cli.StringFlag{
Name: "pan_drive",
Usage: "Webdav绑定的网盘类型。File-文件 Album-相册。默认为文件网盘",
Value: "File",
},
cli.StringFlag{
Name: "pan_dir_path",
Usage: "Webdav绑定的网盘文件夹路径,默认为:/",
},
cli.IntFlag{
Name: "bs",
Usage: "block size,上传分片大小,单位KB。推荐值:1024 ~ 10240",
Value: 10240,
},
},
},
},
}
}
+178
View File
@@ -0,0 +1,178 @@
// 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 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
}
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().FileCrossDriveCopy(&aliyunpan.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().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
}
+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.FileInfoByPath(pu.ActiveDriveId, pathStr)
if apiError != nil {
return nil
}
fileListParam := &aliyunpan.FileListParam{
DriveId: pu.ActiveDriveId,
ParentFileId: fi.FileId,
}
fdl, apiError = pu.panClient.FileListGetAll(fileListParam, 100)
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")
)
+525
View File
@@ -0,0 +1,525 @@
// 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"
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/filepath"
"runtime"
"strings"
"sync"
)
const (
// 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 = 20
// 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浏览器"
)
var (
CmdConfigVerbose = logger.New("CONFIG", EnvVerbose)
configFilePath = filepath.Join(GetConfigDir(), ConfigName)
// Config 配置信息, 由外部调用
Config = NewConfig(configFilePath)
AppVersion string
// IsAppInCliMode 是否在交互模式
IsAppInCliMode = false
)
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, 即字节/每秒
TransferUrlType int `json:"transferUrlType"` // 上传/下载URL类别,1-默认,2-阿里云ECS
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浏览器
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)
}
// 设置本地网卡地址
if c.LocalAddrs != "" {
requester.SetLocalTCPAddrList(strings.Split(c.LocalAddrs, ",")...)
}
// 设置域名解析策略 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)
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
}
return nil
}
func (c *PanConfig) initDefaultConfig() {
// 设置默认的下载路径
switch runtime.GOOS {
case "windows":
c.SaveDir = cmdutil.ExecutablePathJoin("Downloads")
case "android":
// TODO: 获取完整的的下载路径
c.SaveDir = "/sdcard/Download"
default:
dataPath, ok := os.LookupEnv("HOME")
if !ok {
CmdConfigVerbose.Warn("Environment HOME not set")
c.SaveDir = cmdutil.ExecutablePathJoin("Downloads")
} else {
c.SaveDir = filepath.Join(dataPath, "Downloads")
}
}
c.ConfigVer = ConfigVersion
c.VideoFileExtensions = DefaultVideoFileExtensions
c.DeviceId = RandomDeviceId() // 生成默认客户端ID
c.DeviceName = DefaultDeviceName
c.FileRecordConfig = "1" // 默认开启
c.PreferIPType = "ipv4" // 默认优先IPv4
}
// GetConfigDir 获取配置路径
func GetConfigDir() string {
// 按照以下顺序依次获取配置目录
// 1.环境变量ALIYUNPAN_CONFIG_DIR => 2. /etc/aliyunpan/ => 3. ~/.aliyunpan/ => 4.当前程序目录
// 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. /etc/aliyunpan/
if runtime.GOOS == "linux" {
cd := "/etc/aliyunpan"
if IsFolderExist(cd) {
logger.Verboseln("use config dir: ", cd)
return cd
}
}
// 3. ~/.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
}
}
}
}
// 4.当前程序所在目录
configDir = cmdutil.ExecutablePathJoin("")
logger.Verboseln("use config dir: ", configDir)
return configDir
}
// 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.WebToken, c.DeviceId, c.DeviceName)
if err != nil {
logger.Verboseln("setup user error")
return nil
}
u.panClient = user.panClient
u.Nickname = user.Nickname
u.DriveList = user.DriveList
// check workdir valid or not
if user.IsFileDriveActive() {
fe, err1 := u.PanClient().FileInfoByPath(u.ActiveDriveId, u.Workdir)
if err1 != nil {
// default to root
u.Workdir = "/"
u.WorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
} else {
u.WorkdirFileEntity = *fe
}
} else if user.IsResourceDriveActive() {
fe, err1 := u.PanClient().FileInfoByPath(u.ActiveDriveId, u.ResourceWorkdir)
if err1 != nil {
// default to root
u.ResourceWorkdir = "/"
u.ResourceWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
} else {
u.ResourceWorkdirFileEntity = *fe
}
} else if user.IsAlbumDriveActive() {
fe, err1 := u.PanClient().FileInfoByPath(u.ActiveDriveId, u.AlbumWorkdir)
if err1 != nil {
// default to root
u.AlbumWorkdir = "/"
u.AlbumWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
} else {
u.AlbumWorkdirFileEntity = *fe
}
}
}
c.activeUser = u
return u
}
}
return &PanUser{}
}
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.WebToken = user.WebToken
u.RefreshToken = user.RefreshToken
u.TokenId = user.TokenId
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, username string) (*PanUser, error) {
for _, u := range c.UserList {
if u.UserId == uid || u.AccountName == username {
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
}
+122
View File
@@ -0,0 +1,122 @@
// 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
requester.SetLocalTCPAddrList(strings.Split(localAddrs, ",")...)
}
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 ~ 20", "最大下载并发量,即同时下载文件最大数量"},
[]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{"transfer_url_type", strconv.Itoa(c.TransferUrlType), "1-默认,2-阿里云ECS", "上传下载URL类别。除非在阿里云ECS(暂只支持经典网络)服务器中使用,不然请设置1"},
[]string{"savedir", c.SaveDir, "", "下载文件的储存目录"},
[]string{"proxy", c.Proxy, "", "设置代理, 支持 http/socks5 代理,例如: http://127.0.0.1:8888 或者 socks5://127.0.0.1:8889"},
[]string{"local_addrs", c.LocalAddrs, "", "设置本地网卡地址, 多个地址用逗号隔开,例如: 127.0.0.1,192.168.100.126"},
[]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()
}
+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 config
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/library-go/expires/cachemap"
"github.com/tickstep/library-go/logger"
"path"
"path/filepath"
)
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 ""
}
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"`
RefreshToken string `json:"refreshToken"`
WebToken aliyunpan.WebLoginToken `json:"webToken"`
TokenId string `json:"tokenId"`
panClient *aliyunpan.PanClient
cacheOpMap cachemap.CacheOpMap
}
type PanUserList []*PanUser
func SetupUserByCookie(webToken *aliyunpan.WebLoginToken, deviceId, deviceName string) (user *PanUser, err *apierror.ApiError) {
tryRefreshWebToken := true
if webToken == nil {
return nil, apierror.NewFailedApiError("web token is empty")
}
doLoginAct:
appConfig := aliyunpan.AppConfig{
AppId: "25dzX3vbYqktVxyX",
DeviceId: deviceId,
UserId: "",
Nonce: 0,
PublicKey: "",
}
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{}, appConfig, aliyunpan.SessionConfig{
DeviceName: deviceName,
ModelName: "Windows网页版",
})
u := &PanUser{
WebToken: *webToken,
panClient: panClient,
Workdir: "/",
WorkdirFileEntity: *aliyunpan.NewFileEntityForRootDir(),
}
// web api token maybe expired
userInfo, err := panClient.GetUserInfo()
if err != nil {
if err.Code == apierror.ApiCodeTokenExpiredCode && tryRefreshWebToken {
tryRefreshWebToken = false
webCookie, _ := aliyunpan.GetAccessTokenFromRefreshToken(webToken.RefreshToken)
if webCookie != nil {
webToken = webCookie
goto doLoginAct
}
}
return nil, err
}
name := "Unknown"
if userInfo != nil {
if userInfo.Nickname != "" {
name = userInfo.Nickname
}
// update user
u.UserId = userInfo.UserId
u.Nickname = name
u.AccountName = userInfo.UserName
// default file drive
if u.ActiveDriveId == "" {
u.ActiveDriveId = userInfo.FileDriveId
}
// drive list
u.DriveList = DriveInfoList{
{DriveId: userInfo.FileDriveId, DriveTag: "File", DriveName: "备份盘"},
{DriveId: userInfo.ResourceDriveId, DriveTag: "Resource", DriveName: "资源库"},
{DriveId: userInfo.AlbumDriveId, DriveTag: "Album", DriveName: "相册"},
}
} else {
// error, maybe the token has expired
return nil, apierror.NewFailedApiError("cannot get user info, the token has expired")
}
// create session
appConfig.UserId = u.UserId
panClient.UpdateAppConfig(appConfig)
r, e := panClient.CreateSession(nil)
if e != nil {
logger.Verboseln("call CreateSession error in SetupUserByCookie: " + e.Error())
}
if r != nil && !r.Result {
logger.Verboseln("上传签名秘钥失败,可能是你账号登录的设备已超最大数量")
}
return u, nil
}
func (pu *PanUser) PanClient() *aliyunpan.PanClient {
return pu.panClient
}
// 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().FileInfoById(pu.ActiveDriveId, pu.WorkdirFileEntity.FileId)
if err != nil {
logger.Verboseln("刷新工作目录信息失败")
return
}
pu.WorkdirFileEntity = *fe
} else if pu.IsAlbumDriveActive() {
fe, err := pu.PanClient().FileInfoById(pu.ActiveDriveId, pu.AlbumWorkdirFileEntity.FileId)
if err != nil {
logger.Verboseln("刷新工作目录信息失败")
return
}
pu.AlbumWorkdirFileEntity = *fe
}
}
// GetSavePath 根据提供的网盘文件路径 panpath, 返回本地储存路径,
// 返回绝对路径, 获取绝对路径出错时才返回相对路径...
func (pu *PanUser) GetSavePath(filePanPath string) string {
dirStr := filepath.Join(Config.SaveDir, fmt.Sprintf("%s", pu.UserId), filePanPath)
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"
}
+137
View File
@@ -0,0 +1,137 @@
// 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/library-go/converter"
"github.com/tickstep/library-go/crypto"
"github.com/tickstep/library-go/ids"
"github.com/tickstep/library-go/logger"
"math/rand"
"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()
}
// 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()
}
+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 (
"fmt"
"testing"
)
func TestEncryptString(t *testing.T) {
fmt.Println(EncryptString("131687xxxxx@189.cn"))
}
func TestDecryptString(t *testing.T) {
fmt.Println(DecryptString("75b3c8d21607440c0e8a70f4a4861c8669774cc69c70ce2a2c8acb815b6d5d3b"))
}
func TestRandomDeviceId(t *testing.T) {
fmt.Println(RandomDeviceId())
}
+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 downloader
import (
"github.com/tickstep/aliyunpan/library/requester/transfer"
)
const (
//CacheSize 默认的下载缓存
CacheSize = 8192
)
var (
// MinParallelSize 单个线程最小的数据量
MinParallelSize int64 = 10 * 1024 * 1024 // 10MB
// MaxParallelWorkerCount 单个文件下载最大并发线程数量
MaxParallelWorkerCount int = 3
)
//Config 下载配置
type Config struct {
Mode transfer.RangeGenMode // 下载Range分配模式
MaxParallel int // 最大下载并发量
CacheSize int // 下载缓冲
BlockSize int64 // 每个Range区块的大小, RangeGenMode 为 RangeGenMode2 时才有效
MaxRate int64 // 限制最大下载速度
InstanceStateStorageFormat InstanceStateStorageFormat // 断点续传储存类型
InstanceStatePath string // 断点续传信息路径
TryHTTP bool // 是否尝试使用 http 连接
ShowProgress bool // 是否展示下载进度条
UseInternalUrl bool // 是否使用内置链接
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
}
//NewConfig 返回默认配置
func NewConfig() *Config {
return &Config{
MaxParallel: 5,
CacheSize: CacheSize,
}
}
//Fix 修复配置信息, 使其合法
func (cfg *Config) Fix() {
fixCacheSize(&cfg.CacheSize)
if cfg.MaxParallel < 1 {
cfg.MaxParallel = 1
}
}
//Copy 拷贝新的配置
func (cfg *Config) Copy() *Config {
newCfg := *cfg
return &newCfg
}
+568
View File
@@ -0,0 +1,568 @@
// 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 downloader
import (
"context"
"errors"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder/cmdutil"
"github.com/tickstep/aliyunpan/internal/waitgroup"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/cachepool"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/prealloc"
"github.com/tickstep/library-go/requester"
"github.com/tickstep/library-go/requester/rio/speeds"
"io"
"net/http"
"strings"
"sync"
"time"
)
const (
// DefaultAcceptRanges 默认的 Accept-Ranges
DefaultAcceptRanges = "bytes"
)
type (
// Downloader 下载
Downloader struct {
onExecuteEvent requester.Event //开始下载事件
onSuccessEvent requester.Event //成功下载事件
onFailedEvent requester.Event //成功下载事件
onFinishEvent requester.Event //结束下载事件
onPauseEvent requester.Event //暂停下载事件
onResumeEvent requester.Event //恢复下载事件
onCancelEvent requester.Event //取消下载事件
onDownloadStatusEvent DownloadStatusFunc //状态处理事件
monitorCancelFunc context.CancelFunc
globalSpeedsStat *speeds.Speeds // 全局速度统计
fileInfo *aliyunpan.FileEntity // 下载的文件信息
driveId string
loadBalancerCompareFunc LoadBalancerCompareFunc // 负载均衡检测函数
durlCheckFunc DURLCheckFunc // 下载url检测函数
statusCodeBodyCheckFunc StatusCodeBodyCheckFunc
executeTime time.Time
loadBalansers []string
writer io.WriterAt
client *requester.HTTPClient
panClient *aliyunpan.PanClient
config *Config
monitor *Monitor
instanceState *InstanceState
}
// DURLCheckFunc 下载URL检测函数
DURLCheckFunc func(client *requester.HTTPClient, durl string) (contentLength int64, resp *http.Response, err error)
// StatusCodeBodyCheckFunc 响应状态码出错的检查函数
StatusCodeBodyCheckFunc func(respBody io.Reader) error
)
//NewDownloader 初始化Downloader
func NewDownloader(writer io.WriterAt, config *Config, p *aliyunpan.PanClient, globalSpeedsStat *speeds.Speeds) (der *Downloader) {
der = &Downloader{
config: config,
writer: writer,
panClient: p,
globalSpeedsStat: globalSpeedsStat,
}
return
}
//SetClient 设置http客户端
func (der *Downloader) SetFileInfo(f *aliyunpan.FileEntity) {
der.fileInfo = f
}
func (der *Downloader) SetDriveId(driveId string) {
der.driveId = driveId
}
//SetClient 设置http客户端
func (der *Downloader) SetClient(client *requester.HTTPClient) {
der.client = client
}
// SetLoadBalancerCompareFunc 设置负载均衡检测函数
func (der *Downloader) SetLoadBalancerCompareFunc(f LoadBalancerCompareFunc) {
der.loadBalancerCompareFunc = f
}
//SetStatusCodeBodyCheckFunc 设置响应状态码出错的检查函数, 当FirstCheckMethod不为HEAD时才有效
func (der *Downloader) SetStatusCodeBodyCheckFunc(f StatusCodeBodyCheckFunc) {
der.statusCodeBodyCheckFunc = f
}
func (der *Downloader) lazyInit() {
if der.config == nil {
der.config = NewConfig()
}
if der.client == nil {
der.client = requester.NewHTTPClient()
der.client.SetTimeout(20 * time.Minute)
}
if der.monitor == nil {
der.monitor = NewMonitor()
}
if der.durlCheckFunc == nil {
der.durlCheckFunc = DefaultDURLCheckFunc
}
if der.loadBalancerCompareFunc == nil {
der.loadBalancerCompareFunc = DefaultLoadBalancerCompareFunc
}
}
// SelectParallel 获取合适的 parallel
func (der *Downloader) SelectParallel(single bool, maxParallel int, totalSize int64, instanceRangeList transfer.RangeList) (parallel int) {
isRange := instanceRangeList != nil && len(instanceRangeList) > 0
if single { // 单线程下载
parallel = 1
} else if isRange {
parallel = len(instanceRangeList)
} else {
parallel = maxParallel
if int64(parallel) > totalSize/int64(MinParallelSize) {
parallel = int(totalSize/int64(MinParallelSize)) + 1
}
}
if parallel < 1 {
parallel = 1
}
return
}
// SelectBlockSizeAndInitRangeGen 获取合适的 BlockSize, 和初始化 RangeGen
func (der *Downloader) SelectBlockSizeAndInitRangeGen(single bool, status *transfer.DownloadStatus, parallel int) (blockSize int64, initErr error) {
// Range 生成器
if single { // 单线程
blockSize = -1
return
}
gen := status.RangeListGen()
if gen == nil {
switch der.config.Mode {
case transfer.RangeGenMode_Default:
gen = transfer.NewRangeListGenDefault(status.TotalSize(), 0, 0, parallel)
blockSize = gen.LoadBlockSize()
case transfer.RangeGenMode_BlockSize:
b2 := status.TotalSize()/int64(parallel) + 1
if b2 > der.config.BlockSize { // 选小的BlockSize, 以更高并发
blockSize = der.config.BlockSize
} else {
blockSize = b2
}
gen = transfer.NewRangeListGenBlockSize(status.TotalSize(), 0, blockSize)
default:
initErr = transfer.ErrUnknownRangeGenMode
return
}
} else {
blockSize = gen.LoadBlockSize()
}
status.SetRangeListGen(gen)
return
}
// SelectCacheSize 获取合适的 cacheSize
func (der *Downloader) SelectCacheSize(confCacheSize int, blockSize int64) (cacheSize int) {
if blockSize > 0 && int64(confCacheSize) > blockSize {
// 如果 cache size 过高, 则调低
cacheSize = int(blockSize)
} else {
cacheSize = confCacheSize
}
return
}
// DefaultDURLCheckFunc 默认的 DURLCheckFunc
func DefaultDURLCheckFunc(client *requester.HTTPClient, durl string) (contentLength int64, resp *http.Response, err error) {
resp, err = client.Req(http.MethodGet, durl, nil, nil)
if err != nil {
if resp != nil {
resp.Body.Close()
}
return 0, nil, err
}
return resp.ContentLength, resp, nil
}
func (der *Downloader) checkLoadBalancers() *LoadBalancerResponseList {
var (
loadBalancerResponses = make([]*LoadBalancerResponse, 0, len(der.loadBalansers)+1)
handleLoadBalancer = func(req *http.Request) {
if req == nil {
return
}
if der.config.TryHTTP {
req.URL.Scheme = "http"
}
loadBalancer := &LoadBalancerResponse{
URL: req.URL.String(),
}
loadBalancerResponses = append(loadBalancerResponses, loadBalancer)
logger.Verbosef("DEBUG: load balance task: URL: %s", loadBalancer.URL)
}
)
// 加入第一个
loadBalancerResponses = append(loadBalancerResponses, &LoadBalancerResponse{
URL: "der.durl",
})
// 负载均衡
wg := waitgroup.NewWaitGroup(10)
privTimeout := der.client.Client.Timeout
der.client.SetTimeout(5 * time.Second)
for _, loadBalanser := range der.loadBalansers {
wg.AddDelta()
go func(loadBalanser string) {
defer wg.Done()
subContentLength, subResp, subErr := der.durlCheckFunc(der.client, loadBalanser)
if subResp != nil {
subResp.Body.Close() // 不读Body, 马上关闭连接
}
if subErr != nil {
logger.Verbosef("DEBUG: loadBalanser Error: %s\n", subErr)
return
}
// 检测状态码
switch subResp.StatusCode / 100 {
case 2: // succeed
case 4, 5: // error
var err error
if der.statusCodeBodyCheckFunc != nil {
err = der.statusCodeBodyCheckFunc(subResp.Body)
} else {
err = errors.New(subResp.Status)
}
logger.Verbosef("DEBUG: loadBalanser Status Error: %s\n", err)
return
}
// 检测长度
if der.fileInfo.FileSize != subContentLength {
logger.Verbosef("DEBUG: loadBalanser Content-Length not equal to main server\n")
return
}
//if !der.loadBalancerCompareFunc(der.firstInfo.ToMap(), subResp) {
// logger.Verbosef("DEBUG: loadBalanser not equal to main server\n")
// return
//}
handleLoadBalancer(subResp.Request)
}(loadBalanser)
}
wg.Wait()
der.client.SetTimeout(privTimeout)
loadBalancerResponseList := NewLoadBalancerResponseList(loadBalancerResponses)
return loadBalancerResponseList
}
//Execute 开始任务
func (der *Downloader) Execute() error {
der.lazyInit()
// zero file, no need to download data
if der.fileInfo.FileSize == 0 {
cmdutil.Trigger(der.onFinishEvent)
return nil
}
var (
loadBalancerResponseList = der.checkLoadBalancers()
bii *transfer.DownloadInstanceInfo
)
err := der.initInstanceState(der.config.InstanceStateStorageFormat)
if err != nil {
return err
}
bii = der.instanceState.Get()
var (
isInstance = bii != nil // 是否存在断点信息
status *transfer.DownloadStatus
single = false // 默认开启多线程下载
)
if !isInstance {
bii = &transfer.DownloadInstanceInfo{}
}
if bii.DownloadStatus != nil {
// 使用断点信息的状态
status = bii.DownloadStatus
} else {
// 新建状态
status = transfer.NewDownloadStatus()
status.SetTotalSize(der.fileInfo.FileSize)
}
// 设置限速
if der.config.MaxRate > 0 {
rl := speeds.NewRateLimit(der.config.MaxRate)
status.SetRateLimit(rl)
defer rl.Stop()
}
// 计算文件下载的线程数
parallel := der.SelectParallel(single, MaxParallelWorkerCount, status.TotalSize(), bii.Ranges) // 实际的下载并行量
blockSize, err := der.SelectBlockSizeAndInitRangeGen(single, status, parallel) // 实际的BlockSize
if err != nil {
return err
}
cacheSize := der.SelectCacheSize(der.config.CacheSize, blockSize) // 实际下载缓存
cachepool.SetSyncPoolSize(cacheSize) // 调整pool大小
logger.Verbosef("DEBUG: download task CREATED: parallel: %d, cache size: %d\n", parallel, cacheSize)
der.monitor.InitMonitorCapacity(parallel)
var writer Writer
// 尝试修剪文件
if fder, ok := der.writer.(Fder); ok {
err = prealloc.PreAlloc(fder.Fd(), status.TotalSize())
if err != nil {
logger.Verbosef("DEBUG: truncate file error: %s\n", err)
}
}
writer = der.writer
// 数据平均分配给各个线程
isRange := bii.Ranges != nil && len(bii.Ranges) > 0
if !isRange {
// 没有使用断点续传
// 分配线程
bii.Ranges = make(transfer.RangeList, 0, parallel)
if single { // 单线程
bii.Ranges = append(bii.Ranges, &transfer.Range{Begin: 0, End: der.fileInfo.FileSize})
} else {
gen := status.RangeListGen()
for i := 0; i < cap(bii.Ranges); i++ {
_, r := gen.GenRange()
if r == nil {
break
}
bii.Ranges = append(bii.Ranges, r)
}
}
}
var (
writeMu = &sync.Mutex{}
)
// 获取下载链接
var apierr *apierror.ApiError
durl, apierr := der.panClient.GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
DriveId: der.driveId,
FileId: der.fileInfo.FileId,
})
if apierr != nil && apierr.Code == apierror.ApiCodeDeviceSessionSignatureInvalid {
_, e := der.panClient.CreateSession(nil)
if e == nil {
// retry
durl, apierr = der.panClient.GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
DriveId: der.driveId,
FileId: der.fileInfo.FileId,
})
} else {
logger.Verboseln("CreateSession failed")
}
}
time.Sleep(time.Duration(200) * time.Millisecond)
if apierr != nil {
logger.Verbosef("ERROR: get download url error: %s\n", der.fileInfo.FileId)
cmdutil.Trigger(der.onCancelEvent)
return apierr
}
if durl == nil || durl.Url == "" || strings.HasPrefix(durl.Url, aliyunpan.IllegalDownloadUrlPrefix) {
logger.Verbosef("无法获取有效的下载链接: %+v\n", durl)
cmdutil.Trigger(der.onCancelEvent)
der.removeInstanceState() // 移除断点续传文件
cmdutil.Trigger(der.onFailedEvent)
return ErrFileDownloadForbidden
}
// 初始化下载worker
for k, r := range bii.Ranges {
loadBalancer := loadBalancerResponseList.SequentialGet()
if loadBalancer == nil {
continue
}
logger.Verbosef("work id: %d, download url: %v\n", k, durl)
client := requester.NewHTTPClient()
client.SetKeepAlive(true)
client.SetTimeout(10 * time.Minute)
realUrl := durl.Url
if der.config.UseInternalUrl {
realUrl = durl.InternalUrl
}
worker := NewWorker(k, der.driveId, der.fileInfo.FileId, realUrl, writer, der.globalSpeedsStat)
worker.SetClient(client)
worker.SetPanClient(der.panClient)
worker.SetWriteMutex(writeMu)
worker.SetTotalSize(der.fileInfo.FileSize)
worker.SetAcceptRange("bytes")
worker.SetRange(r) // 分配Range
der.monitor.Append(worker)
}
der.monitor.SetStatus(status)
// 服务器不支持断点续传, 或者单线程下载, 都不重载worker
der.monitor.SetReloadWorker(parallel > 1)
moniterCtx, moniterCancelFunc := context.WithCancel(context.Background())
der.monitorCancelFunc = moniterCancelFunc
der.monitor.SetInstanceState(der.instanceState)
// 开始执行
der.executeTime = time.Now()
cmdutil.Trigger(der.onExecuteEvent)
der.downloadStatusEvent() // 启动执行状态处理事件
der.monitor.Execute(moniterCtx)
// 检查错误
err = der.monitor.Err()
if err == nil { // 成功
cmdutil.Trigger(der.onSuccessEvent)
der.removeInstanceState() // 移除断点续传文件
} else {
if err == ErrNoWokers && der.fileInfo.FileSize == 0 {
cmdutil.Trigger(der.onSuccessEvent)
der.removeInstanceState() // 移除断点续传文件
}
}
// 执行结束
cmdutil.Trigger(der.onFinishEvent)
return err
}
//downloadStatusEvent 执行状态处理事件
func (der *Downloader) downloadStatusEvent() {
if der.onDownloadStatusEvent == nil {
return
}
status := der.monitor.Status()
go func() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-der.monitor.completed:
return
case <-ticker.C:
time.Sleep(500 * time.Millisecond)
der.onDownloadStatusEvent(status, der.monitor.RangeWorker)
}
}
}()
}
//Pause 暂停
func (der *Downloader) Pause() {
if der.monitor == nil {
return
}
cmdutil.Trigger(der.onPauseEvent)
der.monitor.Pause()
}
//Resume 恢复
func (der *Downloader) Resume() {
if der.monitor == nil {
return
}
cmdutil.Trigger(der.onResumeEvent)
der.monitor.Resume()
}
//Cancel 取消
func (der *Downloader) Cancel() {
if der.monitor == nil {
return
}
cmdutil.Trigger(der.onCancelEvent)
cmdutil.Trigger(der.monitorCancelFunc)
}
//Failed 失败
func (der *Downloader) Failed() {
if der.monitor == nil {
return
}
cmdutil.Trigger(der.onFailedEvent)
cmdutil.Trigger(der.monitorCancelFunc)
}
//OnExecute 设置开始下载事件
func (der *Downloader) OnExecute(onExecuteEvent requester.Event) {
der.onExecuteEvent = onExecuteEvent
}
//OnSuccess 设置成功下载事件
func (der *Downloader) OnSuccess(onSuccessEvent requester.Event) {
der.onSuccessEvent = onSuccessEvent
}
//OnFailed 设置失败事件
func (der *Downloader) OnFailed(onFailedEvent requester.Event) {
der.onFailedEvent = onFailedEvent
}
//OnFinish 设置结束下载事件
func (der *Downloader) OnFinish(onFinishEvent requester.Event) {
der.onFinishEvent = onFinishEvent
}
//OnPause 设置暂停下载事件
func (der *Downloader) OnPause(onPauseEvent requester.Event) {
der.onPauseEvent = onPauseEvent
}
//OnResume 设置恢复下载事件
func (der *Downloader) OnResume(onResumeEvent requester.Event) {
der.onResumeEvent = onResumeEvent
}
//OnCancel 设置取消下载事件
func (der *Downloader) OnCancel(onCancelEvent requester.Event) {
der.onCancelEvent = onCancelEvent
}
//OnDownloadStatusEvent 设置状态处理函数
func (der *Downloader) OnDownloadStatusEvent(f DownloadStatusFunc) {
der.onDownloadStatusEvent = f
}
+173
View File
@@ -0,0 +1,173 @@
// 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 downloader
import (
"errors"
"github.com/json-iterator/go"
"github.com/tickstep/library-go/cachepool"
"github.com/tickstep/library-go/crypto"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"os"
"sync"
)
type (
//InstanceState 状态, 断点续传信息
InstanceState struct {
saveFile *os.File
format InstanceStateStorageFormat
ii *transfer.DownloadInstanceInfoExport
mu sync.Mutex
}
// InstanceStateStorageFormat 断点续传储存类型
InstanceStateStorageFormat int
)
const (
// InstanceStateStorageFormatJSON json 格式
InstanceStateStorageFormatJSON = iota
// InstanceStateStorageFormatProto3 protobuf 格式
InstanceStateStorageFormatProto3
)
//NewInstanceState 初始化InstanceState
func NewInstanceState(saveFile *os.File, format InstanceStateStorageFormat) *InstanceState {
return &InstanceState{
saveFile: saveFile,
format: format,
}
}
func (is *InstanceState) checkSaveFile() bool {
return is.saveFile != nil
}
func (is *InstanceState) getSaveFileContents() []byte {
if !is.checkSaveFile() {
return nil
}
finfo, err := is.saveFile.Stat()
if err != nil {
panic(err)
}
size := finfo.Size()
if size > 0xffffffff {
panic("savePath too large")
}
intSize := int(size)
buf := cachepool.RawMallocByteSlice(intSize)
n, _ := is.saveFile.ReadAt(buf, 0)
return crypto.Base64Decode(buf[:n])
}
//Get 获取断点续传信息
func (is *InstanceState) Get() (eii *transfer.DownloadInstanceInfo) {
if !is.checkSaveFile() {
return nil
}
is.mu.Lock()
defer is.mu.Unlock()
contents := is.getSaveFileContents()
if len(contents) <= 0 {
return
}
is.ii = &transfer.DownloadInstanceInfoExport{}
var err error
err = jsoniter.Unmarshal(contents, is.ii)
if err != nil {
logger.Verbosef("DEBUG: InstanceInfo unmarshal error: %s\n", err)
return
}
eii = is.ii.GetInstanceInfo()
return
}
//Put 提交断点续传信息
func (is *InstanceState) Put(eii *transfer.DownloadInstanceInfo) {
if !is.checkSaveFile() {
return
}
is.mu.Lock()
defer is.mu.Unlock()
if is.ii == nil {
is.ii = &transfer.DownloadInstanceInfoExport{}
}
is.ii.SetInstanceInfo(eii)
var (
data []byte
err error
)
data, err = jsoniter.Marshal(is.ii)
if err != nil {
panic(err)
}
err = is.saveFile.Truncate(int64(len(data)))
if err != nil {
logger.Verbosef("DEBUG: truncate file error: %s\n", err)
}
_, err = is.saveFile.WriteAt(crypto.Base64Encode(data), 0)
if err != nil {
logger.Verbosef("DEBUG: write instance state error: %s\n", err)
}
}
//Close 关闭
func (is *InstanceState) Close() error {
if !is.checkSaveFile() {
return nil
}
return is.saveFile.Close()
}
func (der *Downloader) initInstanceState(format InstanceStateStorageFormat) (err error) {
if der.instanceState != nil {
return errors.New("already initInstanceState")
}
var saveFile *os.File
if der.config.InstanceStatePath != "" {
saveFile, err = os.OpenFile(der.config.InstanceStatePath, os.O_RDWR|os.O_CREATE, 0777)
if err != nil {
return err
}
}
der.instanceState = NewInstanceState(saveFile, format)
return nil
}
func (der *Downloader) removeInstanceState() error {
der.instanceState.Close()
if der.config.InstanceStatePath != "" {
return os.Remove(der.config.InstanceStatePath)
}
return nil
}
+81
View File
@@ -0,0 +1,81 @@
// 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 downloader
import (
"net/http"
"sync/atomic"
)
type (
// LoadBalancerResponse 负载均衡响应状态
LoadBalancerResponse struct {
URL string
}
// LoadBalancerResponseList 负载均衡列表
LoadBalancerResponseList struct {
lbr []*LoadBalancerResponse
cursor int32
}
LoadBalancerCompareFunc func(info map[string]string, subResp *http.Response) bool
)
// NewLoadBalancerResponseList 初始化负载均衡列表
func NewLoadBalancerResponseList(lbr []*LoadBalancerResponse) *LoadBalancerResponseList {
return &LoadBalancerResponseList{
lbr: lbr,
}
}
// SequentialGet 顺序获取
func (lbrl *LoadBalancerResponseList) SequentialGet() *LoadBalancerResponse {
if len(lbrl.lbr) == 0 {
return nil
}
if int(lbrl.cursor) >= len(lbrl.lbr) {
lbrl.cursor = 0
}
lbr := lbrl.lbr[int(lbrl.cursor)]
atomic.AddInt32(&lbrl.cursor, 1)
return lbr
}
// RandomGet 随机获取
func (lbrl *LoadBalancerResponseList) RandomGet() *LoadBalancerResponse {
return lbrl.lbr[RandomNumber(0, len(lbrl.lbr))]
}
// AddLoadBalanceServer 增加负载均衡服务器
func (der *Downloader) AddLoadBalanceServer(urls ...string) {
der.loadBalansers = append(der.loadBalansers, urls...)
}
// DefaultLoadBalancerCompareFunc 检测负载均衡的服务器是否一致
func DefaultLoadBalancerCompareFunc(info map[string]string, subResp *http.Response) bool {
if info == nil || subResp == nil {
return false
}
for headerKey, value := range info {
if value != subResp.Header.Get(headerKey) {
return false
}
}
return true
}
+447
View File
@@ -0,0 +1,447 @@
// 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 downloader
import (
"context"
"errors"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"sort"
"time"
)
var (
//ErrNoWokers no workers
ErrNoWokers = errors.New("no workers")
)
type (
//Monitor 线程监控器
Monitor struct {
workers WorkerList
status *transfer.DownloadStatus
instanceState *InstanceState
completed chan struct{}
err error
resetController *ResetController
isReloadWorker bool //是否重载worker, 单线程模式不重载
// 临时变量
lastAvaliableIndex int
}
// RangeWorkerFunc 遍历workers的函数
RangeWorkerFunc func(key int, worker *Worker) bool
)
//NewMonitor 初始化Monitor
func NewMonitor() *Monitor {
monitor := &Monitor{}
return monitor
}
func (mt *Monitor) lazyInit() {
if mt.workers == nil {
mt.workers = make(WorkerList, 0, 100)
}
if mt.status == nil {
mt.status = transfer.NewDownloadStatus()
}
if mt.resetController == nil {
mt.resetController = NewResetController(80)
}
}
//InitMonitorCapacity 初始化workers, 用于Append
func (mt *Monitor) InitMonitorCapacity(capacity int) {
mt.workers = make(WorkerList, 0, capacity)
}
//Append 增加Worker
func (mt *Monitor) Append(worker *Worker) {
if worker == nil {
return
}
mt.workers = append(mt.workers, worker)
}
//SetWorkers 设置workers, 此操作会覆盖原有的workers
func (mt *Monitor) SetWorkers(workers WorkerList) {
mt.workers = workers
}
//SetStatus 设置DownloadStatus
func (mt *Monitor) SetStatus(status *transfer.DownloadStatus) {
mt.status = status
}
//SetInstanceState 设置状态
func (mt *Monitor) SetInstanceState(instanceState *InstanceState) {
mt.instanceState = instanceState
}
//Status 返回DownloadStatus
func (mt *Monitor) Status() *transfer.DownloadStatus {
return mt.status
}
//Err 返回遇到的错误
func (mt *Monitor) Err() error {
return mt.err
}
//CompletedChan 获取completed chan
func (mt *Monitor) CompletedChan() <-chan struct{} {
return mt.completed
}
//GetAvailableWorker 获取空闲的worker
func (mt *Monitor) GetAvailableWorker() *Worker {
workerCount := len(mt.workers)
for i := mt.lastAvaliableIndex; i < mt.lastAvaliableIndex+workerCount; i++ {
index := i % workerCount
worker := mt.workers[index]
if worker.Completed() {
mt.lastAvaliableIndex = index
return worker
}
}
return nil
}
//GetAllWorkersRange 获取所有worker的范围
func (mt *Monitor) GetAllWorkersRange() transfer.RangeList {
allWorkerRanges := make(transfer.RangeList, 0, len(mt.workers))
for _, worker := range mt.workers {
allWorkerRanges = append(allWorkerRanges, worker.GetRange())
}
return allWorkerRanges
}
//NumLeftWorkers 剩余的worker数量
func (mt *Monitor) NumLeftWorkers() (num int) {
for _, worker := range mt.workers {
if !worker.Completed() {
num++
}
}
return
}
//SetReloadWorker 是否重载worker
func (mt *Monitor) SetReloadWorker(b bool) {
mt.isReloadWorker = b
}
//IsLeftWorkersAllFailed 剩下的线程是否全部失败
func (mt *Monitor) IsLeftWorkersAllFailed() bool {
failedNum := 0
for _, worker := range mt.workers {
if worker.Completed() {
continue
}
if !worker.Failed() {
failedNum++
return false
}
}
return failedNum != 0
}
//registerAllCompleted 全部完成则发送消息
func (mt *Monitor) registerAllCompleted() {
mt.completed = make(chan struct{}, 0)
var (
workerNum = len(mt.workers)
completeNum = 0
)
go func() {
for {
time.Sleep(1 * time.Second)
completeNum = 0
for _, worker := range mt.workers {
switch worker.GetStatus().StatusCode() {
case StatusCodeInternalError:
// 检测到内部错误
// 马上停止执行
mt.err = worker.Err()
close(mt.completed)
return
case StatusCodeSuccessed, StatusCodeCanceled:
completeNum++
}
}
// status 在 lazyInit 之后, 不可能为空
// 完成条件: 所有worker 都已经完成, 且 rangeGen 已生成完毕
gen := mt.status.RangeListGen()
if completeNum >= workerNum && (gen == nil || gen.IsDone()) { // 已完成
close(mt.completed)
return
}
}
}()
}
//ResetFailedAndNetErrorWorkers 重设部分网络错误的worker
func (mt *Monitor) ResetFailedAndNetErrorWorkers() {
for k := range mt.workers {
if !mt.resetController.CanReset() {
continue
}
switch mt.workers[k].GetStatus().StatusCode() {
case StatusCodeNetError:
logger.Verbosef("DEBUG: monitor: ResetFailedAndNetErrorWorkers: reset StatusCodeNetError worker, id: %d\n", mt.workers[k].id)
goto reset
case StatusCodeFailed:
logger.Verbosef("DEBUG: monitor: ResetFailedAndNetErrorWorkers: reset StatusCodeFailed worker, id: %d\n", mt.workers[k].id)
goto reset
default:
continue
}
reset:
mt.workers[k].Reset()
mt.resetController.AddResetNum()
}
}
//RangeWorker 遍历worker
func (mt *Monitor) RangeWorker(f RangeWorkerFunc) {
for k := range mt.workers {
if !f(k, mt.workers[k]) {
break
}
}
}
//Pause 暂停所有的下载
func (mt *Monitor) Pause() {
for k := range mt.workers {
mt.workers[k].Pause()
}
}
//Resume 恢复所有的下载
func (mt *Monitor) Resume() {
for k := range mt.workers {
mt.workers[k].Resume()
}
}
// TryAddNewWork 尝试加入新range
func (mt *Monitor) TryAddNewWork() {
if mt.status == nil {
return
}
gen := mt.status.RangeListGen()
if gen == nil || gen.IsDone() {
return
}
if !mt.resetController.CanReset() { //能否建立新连接
return
}
availableWorker := mt.GetAvailableWorker()
if availableWorker == nil {
return
}
// 有空闲的range, 执行
_, r := gen.GenRange()
if r == nil {
// 没有range了
return
}
availableWorker.SetRange(r)
availableWorker.ClearStatus()
mt.resetController.AddResetNum()
logger.Verbosef("MONITER: worker[%d] add new range: %s\n", availableWorker.ID(), r.ShowDetails())
go availableWorker.Execute()
}
// DynamicSplitWorker 动态分配线程
func (mt *Monitor) DynamicSplitWorker(worker *Worker) {
if !mt.resetController.CanReset() {
return
}
switch worker.status.statusCode {
case StatusCodeDownloading, StatusCodeFailed, StatusCodeNetError:
//pass
default:
return
}
// 筛选空闲的Worker
availableWorker := mt.GetAvailableWorker()
if availableWorker == nil || worker == availableWorker { // 没有空的
return
}
workerRange := worker.GetRange()
end := workerRange.LoadEnd()
middle := (workerRange.LoadBegin() + end) / 2
if end-middle < MinParallelSize/5 { // 如果线程剩余的下载量太少, 不分配空闲线程
return
}
// 折半
availableWorkerRange := availableWorker.GetRange()
availableWorkerRange.StoreBegin(middle) // middle不能加1
availableWorkerRange.StoreEnd(end)
availableWorker.ClearStatus()
workerRange.StoreEnd(middle)
mt.resetController.AddResetNum()
logger.Verbosef("MONITOR: worker duplicated: %d <- %d\n", availableWorker.ID(), worker.ID())
go availableWorker.Execute()
}
// ResetWorker 重设长时间无响应, 和下载速度为 0 的 Worker
func (mt *Monitor) ResetWorker(worker *Worker) {
if !mt.resetController.CanReset() { //达到最大重载次数
return
}
if worker.Completed() {
return
}
// 忽略正在写入数据到硬盘的
// 过滤速度有变化的线程
status := worker.GetStatus()
speeds := worker.GetSpeedsPerSecond()
if speeds != 0 {
return
}
switch status.StatusCode() {
case StatusCodePending, StatusCodeReseted:
fallthrough
case StatusCodeWaitToWrite: // 正在写入数据
fallthrough
case StatusCodePaused: // 已暂停
// 忽略, 返回
return
case StatusCodeDownloadUrlExpired: // 下载链接已经过期
worker.RefreshDownloadUrl()
break
}
mt.resetController.AddResetNum()
// 重设连接
logger.Verbosef("MONITOR: worker[%d] reload\n", worker.ID())
worker.Reset()
}
//Execute 执行任务
func (mt *Monitor) Execute(cancelCtx context.Context) {
if len(mt.workers) == 0 {
mt.err = ErrNoWokers
return
}
mt.lazyInit()
for _, worker := range mt.workers {
worker.SetDownloadStatus(mt.status)
go worker.Execute()
}
mt.registerAllCompleted() // 注册completed
ticker := time.NewTicker(990 * time.Millisecond)
defer ticker.Stop()
//开始监控
for {
select {
case <-cancelCtx.Done():
for _, worker := range mt.workers {
err := worker.Cancel()
if err != nil {
logger.Verbosef("DEBUG: cancel failed, worker id: %d, err: %s\n", worker.ID(), err)
}
}
return
case <-mt.completed:
return
case <-ticker.C:
// 初始化监控工作
mt.ResetFailedAndNetErrorWorkers()
mt.status.UpdateSpeeds() // 更新速度
// 保存断点信息到文件
if mt.instanceState != nil {
mt.instanceState.Put(&transfer.DownloadInstanceInfo{
DownloadStatus: mt.status,
Ranges: mt.GetAllWorkersRange(),
})
}
// 加入新range
mt.TryAddNewWork()
// 是否有失败的worker
for _, w := range mt.workers {
if w.status.statusCode == StatusCodeDownloadUrlExpired {
mt.ResetWorker(w)
}
}
// 不重载worker
if !mt.isReloadWorker {
continue
}
// 更新maxSpeeds
mt.status.SetMaxSpeeds(mt.status.SpeedsPerSecond())
// 速度减慢或者全部失败, 开始监控
// 只有一个worker时不重设连接
isLeftWorkersAllFailed := mt.IsLeftWorkersAllFailed()
if mt.status.SpeedsPerSecond() < mt.status.MaxSpeeds()/6 || isLeftWorkersAllFailed {
if isLeftWorkersAllFailed {
logger.Verbosef("DEBUG: monitor: All workers failed\n")
}
mt.status.ClearMaxSpeeds() //清空最大速度的统计
// 先进行动态分配线程
logger.Verbosef("DEBUG: monitor: start duplicate.\n")
sort.Sort(ByLeftDesc{mt.workers})
for _, worker := range mt.workers {
//动态分配线程
mt.DynamicSplitWorker(worker)
}
// 重设长时间无响应, 和下载速度为 0 的线程
logger.Verbosef("DEBUG: monitor: start reload.\n")
for _, worker := range mt.workers {
mt.ResetWorker(worker)
}
} // end if
} //end select
} //end for
}
@@ -0,0 +1,61 @@
// 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 downloader
import (
"github.com/tickstep/library-go/expires"
"sync"
"time"
)
// ResetController 网络连接控制器
type ResetController struct {
mu sync.Mutex
currentTime time.Time
maxResetNum int
resetEntity map[expires.Expires]struct{}
}
// NewResetController 初始化*ResetController
func NewResetController(maxResetNum int) *ResetController {
return &ResetController{
currentTime: time.Now(),
maxResetNum: maxResetNum,
resetEntity: map[expires.Expires]struct{}{},
}
}
func (rc *ResetController) update() {
for k := range rc.resetEntity {
if k.IsExpires() {
delete(rc.resetEntity, k)
}
}
}
// AddResetNum 增加连接
func (rc *ResetController) AddResetNum() {
rc.mu.Lock()
defer rc.mu.Unlock()
rc.update()
rc.resetEntity[expires.NewExpires(9*time.Second)] = struct{}{}
}
// CanReset 是否可以建立连接
func (rc *ResetController) CanReset() bool {
rc.mu.Lock()
defer rc.mu.Unlock()
rc.update()
return len(rc.resetEntity) < rc.maxResetNum
}
+36
View File
@@ -0,0 +1,36 @@
// 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 downloader
type (
// ByLeftDesc 根据剩余下载量倒序排序
ByLeftDesc struct {
WorkerList
}
)
// Len 返回长度
func (wl WorkerList) Len() int {
return len(wl)
}
// Swap 交换
func (wl WorkerList) Swap(i, j int) {
wl[i], wl[j] = wl[j], wl[i]
}
// Less 实现倒序
func (wl ByLeftDesc) Less(i, j int) bool {
return wl.WorkerList[i].wrange.Len() > wl.WorkerList[j].wrange.Len()
}
+122
View File
@@ -0,0 +1,122 @@
// 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 downloader
import (
"github.com/tickstep/aliyunpan/library/requester/transfer"
)
type (
//WorkerStatuser 状态
WorkerStatuser interface {
StatusCode() StatusCode //状态码
StatusText() string
}
//StatusCode 状态码
StatusCode int
//WorkerStatus worker状态
WorkerStatus struct {
statusCode StatusCode
}
// DownloadStatusFunc 下载状态处理函数
DownloadStatusFunc func(status transfer.DownloadStatuser, workersCallback func(RangeWorkerFunc))
)
const (
//StatusCodeInit 初始化
StatusCodeInit StatusCode = iota
//StatusCodeSuccessed 成功
StatusCodeSuccessed
//StatusCodePending 等待响应
StatusCodePending
//StatusCodeDownloading 下载中
StatusCodeDownloading
//StatusCodeWaitToWrite 等待写入数据
StatusCodeWaitToWrite
//StatusCodeInternalError 内部错误
StatusCodeInternalError
//StatusCodeTooManyConnections 连接数太多
StatusCodeTooManyConnections
//StatusCodeNetError 网络错误
StatusCodeNetError
//StatusCodeFailed 下载失败
StatusCodeFailed
//StatusCodePaused 已暂停
StatusCodePaused
//StatusCodeReseted 已重设连接
StatusCodeReseted
//StatusCodeCanceled 已取消
StatusCodeCanceled
//StatusCodeDownloadUrlExpired 下载链接已过期
StatusCodeDownloadUrlExpired
//StatusCodeIllegalDownloadFile 文件非法,不允许下载
StatusCodeIllegalDownloadFile
)
//GetStatusText 根据状态码获取状态信息
func GetStatusText(sc StatusCode) string {
switch sc {
case StatusCodeInit:
return "初始化"
case StatusCodeSuccessed:
return "成功"
case StatusCodePending:
return "等待响应"
case StatusCodeDownloading:
return "下载中"
case StatusCodeWaitToWrite:
return "等待写入数据"
case StatusCodeInternalError:
return "内部错误"
case StatusCodeTooManyConnections:
return "连接数太多"
case StatusCodeNetError:
return "网络错误"
case StatusCodeFailed:
return "下载失败"
case StatusCodePaused:
return "已暂停"
case StatusCodeReseted:
return "已重设连接"
case StatusCodeCanceled:
return "已取消"
default:
return "未知状态码"
}
}
//NewWorkerStatus 初始化WorkerStatus
func NewWorkerStatus() *WorkerStatus {
return &WorkerStatus{
statusCode: StatusCodeInit,
}
}
//SetStatusCode 设置worker状态码
func (ws *WorkerStatus) SetStatusCode(sc StatusCode) {
ws.statusCode = sc
}
//StatusCode 返回状态码
func (ws *WorkerStatus) StatusCode() StatusCode {
return ws.statusCode
}
//StatusText 返回状态信息
func (ws *WorkerStatus) StatusText() string {
return GetStatusText(ws.statusCode)
}
+101
View File
@@ -0,0 +1,101 @@
// 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 downloader
import (
"errors"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
mathrand "math/rand"
"mime"
"net/url"
"path"
"regexp"
"strconv"
"time"
)
var (
// ContentRangeRE Content-Range 正则
ContentRangeRE = regexp.MustCompile(`^.*? \d*?-\d*?/(\d*?)$`)
// ranSource 随机数种子
ranSource = mathrand.NewSource(time.Now().UnixNano())
// ran 一个随机数实例
ran = mathrand.New(ranSource)
// 文件被禁止下载
ErrFileDownloadForbidden = errors.New("文件被禁止下载")
)
// RandomNumber 生成指定区间随机数
func RandomNumber(min, max int) int {
if min > max {
min, max = max, min
}
return ran.Intn(max-min) + min
}
// GetFileName 获取文件名
func GetFileName(uri string, client *requester.HTTPClient) (filename string, err error) {
if client == nil {
client = requester.NewHTTPClient()
}
resp, err := client.Req("HEAD", uri, nil, nil)
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
return "", err
}
_, params, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition"))
if err != nil {
logger.Verbosef("DEBUG: GetFileName ParseMediaType error: %s\n", err)
return path.Base(uri), nil
}
filename, err = url.QueryUnescape(params["filename"])
if err != nil {
return
}
if filename == "" {
filename = path.Base(uri)
}
return
}
// ParseContentRange 解析Content-Range
func ParseContentRange(contentRange string) (contentLength int64) {
raw := ContentRangeRE.FindStringSubmatch(contentRange)
if len(raw) < 2 {
return -1
}
c, err := strconv.ParseInt(raw[1], 10, 64)
if err != nil {
return -1
}
return c
}
func fixCacheSize(size *int) {
if *size < 1024 {
*size = 1024
}
}
+496
View File
@@ -0,0 +1,496 @@
// 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 downloader
import (
"context"
"errors"
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/library-go/cachepool"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"github.com/tickstep/library-go/requester/rio/speeds"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"io"
"net/http"
"sync"
)
type (
//Worker 工作单元
Worker struct {
totalSize int64 // 整个文件的大小, worker请求range时会获取尝试获取该值, 如果不匹配, 则返回错误
wrange *transfer.Range
speedsStat *speeds.Speeds
globalSpeedsStat *speeds.Speeds // 全局速度统计
id int // work id
fileId string // 文件ID
driveId string
url string // 下载地址
acceptRanges string
panClient *aliyunpan.PanClient
client *requester.HTTPClient
writerAt io.WriterAt
writeMu *sync.Mutex
execMu sync.Mutex
pauseChan chan struct{}
workerCancelFunc context.CancelFunc
resetFunc context.CancelFunc
readRespBodyCancelFunc func()
err error // 错误信息
status WorkerStatus
downloadStatus *transfer.DownloadStatus // 总的下载状态
}
// WorkerList worker列表
WorkerList []*Worker
)
// Duplicate 构造新的列表
func (wl WorkerList) Duplicate() WorkerList {
n := make(WorkerList, len(wl))
copy(n, wl)
return n
}
//NewWorker 初始化Worker
func NewWorker(id int, driveId string, fileId, durl string, writerAt io.WriterAt, globalSpeedsStat *speeds.Speeds) *Worker {
return &Worker{
id: id,
url: durl,
writerAt: writerAt,
fileId: fileId,
driveId: driveId,
globalSpeedsStat: globalSpeedsStat,
}
}
//ID 返回worker ID
func (wer *Worker) ID() int {
return wer.id
}
func (wer *Worker) lazyInit() {
if wer.client == nil {
wer.client = requester.NewHTTPClient()
}
if wer.pauseChan == nil {
wer.pauseChan = make(chan struct{})
}
if wer.wrange == nil {
wer.wrange = &transfer.Range{}
}
if wer.wrange.LoadBegin() == 0 && wer.wrange.LoadEnd() == 0 {
// 取消多线程下载
wer.acceptRanges = ""
wer.wrange.StoreEnd(-2)
}
if wer.speedsStat == nil {
wer.speedsStat = &speeds.Speeds{}
}
}
// SetTotalSize 设置整个文件的大小, worker请求range时会获取尝试获取该值, 如果不匹配, 则返回错误
func (wer *Worker) SetTotalSize(size int64) {
wer.totalSize = size
}
//SetClient 设置http客户端
func (wer *Worker) SetClient(c *requester.HTTPClient) {
wer.client = c
}
func (wer *Worker) SetPanClient(p *aliyunpan.PanClient) {
wer.panClient = p
}
//SetAcceptRange 设置AcceptRange
func (wer *Worker) SetAcceptRange(acceptRanges string) {
wer.acceptRanges = acceptRanges
}
//SetRange 设置请求范围
func (wer *Worker) SetRange(r *transfer.Range) {
if wer.wrange == nil {
wer.wrange = r
return
}
wer.wrange.StoreBegin(r.LoadBegin())
wer.wrange.StoreEnd(r.LoadEnd())
}
//SetWriteMutex 设置数据写锁
func (wer *Worker) SetWriteMutex(mu *sync.Mutex) {
wer.writeMu = mu
}
//SetDownloadStatus 增加其他需要统计的数据
func (wer *Worker) SetDownloadStatus(downloadStatus *transfer.DownloadStatus) {
wer.downloadStatus = downloadStatus
}
//GetStatus 返回下载状态
func (wer *Worker) GetStatus() WorkerStatuser {
// 空接口与空指针不等价
return &wer.status
}
//GetRange 返回worker范围
func (wer *Worker) GetRange() *transfer.Range {
return wer.wrange
}
//GetSpeedsPerSecond 获取每秒的速度
func (wer *Worker) GetSpeedsPerSecond() int64 {
return wer.speedsStat.GetSpeeds()
}
//Pause 暂停下载
func (wer *Worker) Pause() {
wer.lazyInit()
if wer.acceptRanges == "" {
logger.Verbosef("WARNING: worker unsupport pause")
return
}
if wer.status.statusCode == StatusCodePaused {
return
}
wer.pauseChan <- struct{}{}
wer.status.statusCode = StatusCodePaused
}
//Resume 恢复下载
func (wer *Worker) Resume() {
if wer.status.statusCode != StatusCodePaused {
return
}
go wer.Execute()
}
//Cancel 取消下载
func (wer *Worker) Cancel() error {
if wer.workerCancelFunc == nil {
return errors.New("cancelFunc not set")
}
wer.workerCancelFunc()
if wer.readRespBodyCancelFunc != nil {
wer.readRespBodyCancelFunc()
}
return nil
}
//Reset 重设连接
func (wer *Worker) Reset() {
if wer.resetFunc == nil {
logger.Verbosef("DEBUG: worker: resetFunc not set")
return
}
wer.resetFunc()
if wer.readRespBodyCancelFunc != nil {
wer.readRespBodyCancelFunc()
}
wer.ClearStatus()
go wer.Execute()
}
// RefreshDownloadUrl 重新刷新下载链接
func (wer *Worker) RefreshDownloadUrl() {
var apierr *apierror.ApiError
durl, apierr := wer.panClient.GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{DriveId: wer.driveId, FileId: wer.fileId})
if apierr != nil {
wer.status.statusCode = StatusCodeTooManyConnections
return
}
wer.url = durl.Url
}
// Canceled 是否已经取消
func (wer *Worker) Canceled() bool {
return wer.status.statusCode == StatusCodeCanceled
}
//Completed 是否已经完成
func (wer *Worker) Completed() bool {
switch wer.status.statusCode {
case StatusCodeSuccessed, StatusCodeCanceled:
return true
default:
return false
}
}
//Failed 是否失败
func (wer *Worker) Failed() bool {
switch wer.status.statusCode {
case StatusCodeFailed, StatusCodeInternalError, StatusCodeTooManyConnections, StatusCodeNetError:
return true
default:
return false
}
}
//ClearStatus 清空状态
func (wer *Worker) ClearStatus() {
wer.status.statusCode = StatusCodeInit
}
//Err 返回worker错误
func (wer *Worker) Err() error {
return wer.err
}
//Execute 执行任务
func (wer *Worker) Execute() {
wer.lazyInit()
wer.execMu.Lock()
defer wer.execMu.Unlock()
wer.status.statusCode = StatusCodeInit
single := wer.acceptRanges == ""
// 如果已暂停, 退出
if wer.status.statusCode == StatusCodePaused {
return
}
if !single {
// 已完成
if rlen := wer.wrange.Len(); rlen <= 0 {
if rlen < 0 {
logger.Verbosef("DEBUG: RangeLen is negative at begin: %v, %d\n", wer.wrange, wer.wrange.Len())
}
wer.status.statusCode = StatusCodeSuccessed
return
}
}
// zero size file
if wer.totalSize == 0 {
wer.status.statusCode = StatusCodeSuccessed
return
}
workerCancelCtx, workerCancelFunc := context.WithCancel(context.Background())
wer.workerCancelFunc = workerCancelFunc
resetCtx, resetFunc := context.WithCancel(context.Background())
wer.resetFunc = resetFunc
wer.status.statusCode = StatusCodePending
var resp *http.Response
apierr := wer.panClient.DownloadFileData(wer.url, aliyunpan.FileDownloadRange{
Offset: wer.wrange.Begin,
End: wer.wrange.End - 1,
}, func(httpMethod, fullUrl string, headers map[string]string) (*http.Response, error) {
resp, wer.err = wer.client.Req(httpMethod, fullUrl, nil, headers)
if wer.err != nil {
return nil, wer.err
}
return resp, wer.err
})
if resp != nil {
defer func() {
resp.Body.Close()
}()
wer.readRespBodyCancelFunc = func() {
resp.Body.Close()
}
}
if wer.err != nil || apierr != nil {
wer.status.statusCode = StatusCodeNetError
return
}
// 判断响应状态
switch resp.StatusCode {
case 200, 206:
// do nothing, continue
wer.status.statusCode = StatusCodeDownloading
break
case 416: //Requested Range Not Satisfiable
fallthrough
case 403: // Forbidden
fallthrough
case 406: // Not Acceptable
wer.status.statusCode = StatusCodeNetError
wer.err = errors.New(resp.Status)
return
case 404:
wer.status.statusCode = StatusCodeDownloadUrlExpired
wer.err = errors.New(resp.Status)
return
case 429, 509: // Too Many Requests
wer.status.SetStatusCode(StatusCodeTooManyConnections)
wer.err = errors.New(resp.Status)
return
default:
wer.status.statusCode = StatusCodeNetError
wer.err = fmt.Errorf("unexpected http status code, %d, %s", resp.StatusCode, resp.Status)
return
}
var (
contentLength = resp.ContentLength
rangeLength = wer.wrange.Len()
)
if !single {
// 检查请求长度
if contentLength != rangeLength {
wer.status.statusCode = StatusCodeNetError
wer.err = fmt.Errorf("Content-Length is unexpected: %d, need %d", contentLength, rangeLength)
return
}
// 检查总大小
if wer.totalSize > 0 {
total := ParseContentRange(resp.Header.Get("Content-Range"))
if total > 0 {
if total != wer.totalSize {
wer.status.statusCode = StatusCodeInternalError // 这里设置为内部错误, 强制停止下载
wer.err = fmt.Errorf("Content-Range total length is unexpected: %d, need %d", total, wer.totalSize)
return
}
}
}
}
var (
buf = cachepool.SyncPool.Get().([]byte)
n, nn int
n64, nn64 int64
)
defer cachepool.SyncPool.Put(buf)
for {
select {
case <-workerCancelCtx.Done(): //取消
wer.status.statusCode = StatusCodeCanceled
return
case <-resetCtx.Done(): //重设连接
wer.status.statusCode = StatusCodeReseted
return
case <-wer.pauseChan: //暂停
return
default:
wer.status.statusCode = StatusCodeDownloading
// 初始化数据
var readErr error
n = 0
// 读取数据
for n < len(buf) && readErr == nil && (single || wer.wrange.Len() > 0) {
nn, readErr = resp.Body.Read(buf[n:])
nn64 = int64(nn)
// 更新速度统计
if wer.downloadStatus != nil {
wer.downloadStatus.AddSpeedsDownloaded(nn64) // 限速在这里阻塞
}
wer.speedsStat.Add(nn64)
if wer.globalSpeedsStat != nil {
wer.globalSpeedsStat.Add(nn64)
}
n += nn
}
if n > 0 && readErr == io.EOF {
readErr = io.ErrUnexpectedEOF
}
n64 = int64(n)
// 非单线程模式下
if !single {
rangeLength = wer.wrange.Len()
// 已完成
if rangeLength <= 0 {
wer.status.statusCode = StatusCodeCanceled
wer.err = errors.New("worker already complete")
return
}
if n64 > rangeLength {
// 数据大小不正常
n64 = rangeLength
n = int(rangeLength)
readErr = io.EOF
}
}
// 写入数据
if wer.writerAt != nil {
wer.status.statusCode = StatusCodeWaitToWrite
if wer.writeMu != nil {
wer.writeMu.Lock() // 加锁, 减轻硬盘的压力
}
_, wer.err = wer.writerAt.WriteAt(buf[:n], wer.wrange.Begin) // 写入数据
if wer.err != nil {
if wer.writeMu != nil {
wer.writeMu.Unlock() //解锁
}
wer.status.statusCode = StatusCodeInternalError
return
}
if wer.writeMu != nil {
wer.writeMu.Unlock() //解锁
}
wer.status.statusCode = StatusCodeDownloading
}
// 更新下载统计数据
wer.wrange.AddBegin(n64)
if wer.downloadStatus != nil {
wer.downloadStatus.AddDownloaded(n64)
if single {
wer.downloadStatus.AddTotalSize(n64)
}
}
if readErr != nil {
rlen := wer.wrange.Len()
switch {
case single && readErr == io.ErrUnexpectedEOF:
// 单线程判断下载成功
fallthrough
case readErr == io.EOF:
fallthrough
case rlen <= 0:
// 下载完成
// 小于0可能是因为 worker 被 duplicate
wer.status.statusCode = StatusCodeSuccessed
if rlen < 0 {
logger.Verbosef("DEBUG: RangeLen is negative at end: %v, %d\n", wer.wrange, wer.wrange.Len())
}
return
default:
// 其他错误, 返回
wer.status.statusCode = StatusCodeFailed
wer.err = readErr
return
}
}
}
}
}
+46
View File
@@ -0,0 +1,46 @@
// 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 downloader
import (
"io"
"os"
"runtime"
)
type (
// Fder 获取fd接口
Fder interface {
Fd() uintptr
}
// Writer 下载器数据输出接口
Writer interface {
io.WriterAt
}
)
// NewDownloaderWriterByFilename 创建下载器数据输出接口, 类似于os.OpenFile
func NewDownloaderWriterByFilename(name string, flag int, perm os.FileMode) (writer Writer, file *os.File, err error) {
if runtime.GOOS == "windows" {
// TODO: name进行特殊字符转换
}
file, err = os.OpenFile(name, flag, perm)
if err != nil {
return
}
writer = file
return
}

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