Compare commits

...

339 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
139 changed files with 13318 additions and 2648 deletions
+4 -1
View File
@@ -41,4 +41,7 @@ main_test.go
license_header.sh
account.txt
git_push.sh
git_pull.sh
git_pull.sh
docker/sync/data
clear_docker.sh
upload.sh
+272 -701
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity version="0.1.3.0" processorArchitecture="*" name="com.tickstep.aliyunpan" type="win32"/>
<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="*"/>
Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 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"
+3
View File
@@ -1,5 +1,8 @@
@echo off
REM 配置环境变量
REM set ALIYUNPAN_CONFIG_DIR=d:\path\to\your\aliyunpan\config
REM 指定refresh token用于登录
aliyunpan login -RefreshToken=9078907....adg9087
+5
View File
@@ -1,6 +1,11 @@
#!/bin/sh
# 请更改成你自己电脑上aliyunpan执行文件所在的目录
#cd /path/to/aliyunpan/folder
# 配置环境变量
#export ALIYUNPAN_CONFIG_DIR=/path/to/your/aliyunpan/config
chmod +x ./aliyunpan
# 指定refresh token用于登录
@@ -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"
}
]
}
+32 -16
View File
@@ -75,8 +75,10 @@ Pack() {
fi
cp README.md "$output/$1"
cp webdav.sh "$output/$1"
cp webdav.bat "$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"
@@ -92,45 +94,59 @@ RicePack() {
return # 已取消web功能
}
# Android
############### Android ###############
export ANDROID_NDK_ROOT=/Users/tickstep/Applications/android_ndk/android-ndk-r23-darwin
CC=$ANDROID_NDK_ROOT/bin/arm-linux-androideabi/bin/clang AndroidBuild $name-$version"-android-api16-armv7" android arm 7
CC=$ANDROID_NDK_ROOT/bin/aarch64-linux-android/bin/clang AndroidBuild $name-$version"-android-api21-arm64" android arm64 7
CC=$ANDROID_NDK_ROOT/bin/i686-linux-android/bin/clang AndroidBuild $name-$version"-android-api16-386" android 386 7
CC=$ANDROID_NDK_ROOT/bin/x86_64-linux-android/bin/clang AndroidBuild $name-$version"-android-api21-amd64" android amd64 7
# iOS
############### iOS ###############
IOSBuild $name-$version"-ios-arm64"
# OS X / macOS
############### macOS ###############
Build $name-$version"-darwin-macos-amd64" darwin amd64
# Build $name-$version"-darwin-macos-386" darwin 386
Build $name-$version"-darwin-macos-arm64" darwin arm64
# Windows
############### Windows ###############
Build $name-$version"-windows-x86" windows 386
Build $name-$version"-windows-x64" windows amd64
Build $name-$version"-windows-arm" windows arm
# Linux
############### Linux ###############
# x64/x86
Build $name-$version"-linux-386" linux 386
Build $name-$version"-linux-amd64" linux amd64
# 龙芯 LoongArch
Build $name-$version"-linux-loong64" linux loong64
# arm
Build $name-$version"-linux-armv5" linux arm 5
Build $name-$version"-linux-armv7" linux arm 7
Build $name-$version"-linux-arm64" linux arm64
GOMIPS=softfloat Build $name-$version"-linux-mips" linux mips
Build $name-$version"-linux-mips64" linux mips64
GOMIPS=softfloat Build $name-$version"-linux-mipsle" linux mipsle
Build $name-$version"-linux-mips64le" linux mips64le
# Build $name-$version"-linux-ppc64" linux ppc64
# Build $name-$version"-linux-ppc64le" linux ppc64le
# Build $name-$version"-linux-s390x" linux s390x
# Others
# Build $name-$version"-solaris-amd64" solaris amd64
# mips
GOMIPS=softfloat Build $name-$version"-linux-mips64" linux mips64
GOMIPS=softfloat Build $name-$version"-linux-mips64le" linux mips64le
GOMIPS=hardfloat Build $name-$version"-linux-mips64hf" linux mips64
GOMIPS=hardfloat Build $name-$version"-linux-mips64lehf" linux mips64le
GOMIPS=softfloat Build $name-$version"-linux-mips" linux mips
GOMIPS=softfloat Build $name-$version"-linux-mipsle" linux mipsle
GOMIPS=hardfloat Build $name-$version"-linux-mipshf" linux mips
GOMIPS=hardfloat Build $name-$version"-linux-mipslehf" linux mipsle
# freebsd
Build $name-$version"-freebsd-386" freebsd 386
Build $name-$version"-freebsd-amd64" freebsd amd64
# Build $name-$version"-freebsd-arm" freebsd arm
# Others
# Build $name-$version"-linux-ppc64" linux ppc64
# Build $name-$version"-linux-ppc64le" linux ppc64le
# Build $name-$version"-linux-s390x" linux s390x
# Build $name-$version"-solaris-amd64" solaris amd64
# Build $name-$version"-netbsd-386" netbsd 386
# Build $name-$version"-netbsd-amd64" netbsd amd64
# Build $name-$version"-netbsd-arm" netbsd arm
-98
View File
@@ -1,39 +1,11 @@
package cmder
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder/cmdliner"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"sync"
)
var (
appInstance *cli.App
saveConfigMutex *sync.Mutex = new(sync.Mutex)
ReloadConfigFunc = func(c *cli.Context) error {
err := config.Config.Reload()
if err != nil {
fmt.Printf("重载配置错误: %s\n", err)
}
return nil
}
SaveConfigFunc = func(c *cli.Context) error {
saveConfigMutex.Lock()
defer saveConfigMutex.Unlock()
err := config.Config.Save()
if err != nil {
fmt.Printf("保存配置错误: %s\n", err)
}
return nil
}
)
func SetApp(app *cli.App) {
@@ -43,73 +15,3 @@ func SetApp(app *cli.App) {
func App() *cli.App {
return appInstance
}
func DoLoginHelper(refreshToken string) (refreshTokenStr string, webToken aliyunpan.WebLoginToken, error error) {
line := cmdliner.NewLiner()
defer line.Close()
if refreshToken == "" {
refreshToken, error = line.State.Prompt("请输入RefreshToken, 回车键提交 > ")
if error != nil {
return
}
}
// app login
atoken, apperr := aliyunpan.GetAccessTokenFromRefreshToken(refreshToken)
if apperr != nil {
if apperr.Code == apierror.ApiCodeTokenExpiredCode || apperr.Code == apierror.ApiCodeRefreshTokenExpiredCode {
fmt.Println("Token过期,需要重新登录")
} else {
fmt.Println("Token登录失败:", apperr)
}
return "", webToken, fmt.Errorf("登录失败")
}
refreshTokenStr = refreshToken
return refreshTokenStr, *atoken, nil
}
func TryLogin() *config.PanUser {
// can do automatically login?
for _, u := range config.Config.UserList {
if u.UserId == config.Config.ActiveUID {
// login
_, webToken, err := DoLoginHelper(u.RefreshToken)
if err != nil {
logger.Verboseln("automatically login 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)
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
}
break
}
// success
u.WebToken = webToken
// save
SaveConfigFunc(nil)
// reload
ReloadConfigFunc(nil)
return config.Config.ActiveUser()
}
}
return nil
}
+5
View File
@@ -123,3 +123,8 @@ func WalkDir(dirPth, suffix string) (files []string, err error) {
func ConvertToUnixPathSeparator(p string) string {
return strings.Replace(p, "\\", "/", -1)
}
// ConvertToWindowsPathSeparator 将路径中的所有分隔符转换成windows格式
func ConvertToWindowsPathSeparator(p string) string {
return strings.Replace(p, "/", "\\", -1)
}
+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
+17 -2
View File
@@ -22,23 +22,38 @@ WORKDIR /home/app
RUN cd /home/app
# 创建配置文件目录
RUN mkdir /home/app/config
RUN mkdir -p /home/app/config
# 复制文件
COPY ./docker/app.sh /home/app/app.sh
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
+11 -1
View File
@@ -1,6 +1,16 @@
#!/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 ]
@@ -12,4 +22,4 @@ else
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}" -pan_drive "${ALIYUNPAN_PAN_DRIVE}" -pan_dir_path "${ALIYUNPAN_PAN_DIR}" -bs ${ALIYUNPAN_BLOCK_SIZE}
./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}
@@ -2,7 +2,7 @@ version: '3'
services:
webdav:
image: tickstep/aliyunpan-webdav:tag
image: tickstep/aliyunpan-webdav:v0.1.6
container_name: aliyunpan-webdav
restart: always
ports:
@@ -10,11 +10,13 @@ services:
environment:
- TZ=Asia/Shanghai
# refresh token
- ALIYUNPAN_REFRESH_TOKEN=b9123...13e66a1
- 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服务根目录
@@ -23,7 +25,8 @@ services:
- 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
-162
View File
@@ -1,162 +0,0 @@
#!/bin/sh
# how to use
# for macOS & linux, run this command in shell
# ./docker_build.sh v0.0.1
name="aliyunpan"
version=$1
docker_image_name=tickstep/aliyunpan-webdav
if [ "$1" = "" ]; then
version=v1.0.0
fi
# build amd64 docker
echo "building amd64 docker image"
ARCH=amd64
ZIP_FILE_NAME=aliyunpan-$version-linux-$ARCH
# use alpine:3.15.0 as base image
# browse the url to view more information: https://hub.docker.com/layers/alpine/library/alpine/3.15.0/images/sha256-c74f1b1166784193ea6c8f9440263b9be6cae07dfe35e32a5df7a31358ac2060?context=explore
DOCKER_IMAGE_HASH=e7d88de73db3d3fd9b2d63aa7f447a10fd0220b7cbf39803c803f2af9ba256b3
unzip -d ./out ./out/$ZIP_FILE_NAME.zip
mv ./out/$ZIP_FILE_NAME ./out/binary_files
docker build \
-f ./docker/Dockerfile \
-t $docker_image_name:$version-$ARCH \
--build-arg DOCKER_IMAGE_HASH=$DOCKER_IMAGE_HASH \
--no-cache .
rm -rf out/binary_files
# build i386 docker
echo "building i386 docker image"
ARCH=386
ZIP_FILE_NAME=aliyunpan-$version-linux-$ARCH
DOCKER_IMAGE_HASH=2689e157117d2da668ad4699549e55eba1ceb79cb7862368b30919f0488213f4
unzip -d ./out ./out/$ZIP_FILE_NAME.zip
mv ./out/$ZIP_FILE_NAME ./out/binary_files
docker build \
-f ./docker/Dockerfile \
-t $docker_image_name:$version-$ARCH \
--build-arg DOCKER_IMAGE_HASH=$DOCKER_IMAGE_HASH \
--no-cache .
rm -rf out/binary_files
# build arm64 docker
echo "building arm64 docker image"
ARCH=arm64
ZIP_FILE_NAME=aliyunpan-$version-linux-$ARCH
DOCKER_IMAGE_HASH=c74f1b1166784193ea6c8f9440263b9be6cae07dfe35e32a5df7a31358ac2060
unzip -d ./out ./out/$ZIP_FILE_NAME.zip
mv ./out/$ZIP_FILE_NAME ./out/binary_files
docker build \
-f ./docker/Dockerfile \
-t $docker_image_name:$version-$ARCH \
--build-arg DOCKER_IMAGE_HASH=$DOCKER_IMAGE_HASH \
--no-cache .
rm -rf out/binary_files
# build armv7 docker
echo "building armv7 docker image"
ARCH=armv7
ZIP_FILE_NAME=aliyunpan-$version-linux-$ARCH
DOCKER_IMAGE_HASH=8483ecd016885d8dba70426fda133c30466f661bb041490d525658f1aac73822
unzip -d ./out ./out/$ZIP_FILE_NAME.zip
mv ./out/$ZIP_FILE_NAME ./out/binary_files
docker build \
-f ./docker/Dockerfile \
-t $docker_image_name:$version-$ARCH \
--build-arg DOCKER_IMAGE_HASH=$DOCKER_IMAGE_HASH \
--no-cache .
rm -rf out/binary_files
# build armv5 docker
echo "building armv5 docker image"
ARCH=armv5
ZIP_FILE_NAME=aliyunpan-$version-linux-$ARCH
DOCKER_IMAGE_HASH=e047bc2af17934d38c5a7fa9f46d443f1de3a7675546402592ef805cfa929f9d
unzip -d ./out ./out/$ZIP_FILE_NAME.zip
mv ./out/$ZIP_FILE_NAME ./out/binary_files
docker build \
-f ./docker/Dockerfile \
-t $docker_image_name:$version-$ARCH \
--build-arg DOCKER_IMAGE_HASH=$DOCKER_IMAGE_HASH \
--no-cache .
rm -rf out/binary_files
echo "push docker images"
docker push $docker_image_name:$version-amd64
docker push $docker_image_name:$version-386
docker push $docker_image_name:$version-arm64
docker push $docker_image_name:$version-armv7
docker push $docker_image_name:$version-armv5
echo "create docker manifest"
docker manifest create $docker_image_name:$version \
$docker_image_name:$version-amd64 \
$docker_image_name:$version-386 \
$docker_image_name:$version-arm64 \
$docker_image_name:$version-armv7 \
$docker_image_name:$version-armv5 \
--amend
echo "annotate manifest for amd64 image"
docker manifest annotate \
--arch=amd64 \
--os=linux \
$docker_image_name:$version \
$docker_image_name:$version-amd64
echo "annotate manifest for 386 image"
docker manifest annotate \
--arch=386 \
--os=linux \
$docker_image_name:$version \
$docker_image_name:$version-386
echo "annotate manifest for arm64 image"
docker manifest annotate \
--arch=arm64 \
--os=linux \
--variant=v8 \
$docker_image_name:$version \
$docker_image_name:$version-arm64
echo "annotate manifest for armv7 image"
docker manifest annotate \
--arch=arm \
--os=linux \
--variant=v7 \
$docker_image_name:$version \
$docker_image_name:$version-armv7
echo "annotate manifest for armv5 image"
docker manifest annotate \
--arch=arm \
--os=linux \
--variant=v6 \
$docker_image_name:$version \
$docker_image_name:$version-armv5
echo "push manifest to docker hub"
docker manifest push $docker_image_name:$version
echo "clear local docker image"
docker rmi $docker_image_name:$version-amd64
docker rmi $docker_image_name:$version-386
docker rmi $docker_image_name:$version-arm64
docker rmi $docker_image_name:$version-armv7
docker rmi $docker_image_name:$version-armv5
echo "ALL DONE"
+1253
View File
File diff suppressed because it is too large Load Diff
+25 -6
View File
@@ -1,23 +1,42 @@
module github.com/tickstep/aliyunpan
go 1.16
go 1.20
require (
github.com/GeertJohan/go.incremental v1.0.0
github.com/json-iterator/go v1.1.10
github.com/deckarep/golang-set v1.8.0
github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e
github.com/json-iterator/go v1.1.12
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce
github.com/peterh/liner v1.2.1
github.com/tickstep/aliyunpan-api v0.0.5
github.com/tickstep/bolt v1.3.3
github.com/tickstep/library-go v0.0.5
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
+78 -73
View File
@@ -1,51 +1,93 @@
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/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.1 h1:CnwP9LM/M9xuRrGSCGeMVs9iv09uMqwsVX7EeIpgV2c=
github.com/btcsuite/btcd v0.22.1/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4=
github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1 h1:9/4Hyp+A98nqssD7qceXwoLeAn75MZr25IN76dxlfbg=
github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1/go.mod h1:TQJQ+ZNyFVvUtUEtCZxBhfWiH7RJqR3EivNmvD6Waik=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e h1:OGunVjqY7y4U4laftpEHv+mvZBlr7UGimJXKEGQtg48=
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/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/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb h1:x0yCvYsspui5SAxSRvLd2zFg7PfFijzKdCo7QAtN92I=
github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb/go.mod h1:ym0w0flrmBtGvApLDgFLa0sfGJkWxDQqnm0/0ok5w3Y=
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce h1:RLmZmfx/K62HKpbwPqtW3tg+V2GgugN/XNNx+uiMH/Y=
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
github.com/peterh/liner v1.1.1-0.20190305032635-6f820f8f90ce h1:Lz+a/i+oS4A7tb6J6IyH4ZFiWgqvNv2yslv0Qn79wok=
github.com/peterh/liner v1.1.1-0.20190305032635-6f820f8f90ce/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/peterh/liner v1.2.1 h1:O4BlKaq/LWu6VRWmol4ByWfzx6MfXc5Op5HETyIy5yg=
github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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=
@@ -53,88 +95,51 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tickstep/aliyunpan-api v0.0.2 h1:aXChXeb1vmpINqV2qf60Bi3SGKgZPdmV7D8Eymb29Yw=
github.com/tickstep/aliyunpan-api v0.0.2/go.mod h1:EdciagdVeAHfqvzStVEVp1UVtySaq1DFd5RSucDfKWY=
github.com/tickstep/aliyunpan-api v0.0.3 h1:DBxRIjRUuebIjAgAPtS7rYZ4tafzOEF6J2949V2FeDY=
github.com/tickstep/aliyunpan-api v0.0.3/go.mod h1:EdciagdVeAHfqvzStVEVp1UVtySaq1DFd5RSucDfKWY=
github.com/tickstep/aliyunpan-api v0.0.4 h1:+ct8x6F6vpkezi3EMLb8zDBoWdQBA+BFuzWiYWf8q1s=
github.com/tickstep/aliyunpan-api v0.0.4/go.mod h1:EdciagdVeAHfqvzStVEVp1UVtySaq1DFd5RSucDfKWY=
github.com/tickstep/aliyunpan-api v0.0.5 h1:41eCJ1iGMtj2TXZGdrgv66RRcZpWpWq19yk8jOOpqdE=
github.com/tickstep/aliyunpan-api v0.0.5/go.mod h1:EdciagdVeAHfqvzStVEVp1UVtySaq1DFd5RSucDfKWY=
github.com/tickstep/bolt v1.3.3 h1:3abb88T5JJocnBlYTJE3EqKvwWV6+D/JZD2Fsdt1QHI=
github.com/tickstep/bolt v1.3.3/go.mod h1:Y24RwCywbOsKAyHiVcJ4K2QQfED+q67x7PEOH9OG7Q4=
github.com/tickstep/cloudpan189-api v0.0.3 h1:L8woe6XSUjQOsa7XRC7zH8VZj8SNKo/1qQb1k2ttLkU=
github.com/tickstep/cloudpan189-api v0.0.3/go.mod h1:1H1r6h5fOu8sEhPY6h09YTuSt4OKl5uFJchFscBFRwo=
github.com/tickstep/cloudpan189-api v0.0.4 h1:qsEhqvL2L5P6EHP+vfLhVqyjJbcHUkE9JEfUiyu1z0g=
github.com/tickstep/cloudpan189-api v0.0.4/go.mod h1:1H1r6h5fOu8sEhPY6h09YTuSt4OKl5uFJchFscBFRwo=
github.com/tickstep/cloudpan189-api v0.0.5 h1:R46M4op2aABKJgR2S8c/KUe/fIQYbUpQGxlJ9SUnsBo=
github.com/tickstep/cloudpan189-api v0.0.5/go.mod h1:HYu2wtfBDRldAnohj9UmNpEZipQDMm2wOYszNuLzZ9s=
github.com/tickstep/cloudpan189-api v0.0.6 h1:b+ctfvWfOVyq/Zot3rCrJxqf85b+toDSNLeGon4OWQo=
github.com/tickstep/cloudpan189-api v0.0.6/go.mod h1:qhNVXxF1UGOApXy9uG+UuKHReFI1GpwHn0pzL4tqSd8=
github.com/tickstep/library-go v0.0.1 h1:UbXeGE6ZxnxA6KTjMofEhG3h2aHJ4UGacfh7U2B4zgw=
github.com/tickstep/library-go v0.0.1/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
github.com/tickstep/library-go v0.0.2 h1:0JCxT2ZzRMrydUyqou3d9FI44ULrlxnkFcyioaGUbrE=
github.com/tickstep/library-go v0.0.2/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
github.com/tickstep/library-go v0.0.3 h1:j6C2qIYMC33Re/qa3cYS49DSMalgjtLR4KqGi6d1qBI=
github.com/tickstep/library-go v0.0.3/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
github.com/tickstep/library-go v0.0.4 h1:wNR48yDlaikxdJokL/NYX0J4ul8NN+TmBfXjxO2kfNw=
github.com/tickstep/library-go v0.0.4/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
github.com/tickstep/library-go v0.0.5 h1:MBb1tsvs4Wi67zy0E9eobVWLgsfPRLsqKAEdSEi3LBE=
github.com/tickstep/library-go v0.0.5/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
github.com/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=
github.com/xujiajun/gorouter v1.2.0/go.mod h1:yJrIta+bTNpBM/2UT8hLOaEAFckO+m/qmR3luMIQygM=
github.com/xujiajun/mmap-go v1.0.1 h1:7Se7ss1fLPPRW+ePgqGpCkfGIZzJV6JPq9Wq9iv/WHc=
github.com/xujiajun/mmap-go v1.0.1/go.mod h1:CNN6Sw4SL69Sui00p0zEzcZKbt+5HtEnYUsc6BKKRMg=
github.com/xujiajun/nutsdb v0.5.1-0.20200830145825-432dd3d0c873 h1:pgTLmYRrfy9lGWgXrEqaIhn4WSJb9xA5Q+Uir2Dxeag=
github.com/xujiajun/nutsdb v0.5.1-0.20200830145825-432dd3d0c873/go.mod h1:Q8FXi2zeQRluPpUl/CKQ6J7u/9gcI02J6cZp3owFLyA=
github.com/xujiajun/utils v0.0.0-20190123093513-8bf096c4f53b h1:jKG9OiL4T4xQN3IUrhUpc1tG+HfDXppkgVcrAiiaI/0=
github.com/xujiajun/utils v0.0.0-20190123093513-8bf096c4f53b/go.mod h1:AZd87GYJlUzl82Yab2kTjx1EyXSQCAfZDhpTo1SQC4k=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
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/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 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=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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.2.8/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=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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()
}
}
-303
View File
@@ -1,303 +0,0 @@
// Copyright (c) 2020 tickstep
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/functions/panupload"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"os"
"path"
"path/filepath"
"strings"
"sync"
)
func CmdBackup() cli.Command {
return cli.Command{
Name: "backup",
Description: `备份指定 <文件/目录> 到云盘 <目标目录> 中
和上传的功能一样,只是备份多进行了如下操作
1. 增加了数据库,记录已经上传的文件信息。
目前只记录 文件位置、大小、修改时间、MD5 。
2. 上传前先根据数据库记录判断是否需要重新上传。
3. 强制同名覆盖。
注:只备份(上传)新的文件(同名覆盖),不处理删除操作。
示例:
1. 将本地的 C:\Users\Administrator\Video 整个目录备份到网盘 /视频 目录
注意区别反斜杠 "\" 和 斜杠 "/" !!!
aliyunpan-go backup C:/Users/Administrator/Video /视频
2. 将本地的 C:\Users\Administrator\Video 整个目录备份到网盘 /视频 目录,但是排除所有的.jpg文件
aliyunpan-go backup -exn "\.jpg$" C:/Users/Administrator/Video /视频
3. 将本地的 C:\Users\Administrator\Video 整个目录备份到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
aliyunpan-go backup -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视频
4. 将本地的 C:\Users\Administrator\Video 整个目录备份到网盘 /视频 目录,但是排除所有的 @eadir 文件夹
aliyunpan-go backup -exn "^@eadir$" C:/Users/Administrator/Video /视频
参考:
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式。在正则表达式中,^表示匹配开头,$表示匹配结尾。
1)排除@eadir文件或者文件夹:-exn "^@eadir$"
2)排除.jpg文件:-exn "\.jpg$"
3)排除.号开头的文件:-exn "^\."
4)排除~号开头的文件:-exn "^~"
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
`,
Usage: "备份文件或目录",
UsageText: "backup <文件/目录路径1> <文件/目录2> <文件/目录3> ... <目标目录>",
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Action: Backup,
Flags: append(UploadFlags, cli.BoolFlag{
Name: "delete",
Usage: "通过本地数据库记录同步删除网盘文件",
}, cli.BoolFlag{
Name: "sync",
Usage: "本地同步到网盘(会同步删除网盘文件)",
}),
}
}
func OpenSyncDb(path string) (panupload.SyncDb, error) {
return panupload.OpenSyncDb(path, BackupMetaBucketName)
}
// 删除那些本地不存在而网盘存在的网盘文件 默认使用本地数据库判断,如果 flagSync 为 true 则遍历网盘文件列表进行判断(速度较慢)。
func DelRemoteFileFromDB(driveId string, localDir string, savePath string, flagSync bool) {
activeUser := config.Config.ActiveUser()
var db panupload.SyncDb
var err error
dbpath := filepath.Join(localDir, BackupMetaDirName)
db, err = OpenSyncDb(dbpath + string(os.PathSeparator) + "db")
if err != nil {
fmt.Println("同步数据库打开失败!", err)
return
}
defer db.Close()
savePath = path.Join(savePath, filepath.Base(localDir))
//判断本地文件是否存在,如果存在返回 true 否则删除数据库相关记录和网盘上的文件。
isLocalFileExist := func(ent *panupload.UploadedFileMeta) (isExists bool) {
testPath := strings.TrimPrefix(ent.Path, savePath)
testPath = filepath.Join(localDir, testPath)
logger.Verboseln("同步删除检测:", testPath, ent.Path)
//为防止误删,只有当 err 是文件不存在的时候才进行删除处理。
if fi, err := os.Stat(testPath); err == nil || !os.IsNotExist(err) {
//使用sync功能时没有传时间参数进来,为方便对比回写数据库需补上时间。
if fi != nil {
ent.ModTime = fi.ModTime().Unix()
}
return true
}
var err *apierror.ApiError
// 尝试从本地数据库查找
if ent.ParentId == "" {
if test := db.Get(path.Dir(ent.Path)); test != nil && test.IsFolder && test.FileId != "" {
ent.ParentId = test.FileId
}
}
// 从网盘查找
if ent.FileId == "" || ent.ParentId == "" {
efi, err := activeUser.PanClient().FileInfoById(driveId, ent.FileId)
//网盘上不存在这个文件或目录,只需要清理数据库
if err != nil && err.Code == apierror.ApiCodeFileNotFoundCode {
db.DelWithPrefix(ent.Path)
logger.Verboseln("删除数据库记录", ent.Path)
return
}
if efi != nil {
ent.FileId = efi.FileId
ent.ParentId = efi.ParentFileId
}
}
if ent.FileId == "" {
return
}
// 本地文件不存在
// 删除网盘对应文件
fileDeleteResult, err := activeUser.PanClient().FileDelete([]*aliyunpan.FileBatchActionParam{{DriveId:driveId, FileId:ent.FileId}})
if err != nil || len(fileDeleteResult) == 0 {
fmt.Println("删除网盘文件或目录失败", ent.Path, err)
} else {
db.DelWithPrefix(ent.Path)
logger.Verboseln("删除网盘文件和数据库记录", ent.Path)
}
return
}
// 根据数据库记录删除不存在的文件
if !flagSync {
for ent, err := db.First(savePath); err == nil; ent, err = db.Next(savePath) {
isLocalFileExist(ent)
}
return
}
parent := db.Get(savePath)
if parent.FileId == "" {
efi, err := activeUser.PanClient().FileInfoByPath(driveId, savePath)
if err != nil {
return
}
parent.FileId = efi.FileId
}
var syncFunc func(curPath, parentID string)
syncFunc = func(curPath, parentID string) {
param := &aliyunpan.FileListParam{
DriveId: driveId,
ParentFileId: parentID,
}
fileResult, err := activeUser.PanClient().FileListGetAll(param)
if err != nil {
return
}
if fileResult == nil || len(fileResult) == 0 {
return
}
for _, fileEntity := range fileResult {
ufm := &panupload.UploadedFileMeta{
FileId: fileEntity.FileId,
ParentId: fileEntity.ParentFileId,
Size: fileEntity.FileSize,
IsFolder: fileEntity.IsFolder(),
Path: path.Join(curPath, fileEntity.FileName),
SHA1: strings.ToLower(fileEntity.ContentHash),
}
if !isLocalFileExist(ufm) {
continue
}
//如果这是一个目录就直接更新数据库,否则判断原始记录的Hash信息,如果一致才更新。
if ufm.IsFolder {
db.Put(ufm.Path, ufm)
syncFunc(ufm.Path, ufm.FileId)
} else if test := db.Get(ufm.Path); test.SHA1 == ufm.SHA1 {
db.Put(ufm.Path, ufm)
}
}
}
//开启自动清理功能
db.AutoClean(parent.Path, true)
db.Put(parent.Path, parent)
syncFunc(savePath, parent.FileId)
}
func checkPath(localdir string) (string, error) {
fullPath, err := filepath.Abs(localdir)
if err != nil {
fullPath = localdir
}
if fi, err := os.Stat(fullPath); err != nil && !fi.IsDir() {
return fullPath, os.ErrInvalid
}
dbpath := filepath.Join(fullPath, BackupMetaDirName)
//数据库目录判断
fi, err := os.Stat(dbpath)
if err != nil {
if os.IsNotExist(err) {
err = os.Mkdir(dbpath, 0755)
}
if err != nil {
return fullPath, fmt.Errorf("数据库目录[%s]创建失败,跳过处理: %s", dbpath, err)
}
}
if fi != nil && !fi.IsDir() {
return fullPath, os.ErrPermission
}
return fullPath, nil
}
func Backup(c *cli.Context) error {
if c.NArg() < 2 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
subArgs := c.Args()
localpaths := make([]string, 0)
flagSync := c.Bool("sync")
flagDelete := c.Bool("delete")
opt := &UploadOptions{
AllParallel: c.Int("p"),
Parallel: 1, // 阿里云盘一个文件只支持单线程上传
MaxRetry: c.Int("retry"),
NoRapidUpload: c.Bool("norapid"),
ShowProgress: !c.Bool("np"),
IsOverwrite: true,
DriveId: parseDriveId(c),
ExcludeNames: c.StringSlice("exn"),
BlockSize: int64(c.Int("bs") * 1024),
}
localCount := c.NArg() - 1
savePath := GetActiveUser().PathJoin(opt.DriveId, subArgs[localCount])
wg := sync.WaitGroup{}
wg.Add(localCount)
for _, p := range subArgs[:localCount] {
go func(p string) {
defer wg.Done()
fullPath, err := checkPath(p)
switch err {
case nil:
if flagSync || flagDelete {
DelRemoteFileFromDB(opt.DriveId, fullPath, savePath, flagSync)
}
case os.ErrInvalid:
default:
return
}
localpaths = append(localpaths, fullPath)
}(p)
}
wg.Wait()
if len(localpaths) == 0 {
return nil
}
RunUpload(localpaths, savePath, opt)
return nil
}
+35 -9
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,6 +15,7 @@ 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"
@@ -33,14 +34,17 @@ func CmdCd() cli.Command {
切换 /我的资源 工作目录:
aliyunpan cd /我的资源
切换 /我的资源 工作目录,使用通配符:
aliyunpan cd /我的*
切换上级目录:
aliyunpan cd ..
切换根目录:
aliyunpan cd /
`,
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -69,7 +73,7 @@ func CmdPwd() cli.Command {
Usage: "输出工作目录",
UsageText: cmder.App().Name + " pwd",
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
@@ -78,6 +82,8 @@ func CmdPwd() cli.Command {
activeUser := config.Config.ActiveUser()
if activeUser.IsFileDriveActive() {
fmt.Println(activeUser.Workdir)
} else if activeUser.IsResourceDriveActive() {
fmt.Println(activeUser.ResourceWorkdir)
} else if activeUser.IsAlbumDriveActive() {
fmt.Println(activeUser.AlbumWorkdir)
}
@@ -90,24 +96,44 @@ func RunChangeDirectory(driveId, targetPath string) {
user := config.Config.ActiveUser()
targetPath = user.PathJoin(driveId, targetPath)
targetPathInfo, err := user.PanClient().FileInfoByPath(driveId, targetPath)
//targetPathInfo, err := user.PanClient().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", targetPath)
fmt.Printf("错误: %s 不是一个目录 (文件夹)\n", targetPathInfo.Path)
return
}
if user.IsFileDriveActive() {
user.Workdir = targetPath
user.Workdir = targetPathInfo.Path
user.WorkdirFileEntity = *targetPathInfo
} else if user.IsResourceDriveActive() {
user.ResourceWorkdir = targetPathInfo.Path
user.ResourceWorkdirFileEntity = *targetPathInfo
} else if user.IsAlbumDriveActive() {
user.AlbumWorkdir = targetPath
user.AlbumWorkdir = targetPathInfo.Path
user.AlbumWorkdirFileEntity = *targetPathInfo
}
fmt.Printf("改变工作目录: %s\n", targetPath)
fmt.Printf("改变工作目录: %s\n", targetPathInfo.Path)
}
+26 -7
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -97,7 +97,7 @@ func newRapidUploadItem(rapidUploadShareLink string) (*RapidUploadItem, error) {
item.FileSha1 = strings.TrimSpace(parts[1])
// size
if size,e := strconv.ParseInt(parts[2], 10, 64); e == nil{
if size, e := strconv.ParseInt(parts[2], 10, 64); e == nil {
item.FileSize = size
} else {
return nil, fmt.Errorf("文件大小错误: %s", rapidUploadShareLink)
@@ -131,7 +131,6 @@ func (r *RapidUploadItem) createRapidUploadLink(hideRelativePath bool) string {
p := r.FilePath
p = strings.ReplaceAll(p, "\\", "/")
fileName := path.Base(p)
dirPath := path.Dir(p)
@@ -160,10 +159,10 @@ func CmdConfig() cli.Command {
Usage: "显示和修改程序配置项",
Description: "显示和修改程序配置项",
Category: "配置",
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
fmt.Printf("----\n运行 %s config set 可进行设置配置\n\n当前配置:\n", cmder.App().Name)
fmt.Printf("----\n当前配置目录: %s\n运行 %s config set 可进行设置配置\n\n当前配置:\n", config.GetConfigDir(), cmder.App().Name)
config.Config.PrintTable()
return nil
},
@@ -227,6 +226,15 @@ func CmdConfig() cli.Command {
if c.IsSet("local_addrs") {
config.Config.SetLocalAddrs(c.String("local_addrs"))
}
if c.IsSet("ip_type") {
config.Config.SetPreferIPType(c.String("ip_type"))
}
if c.IsSet("file_record_config") {
config.Config.SetFileRecorderConfig(c.String("file_record_config"))
}
if c.IsSet("device_id") {
config.Config.SetDeviceId(c.String("device_id"))
}
err := config.Config.Save()
if err != nil {
@@ -277,13 +285,24 @@ func CmdConfig() cli.Command {
Name: "local_addrs",
Usage: "设置本地网卡地址, 多个地址用逗号隔开",
},
cli.StringFlag{
Name: "ip_type",
Usage: "设置域名解析IP优先类型",
},
cli.StringFlag{
Name: "file_record_config",
Usage: "设置是否开启上传、下载、同步文件的结果记录功能",
},
cli.StringFlag{
Name: "device_id",
Usage: "设置客户端ID,24位的字符串",
},
},
},
},
}
}
func CmdTool() cli.Command {
return cli.Command{
Name: "tool",
+140 -49
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -20,6 +20,7 @@ import (
"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"
@@ -28,8 +29,10 @@ import (
"github.com/tickstep/library-go/requester/rio/speeds"
"github.com/urfave/cli"
"os"
"path"
"path/filepath"
"runtime"
"sync/atomic"
"time"
)
@@ -45,8 +48,9 @@ type (
MaxRetry int
NoCheck bool
ShowProgress bool
DriveId string
UseInternalUrl bool // 是否使用内置链接
DriveId string
UseInternalUrl bool // 是否使用内置链接
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
}
// LocateDownloadOption 获取下载链接可选参数
@@ -70,13 +74,10 @@ func CmdDownload() cli.Command {
Usage: "下载文件/目录",
UsageText: cmder.App().Name + " download <文件/目录路径1> <文件/目录2> <文件/目录3> ...",
Description: `
下载的文件默认保存到, 程序所在目录的 download/ 目录.
通过 aliyunpan config set -savedir <savedir>, 自定义保存的目录.
支持多个文件或目录下载.
自动跳过下载重名的文件!
下载的文件默认保存到, 程序所在目录的 download/ 目录。支持软链接文件,包括Linux/macOS(ln命令)和Windows(mklink命令)创建的符号链接文件。
通过 aliyunpan config set -savedir <savedir>, 自定义保存的目录。支持多个文件或目录下载,支持自动跳过下载重名的文件!
示例:
设置保存目录, 保存到 D:\Downloads
注意区别反斜杠 "\" 和 斜杠 "/" !!!
aliyunpan config set -savedir D:\\Downloads
@@ -84,16 +85,30 @@ func CmdDownload() cli.Command {
aliyunpan config set -savedir D:/Downloads
下载 /我的资源/1.mp4
aliyunpan d /我的资源/1.mp4
aliyunpan download /我的资源/1.mp4
下载 /我的资源 目录下面所有的mp4文件,使用通配符
aliyunpan download /我的资源/*.mp4
下载 /我的资源 整个目录!!
aliyunpan d /我的资源
aliyunpan download /我的资源
下载 /我的资源/1.mp4 并保存下载的文件到本地的 d:/panfile
aliyunpan d --saveto d:/panfile /我的资源/1.mp4
下载 /我的资源 整个目录,但是排除所有的jpg文件
aliyunpan download -exn "\.jpg$" /我的资源
下载 /我的资源/1.mp4 并保存下载的文件到本地的 d:/panfile
aliyunpan download --saveto d:/panfile /我的资源/1.mp4
参考:
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式。在正则表达式中,^表示匹配开头,$表示匹配结尾。
1)排除@eadir文件或者文件夹:-exn "^@eadir$"
2)排除.jpg文件:-exn "\.jpg$"
3)排除.号开头的文件:-exn "^\."
4)排除~号开头的文件:-exn "^~"
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -105,7 +120,9 @@ func CmdDownload() cli.Command {
saveTo string
)
if c.Bool("save") {
saveTo = "."
// 使用当前工作目录
pwd, _ := os.Getwd()
saveTo = path.Clean(pwd)
} else if c.String("saveto") != "" {
saveTo = filepath.Clean(c.String("saveto"))
}
@@ -120,10 +137,24 @@ func CmdDownload() cli.Command {
MaxRetry: c.Int("retry"),
NoCheck: c.Bool("nocheck"),
ShowProgress: !c.Bool("np"),
DriveId: parseDriveId(c),
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{
@@ -149,7 +180,7 @@ func CmdDownload() cli.Command {
},
cli.IntFlag{
Name: "p",
Usage: "指定同时进行下载文件的数量",
Usage: "指定同时进行下载文件的数量(取值范围:1 ~ 20)",
},
cli.IntFlag{
Name: "retry",
@@ -169,6 +200,11 @@ func CmdDownload() cli.Command {
Usage: "网盘ID",
Value: "",
},
cli.StringSliceFlag{
Name: "exn",
Usage: "exclude name,指定排除的文件夹或者文件的名称,被排除的文件不会进行下载,只支持正则表达式。支持同时排除多个名称,每一个名称就是一个exn参数",
Value: nil,
},
},
}
}
@@ -183,15 +219,23 @@ func downloadPrintFormat(load int) string {
// RunDownload 执行下载网盘内文件
func RunDownload(paths []string, options *DownloadOptions) {
activeUser := GetActiveUser()
activeUser.PanClient().EnableCache()
activeUser.PanClient().ClearCache()
defer activeUser.PanClient().DisableCache()
// pan token expired checker
go func() {
for {
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) {
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
logger.Verboseln("update access token for download task")
}
}
}()
}(&continueFlag)
if options == nil {
options = &DownloadOptions{}
@@ -213,8 +257,9 @@ func RunDownload(paths []string, options *DownloadOptions) {
BlockSize: MaxDownloadRangeSize,
MaxRate: config.Config.MaxDownloadRate,
InstanceStateStorageFormat: downloader.InstanceStateStorageFormatJSON,
ShowProgress: options.ShowProgress,
UseInternalUrl: config.Config.TransferUrlType == 2,
ShowProgress: options.ShowProgress,
UseInternalUrl: config.Config.TransferUrlType == 2,
ExcludeNames: options.ExcludeNames,
}
if cfg.CacheSize == 0 {
cfg.CacheSize = int(DownloadCacheSize)
@@ -227,13 +272,34 @@ func RunDownload(paths []string, options *DownloadOptions) {
options.Parallel = config.DefaultFileDownloadParallelNum
}
}
if options.Parallel > config.MaxFileDownloadParallelNum {
options.Parallel = config.MaxFileDownloadParallelNum
}
paths, err := matchPathByShellPattern(options.DriveId, paths...)
// 保存文件的本地根文件夹
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 (
@@ -253,36 +319,61 @@ func RunDownload(paths []string, options *DownloadOptions) {
// 全局速度统计
globalSpeedsStat := &speeds.Speeds{}
// 下载记录器
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/download_file_records.csv")
// 处理队列
for k := range paths {
newCfg := *cfg
unit := pandownload.DownloadTaskUnit{
Cfg: &newCfg, // 复制一份新的cfg
PanClient: panClient,
VerbosePrinter: panCommandVerbose,
PrintFormat: downloadPrintFormat(options.Load),
ParentTaskExecutor: &executor,
DownloadStatistic: statistic,
IsPrintStatus: options.IsPrintStatus,
IsExecutedPermission: options.IsExecutedPermission,
IsOverwrite: options.IsOverwrite,
NoCheck: options.NoCheck,
FilePanPath: paths[k],
DriveId: options.DriveId,
GlobalSpeedsStat: globalSpeedsStat,
// 使用通配符匹配
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 options.SaveTo != "" {
unit.OriginSaveRootPath = options.SaveTo
unit.SavePath = filepath.Join(options.SaveTo, filepath.Base(paths[k]))
} else {
// 使用默认的保存路径
unit.OriginSaveRootPath = GetActiveUser().GetSavePath("")
unit.SavePath = GetActiveUser().GetSavePath(paths[k])
// 是否排除下载
if utils.IsExcludeFile(f.Path, &newCfg.ExcludeNames) {
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)
}
info := executor.Append(&unit, options.MaxRetry)
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), paths[k])
}
// 开始计时
+16 -12
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,7 +17,6 @@ import (
"fmt"
"github.com/olekukonko/tablewriter"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
@@ -28,9 +27,9 @@ import (
func CmdDrive() cli.Command {
return cli.Command{
Name: "drive",
Usage: "切换网盘(文件网盘/相册网盘)",
Usage: "切换网盘(备份盘/资源库/相册)",
Description: `
切换已登录的阿里云盘的云工作网盘(文件网盘/相册网盘)
切换已登录账号的阿里云盘的工作网盘(备份盘/资源库/相册)
如果运行该条命令没有提供参数, 程序将会列出所有的网盘列表, 供选择切换.
示例:
@@ -38,8 +37,8 @@ func CmdDrive() cli.Command {
aliyunpan drive <driveId>
`,
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
inputData := c.Args().Get(0)
targetDriveId := strings.TrimSpace(inputData)
@@ -49,10 +48,10 @@ func CmdDrive() cli.Command {
}
}
func RunSwitchDriveList(targetDriveId string) {
func RunSwitchDriveList(targetDriveId string) {
currentDriveId := config.Config.ActiveUser().ActiveDriveId
var activeDriveInfo *config.DriveInfo = nil
driveList,renderStr := getDriveOptionList()
driveList, renderStr := getDriveOptionList()
if driveList == nil || len(driveList) == 0 {
fmt.Println("切换网盘失败")
@@ -71,15 +70,15 @@ func RunSwitchDriveList(targetDriveId string) {
return
}
if n, err := strconv.Atoi(index); err == nil && n >= 0 && n < len(driveList) {
activeDriveInfo = driveList[n]
if n, err1 := strconv.Atoi(index); err1 == nil && (n-1) >= 0 && (n-1) < len(driveList) {
activeDriveInfo = driveList[n-1]
} else {
fmt.Printf("切换网盘失败, 请检查 # 值是否正确\n")
return
}
} else {
// 直接切换
for _,driveInfo := range driveList {
for _, driveInfo := range driveList {
if driveInfo.DriveId == targetDriveId {
activeDriveInfo = driveInfo
break
@@ -101,6 +100,11 @@ func RunSwitchDriveList(targetDriveId string) {
config.Config.ActiveUser().Workdir = "/"
config.Config.ActiveUser().WorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
}
} else if activeUser.IsResourceDriveActive() {
if activeUser.ResourceWorkdir == "" {
config.Config.ActiveUser().ResourceWorkdir = "/"
config.Config.ActiveUser().ResourceWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
}
} else if activeUser.IsAlbumDriveActive() {
if activeUser.AlbumWorkdir == "" {
config.Config.ActiveUser().AlbumWorkdir = "/"
@@ -121,7 +125,7 @@ func getDriveOptionList() (config.DriveInfoList, string) {
tb.SetHeader([]string{"#", "drive_id", "网盘名称"})
for k, info := range driveList {
tb.Append([]string{strconv.Itoa(k), info.DriveId, info.DriveName})
tb.Append([]string{strconv.Itoa(k + 1), info.DriveId, info.DriveName})
}
tb.Render()
return driveList, builder.String()
+4 -6
View File
@@ -28,7 +28,6 @@ import (
"time"
)
func CmdExport() cli.Command {
return cli.Command{
Name: "export",
@@ -50,7 +49,7 @@ func CmdExport() cli.Command {
aliyunpan export / /Users/tickstep/Downloads/export_files.txt
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() < 2 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -75,12 +74,11 @@ func CmdExport() cli.Command {
}
}
func RunExportFiles(driveId string, overwrite bool, panPaths []string, saveLocalFilePath string) {
activeUser := config.Config.ActiveUser()
panClient := activeUser.PanClient()
lfi,_ := os.Stat(saveLocalFilePath)
lfi, _ := os.Stat(saveLocalFilePath)
realSaveFilePath := saveLocalFilePath
if lfi != nil {
if lfi.IsDir() {
@@ -94,7 +92,7 @@ func RunExportFiles(driveId string, overwrite bool, panPaths []string, saveLocal
} else {
// create file
localDir := path.Dir(saveLocalFilePath)
dirFs,_ := os.Stat(localDir)
dirFs, _ := os.Stat(localDir)
if dirFs != nil {
if !dirFs.IsDir() {
fmt.Println("指定的保存文件路径不合法")
@@ -117,7 +115,7 @@ func RunExportFiles(driveId string, overwrite bool, panPaths []string, saveLocal
return
}
for _,panPath := range panPaths {
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 {
+15 -15
View File
@@ -32,7 +32,7 @@ import (
type (
dirFileListData struct {
Dir *aliyunpan.MkdirResult
Dir *aliyunpan.MkdirResult
FileList aliyunpan.FileList
}
)
@@ -64,7 +64,7 @@ func CmdImport() cli.Command {
aliyunpan import -saveto=/ /Users/tickstep/Downloads/export_files.txt
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -99,7 +99,7 @@ func CmdImport() cli.Command {
}
func RunImportFiles(driveId string, overwrite bool, panSavePath, localFilePath string) {
lfi,_ := os.Stat(localFilePath)
lfi, _ := os.Stat(localFilePath)
if lfi != nil {
if lfi.IsDir() {
fmt.Println("请指定导入文件")
@@ -125,7 +125,7 @@ func RunImportFiles(driveId string, overwrite bool, panSavePath, localFilePath s
}
defer importFile.Close()
fileData,err := ioutil.ReadAll(importFile)
fileData, err := ioutil.ReadAll(importFile)
if err != nil {
fmt.Println("读取文件出错")
return
@@ -138,9 +138,9 @@ func RunImportFiles(driveId string, overwrite bool, panSavePath, localFilePath s
fileText = strings.TrimSpace(fileText)
fileLines := strings.Split(fileText, "\n")
importFileItems := []RapidUploadItem{}
for _,line := range fileLines {
for _, line := range fileLines {
line = strings.TrimSpace(line)
if item,e := newRapidUploadItem(line); e != nil {
if item, e := newRapidUploadItem(line); e != nil {
fmt.Println(e)
continue
} else {
@@ -159,7 +159,7 @@ func RunImportFiles(driveId string, overwrite bool, panSavePath, localFilePath s
fmt.Println("正在导入...")
successImportFiles := []RapidUploadItem{}
failedImportFiles := []RapidUploadItem{}
for _,item := range importFileItems {
for _, item := range importFileItems {
fmt.Printf("正在处理导入: %s\n", item.FilePath)
result, abort := processOneImport(driveId, overwrite, dirMap, item)
if abort {
@@ -175,7 +175,7 @@ func RunImportFiles(driveId string, overwrite bool, panSavePath, localFilePath s
}
if len(failedImportFiles) > 0 {
fmt.Println("\n以下文件导入失败")
for _,f := range failedImportFiles {
for _, f := range failedImportFiles {
fmt.Printf("%s %s\n", f.FileSha1, f.FilePath)
}
fmt.Println("")
@@ -185,13 +185,13 @@ func RunImportFiles(driveId string, overwrite bool, panSavePath, localFilePath s
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)
panDir, fileName := path.Split(item.FilePath)
dataItem := dirMap[path.Dir(panDir)]
if isOverwrite {
// 标记覆盖旧同名文件
// 检查同名文件是否存在
var efi *aliyunpan.FileEntity = nil
for _,fileItem := range dataItem.FileList {
for _, fileItem := range dataItem.FileList {
if !fileItem.IsFolder() && fileItem.FileName == fileName {
efi = fileItem
break
@@ -201,8 +201,8 @@ func processOneImport(driveId string, isOverwrite bool, dirMap map[string]*dirFi
// existed, delete it
fdr, err := panClient.FileDelete([]*aliyunpan.FileBatchActionParam{
{
DriveId:driveId,
FileId:efi.FileId,
DriveId: driveId,
FileId: efi.FileId,
},
})
if err != nil || fdr == nil || !fdr[0].Success {
@@ -239,7 +239,7 @@ func processOneImport(driveId string, isOverwrite bool, dirMap map[string]*dirFi
func prepareMkdir(driveId string, importFileItems []RapidUploadItem) map[string]*dirFileListData {
panClient := config.Config.ActiveUser().PanClient()
resultMap := map[string]*dirFileListData{}
for _,item := range importFileItems {
for _, item := range importFileItems {
var apierr *apierror.ApiError
var rs *aliyunpan.MkdirResult
panDir := path.Dir(item.FilePath)
@@ -263,7 +263,7 @@ func prepareMkdir(driveId string, importFileItems []RapidUploadItem) map[string]
param := &aliyunpan.FileListParam{}
param.DriveId = driveId
param.ParentFileId = rs.FileId
allFileInfo, err1 := panClient.FileListGetAll(param)
allFileInfo, err1 := panClient.FileListGetAll(param, 0)
if err1 != nil {
logger.Verboseln("获取文件信息出错")
continue
@@ -274,4 +274,4 @@ func prepareMkdir(driveId string, importFileItems []RapidUploadItem) map[string]
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()
}
}
+20 -10
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,7 +16,6 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdliner"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
@@ -42,8 +41,8 @@ func CmdLogin() cli.Command {
aliyunpan login -QrCode
`,
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc, // 每次进行登录动作的时候需要调用刷新配置
After: cmder.SaveConfigFunc, // 登录完成需要调用保存配置
Before: ReloadConfigFunc, // 每次进行登录动作的时候需要调用刷新配置
After: SaveConfigFunc, // 登录完成需要调用保存配置
Action: func(c *cli.Context) error {
refreshTokenStr := ""
if refreshTokenStr == "" {
@@ -61,7 +60,7 @@ func CmdLogin() cli.Command {
return err
}
cloudUser, err := config.SetupUserByCookie(&webToken)
cloudUser, err := config.SetupUserByCookie(&webToken, config.Config.DeviceId, config.Config.DeviceName)
if cloudUser == nil {
fmt.Println("登录失败: ", err)
return nil
@@ -93,8 +92,8 @@ func CmdLogout() cli.Command {
Usage: "退出阿里帐号",
Description: "退出当前登录的帐号",
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.NumLogins() == 0 {
fmt.Println("未设置任何帐号, 不能退出")
@@ -105,6 +104,9 @@ func CmdLogout() cli.Command {
confirm string
activeUser = config.Config.ActiveUser()
)
if activeUser == nil {
return nil
}
if !c.Bool("y") {
fmt.Printf("确认退出当前帐号: %s ? (y/n) > ", activeUser.Nickname)
@@ -114,6 +116,12 @@ func CmdLogout() cli.Command {
}
}
// 云端注销登录
//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)
@@ -139,15 +147,16 @@ func RunLogin(useQrCodeLogin bool, refreshToken string) (tokenId, refreshTokenSt
fmt.Println("二维码登录错误:", err)
return "", "", aliyunpan.WebLoginToken{}, err
}
fmt.Printf("请在浏览器打开以下链接进行扫码登录,链接有效时间为5分钟\n%s\n\n", qrCodeUrlResult.TokenUrl)
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 {
for queryResult {
time.Sleep(3 * time.Second)
qr, er := h.GetQRCodeLoginResult(qrCodeUrlResult.TokenId)
if er != nil {
@@ -166,6 +175,7 @@ func RunLogin(useQrCodeLogin bool, refreshToken string) (tokenId, refreshTokenSt
line.State.Prompt("请在浏览器里面完成扫码登录,然后再按Enter键继续...")
if qrCodeLoginResult == nil {
queryResult = false
return "", "", aliyunpan.WebLoginToken{}, fmt.Errorf("二维码登录失败")
}
@@ -178,6 +188,6 @@ func RunLogin(useQrCodeLogin bool, refreshToken string) (tokenId, refreshTokenSt
tokenId = qrCodeUrlResult.TokenId
}
refreshTokenStr, webToken, error = cmder.DoLoginHelper(refreshToken)
refreshTokenStr, webToken, error = DoLoginHelper(refreshToken)
return
}
+32 -22
View File
@@ -21,7 +21,6 @@ import (
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/text"
"github.com/urfave/cli"
"os"
"strconv"
@@ -62,11 +61,14 @@ func CmdLs() cli.Command {
绝对路径
aliyunpan ls /我的资源
列出 我的资源 内的文件和目录,使用通配符
aliyunpan ls /我的*
详细列出 我的资源 内的文件和目录
aliyunpan ll /我的资源
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
@@ -138,15 +140,27 @@ func RunLs(driveId, targetPath string, lsOptions *LsOptions,
orderBy aliyunpan.FileOrderBy, orderDirection aliyunpan.FileOrderDirection) {
activeUser := config.Config.ActiveUser()
targetPath = activeUser.PathJoin(driveId, targetPath)
if targetPath[len(targetPath)-1] == '/' {
targetPath = text.Substr(targetPath, 0, len(targetPath)-1)
}
targetPathInfo, err := activeUser.PanClient().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
}
fileList := aliyunpan.FileList{}
fileListParam := &aliyunpan.FileListParam{}
@@ -155,16 +169,16 @@ func RunLs(driveId, targetPath string, lsOptions *LsOptions,
fileListParam.OrderBy = orderBy
fileListParam.OrderDirection = orderDirection
if targetPathInfo.IsFolder() {
fileResult, err := activeUser.PanClient().FileListGetAll(fileListParam)
if err != nil {
fmt.Println(err)
fileResult, err1 := activeUser.PanClient().FileListGetAll(fileListParam, 200)
if err1 != nil {
fmt.Println(err1)
return
}
fileList = fileResult
} else {
fileList = append(fileList, targetPathInfo)
}
renderTable(opLs, lsOptions.Total, targetPath, fileList)
renderTable(opLs, lsOptions.Total, targetPathInfo.Path, fileList)
}
func renderTable(op int, isTotal bool, path string, files aliyunpan.FileList) {
@@ -186,15 +200,15 @@ func renderTable(op int, isTotal bool, path string, files aliyunpan.FileList) {
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
for k, file := range files {
if file.IsFolder() {
tb.Append([]string{strconv.Itoa(k), file.FileId, "-", "-", "-", file.CreatedAt, file.UpdatedAt, file.FileName + aliyunpan.PathSeparator})
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, "-", "-", "-", file.CreatedAt, file.UpdatedAt, file.FileName + aliyunpan.PathSeparator})
continue
}
switch op {
case opLs:
tb.Append([]string{strconv.Itoa(k), file.FileId, converter.ConvertFileSize(file.FileSize, 2), file.ContentHash, strconv.FormatInt(file.FileSize, 10), file.CreatedAt, file.UpdatedAt, file.FileName})
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, converter.ConvertFileSize(file.FileSize, 2), file.ContentHash, strconv.FormatInt(file.FileSize, 10), file.CreatedAt, file.UpdatedAt, file.FileName})
case opSearch:
tb.Append([]string{strconv.Itoa(k), file.FileId, converter.ConvertFileSize(file.FileSize, 2), file.ContentHash, strconv.FormatInt(file.FileSize, 10), file.CreatedAt, file.UpdatedAt, file.Path})
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, converter.ConvertFileSize(file.FileSize, 2), file.ContentHash, strconv.FormatInt(file.FileSize, 10), file.CreatedAt, file.UpdatedAt, file.Path})
}
}
fN, dN = files.Count()
@@ -204,26 +218,22 @@ func renderTable(op int, isTotal bool, path string, files aliyunpan.FileList) {
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
for k, file := range files {
if file.IsFolder() {
tb.Append([]string{strconv.Itoa(k), "-", file.UpdatedAt, file.FileName + aliyunpan.PathSeparator})
tb.Append([]string{strconv.Itoa(k + 1), "-", file.UpdatedAt, file.FileName + aliyunpan.PathSeparator})
continue
}
switch op {
case opLs:
tb.Append([]string{strconv.Itoa(k), converter.ConvertFileSize(file.FileSize, 2), file.UpdatedAt, file.FileName})
tb.Append([]string{strconv.Itoa(k + 1), converter.ConvertFileSize(file.FileSize, 2), file.UpdatedAt, file.FileName})
case opSearch:
tb.Append([]string{strconv.Itoa(k), converter.ConvertFileSize(file.FileSize, 2), file.UpdatedAt, file.Path})
tb.Append([]string{strconv.Itoa(k + 1), converter.ConvertFileSize(file.FileSize, 2), file.UpdatedAt, file.Path})
}
}
fN, dN = files.Count()
tb.Append([]string{"", "总: " + converter.ConvertFileSize(files.TotalSize(), 2), "", fmt.Sprintf("文件总数: %d, 目录总数: %d", fN, dN)})
}
fmt.Printf("\n当前目录: %s\n", path)
fmt.Printf("----\n")
tb.Render()
if fN+dN >= 60 {
fmt.Printf("\n当前目录: %s\n", path)
}
fmt.Printf("----\n")
}
+5 -8
View File
@@ -20,7 +20,6 @@ import (
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
"strings"
)
func CmdMkdir() cli.Command {
@@ -29,7 +28,7 @@ func CmdMkdir() cli.Command {
Usage: "创建目录",
UsageText: cmder.App().Name + " mkdir <目录>",
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -44,8 +43,8 @@ func CmdMkdir() cli.Command {
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "familyId",
Usage: "家庭云ID",
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
},
@@ -55,11 +54,9 @@ func CmdMkdir() cli.Command {
func RunMkdir(driveId, name string) {
activeUser := GetActiveUser()
fullpath := activeUser.PathJoin(driveId, name)
pathSlice := strings.Split(fullpath, "/")
rs := &aliyunpan.MkdirResult{}
err := apierror.NewFailedApiError("")
rs, err = activeUser.PanClient().MkdirRecursive(driveId,"", "", 0, pathSlice)
rs, err = activeUser.PanClient().Mkdir(driveId, "", fullpath)
if err != nil {
fmt.Println("创建文件夹失败:" + err.Error())
@@ -74,4 +71,4 @@ func RunMkdir(driveId, name string) {
} else {
fmt.Println("创建文件夹失败: ", fullpath)
}
}
}
+45 -16
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,28 +16,33 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
"os"
"path"
"strconv"
)
func CmdMv() cli.Command {
return cli.Command{
Name: "mv",
Usage: "移动文件/目录",
UsageText: `移动:
UsageText: `
aliyunpan mv <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>`,
Description: `
注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败.
注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败。支持通配符匹配移动文件,通配符当前只能匹配文件名,不能匹配文件路径。
示例:
将 /我的资源/1.mp4 移动到 根目录 /
aliyunpan mv /我的资源/1.mp4 /
将 /我的资源 目录下所有的.png文件 移动到 /我的图片 目录下面,使用通配符匹配
aliyunpan mv /我的资源/*.png /我的图片
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() <= 1 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -47,7 +52,6 @@ func CmdMv() cli.Command {
fmt.Println("未登录账号")
return nil
}
RunMove(parseDriveId(c), c.Args()...)
return nil
},
@@ -66,7 +70,7 @@ func RunMove(driveId string, paths ...string) {
activeUser := GetActiveUser()
cacheCleanPaths := []string{}
opFileList, targetFile, _, err := getFileInfo(driveId, paths...)
if err != nil {
if err != nil {
fmt.Println(err)
return
}
@@ -83,20 +87,20 @@ func RunMove(driveId string, paths ...string) {
failedMoveFiles := []*aliyunpan.FileEntity{}
moveFileParamList := []*aliyunpan.FileMoveParam{}
fileId2FileEntity := map[string]*aliyunpan.FileEntity{}
for _,mfi := range opFileList {
for _, mfi := range opFileList {
fileId2FileEntity[mfi.FileId] = mfi
moveFileParamList = append(moveFileParamList,
&aliyunpan.FileMoveParam{
DriveId: driveId,
FileId: mfi.FileId,
ToDriveId: driveId,
DriveId: driveId,
FileId: mfi.FileId,
ToDriveId: driveId,
ToParentFileId: targetFile.FileId,
})
cacheCleanPaths = append(cacheCleanPaths, path.Dir(mfi.Path))
}
fmr,er := activeUser.PanClient().FileMove(moveFileParamList)
fmr, er := activeUser.PanClient().FileMove(moveFileParamList)
for _,rs := range fmr {
for _, rs := range fmr {
if !rs.Success {
failedMoveFiles = append(failedMoveFiles, fileId2FileEntity[rs.FileId])
}
@@ -104,13 +108,22 @@ func RunMove(driveId string, paths ...string) {
if len(failedMoveFiles) > 0 {
fmt.Println("以下文件移动失败:")
for _,f := range failedMoveFiles {
for _, f := range failedMoveFiles {
fmt.Println(f.FileName)
}
fmt.Println("")
}
if er == nil {
fmt.Println("操作成功, 已移动文件到目标目录: ", targetFile.Path)
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("无法移动文件,请稍后重试")
@@ -130,6 +143,22 @@ func getFileInfo(driveId string, paths ...string) (opFileList []*aliyunpan.FileE
return nil, nil, nil, fmt.Errorf("指定目标文件夹不存在")
}
opFileList, failedPaths, error = GetAppFileInfoByPaths(driveId, paths[:len(paths)-1]...)
for idx := 0; idx < (len(paths) - 1); idx++ {
absolutePath = path.Clean(activeUser.PathJoin(driveId, paths[idx]))
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
if err1 != nil {
failedPaths = append(failedPaths, absolutePath)
continue
}
if fileList == nil || len(fileList) == 0 {
// 文件不存在
failedPaths = append(failedPaths, absolutePath)
continue
}
for _, f := range fileList {
// 移动匹配的文件
opFileList = append(opFileList, f)
}
}
return
}
+4 -5
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,7 +15,6 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/converter"
"github.com/urfave/cli"
@@ -34,7 +33,7 @@ func CmdQuota() cli.Command {
Usage: "获取当前帐号空间配额",
Description: "获取网盘的总储存空间, 和已使用的储存空间",
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
@@ -42,7 +41,7 @@ func CmdQuota() cli.Command {
}
q, err := RunGetQuotaInfo()
if err == nil {
fmt.Printf("账号: %s, uid: %s, 个人空间总额: %s, 个人空间已使用: %s, 比率: %f%%\n",
fmt.Printf("账号: %s, uid: %s, 个人空间总额: %s, 个人空间已使用: %s, 比率: %.2f%%\n",
config.Config.ActiveUser().Nickname, config.Config.ActiveUser().UserId,
converter.ConvertFileSize(q.Quota, 2), converter.ConvertFileSize(q.UsedSize, 2),
100*float64(q.UsedSize)/float64(q.Quota))
@@ -59,6 +58,6 @@ func RunGetQuotaInfo() (quotaInfo *QuotaInfo, error error) {
}
return &QuotaInfo{
UsedSize: int64(user.UsedSize),
Quota: int64(user.TotalSize),
Quota: int64(user.TotalSize),
}, nil
}
+30 -38
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -24,6 +24,7 @@ import (
"github.com/urfave/cli"
"os"
"strconv"
"time"
)
func CmdRecycle() cli.Command {
@@ -45,7 +46,7 @@ func CmdRecycle() cli.Command {
aliyunpan recycle delete -all
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NumFlags() <= 0 || c.NArg() <= 0 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -150,7 +151,7 @@ func RunRecycleList(driveId string) {
fn = fn + "/"
fs = "-"
}
tb.Append([]string{strconv.Itoa(k), file.FileId, fn, fs, file.CreatedAt, file.UpdatedAt})
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, fn, fs, file.CreatedAt, file.UpdatedAt})
}
tb.Render()
@@ -161,7 +162,7 @@ func RunRecycleRestore(driveId string, fidStrList ...string) {
panClient := GetActivePanClient()
restoreFileList := []*aliyunpan.FileBatchActionParam{}
for _,fid := range fidStrList {
for _, fid := range fidStrList {
restoreFileList = append(restoreFileList, &aliyunpan.FileBatchActionParam{
DriveId: driveId,
FileId: fid,
@@ -190,7 +191,7 @@ func RunRecycleDelete(driveId string, fidStrList ...string) {
panClient := GetActivePanClient()
deleteFileList := []*aliyunpan.FileBatchActionParam{}
for _,fid := range fidStrList {
for _, fid := range fidStrList {
deleteFileList = append(deleteFileList, &aliyunpan.FileBatchActionParam{
DriveId: driveId,
FileId: fid,
@@ -218,39 +219,30 @@ func RunRecycleDelete(driveId string, fidStrList ...string) {
func RunRecycleClear(driveId string) {
panClient := GetActivePanClient()
for {
// get file list
fdl, err := panClient.RecycleBinFileListGetAll(&aliyunpan.RecycleBinFileListParam{
DriveId: driveId,
Limit: 100,
})
if err != nil {
logger.Verboseln(err)
break
}
if fdl == nil || len(fdl) == 0 {
break
}
// delete
deleteFileList := []*aliyunpan.FileBatchActionParam{}
for _,f := range fdl {
deleteFileList = append(deleteFileList, &aliyunpan.FileBatchActionParam{
DriveId: driveId,
FileId: f.FileId,
})
}
if len(deleteFileList) == 0 {
logger.Verboseln("没有需要删除的文件")
break
}
rbfr, err := panClient.RecycleBinFileDelete(deleteFileList)
if rbfr != nil && len(rbfr) > 0 {
logger.Verboseln("彻底删除文件成功")
}
// 提交清空回收站异步任务
r, err := panClient.RecycleBinFileClear(&aliyunpan.RecycleBinFileClearParam{
DriveId: driveId,
})
if err != nil {
logger.Verboseln(err)
fmt.Printf("当前无法清空回收站,请稍后重试\n")
}
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")
}
+178 -9
View File
@@ -15,14 +15,42 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apiutil"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
"path"
"regexp"
"sort"
"strconv"
"strings"
)
type (
fileItem struct {
file *aliyunpan.FileEntity
newFileName string
}
fileArray []*fileItem
)
func newFileItem(file *aliyunpan.FileEntity) *fileItem {
return &fileItem{
file: file,
newFileName: "",
}
}
func (x fileArray) Len() int {
return len(x)
}
func (x fileArray) Less(i, j int) bool {
return x[i].file.FileName < x[j].file.FileName
}
func (x fileArray) Swap(i, j int) {
x[i], x[j] = x[j], x[i]
}
func CmdRename() cli.Command {
return cli.Command{
Name: "rename",
@@ -31,18 +59,32 @@ func CmdRename() cli.Command {
aliyunpan rename <旧文件/目录名> <新文件/目录名>`,
Description: `
示例:
1. 将文件 1.mp4 重命名为 2.mp4
aliyunpan rename 1.mp4 2.mp4
将文件 1.mp4 重命名为 2.mp4
aliyunpan rename 1.mp4 2.mp4
2. 将文件 /test/1.mp4 重命名为 /test/2.mp4
要求必须是同一个文件目录内
aliyunpan rename /test/1.mp4 /test/2.mp4
将文件 /test/1.mp4 重命名为 /test/2.mp4
要求必须是同一个文件目录内
aliyunpan rename /test/1.mp4 /test/2.mp4
批量重命名,规则:rename [expression] [replacement] [file]
其中,
expression - 表达式,即文件名中需要去掉的部分,如果为*代表旧的文件名全部去掉
replacement - 需要替换的新文件名,如果包含 # 号会按顺序替换成数字编号
file - 文件匹配模式,指定需要查找的文件匹配字符串,支持通配符,例如:*.mp4 即当前目录下所有的mp4文件
3. 批量重命名,将当前目录下所有.mp4文件中包含的字符串 "我的视频" 全部改成 "视频"
aliyunpan rename 我的视频 视频 *.mp4
4. 批量重命名,将当前目录下所有.mp4文件全部进行 "视频+编号.mp4" 的重命名操作,旧的名称全部去掉,即:视频001.mp4, 视频002.mp4, 视频003.mp4...
aliyunpan rename * 视频###.mp4 *.mp4
5. 批量重命名,将当前目录下所有.mp4文件全部进行 "视频+编号.mp4" 的重命名操作,旧的名称全部去掉,直接重命名无需人工确认操作
aliyunpan rename -y * 视频###.mp4 *.mp4
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() != 2 {
if c.NArg() != 2 && c.NArg() != 3 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
@@ -50,7 +92,12 @@ func CmdRename() cli.Command {
fmt.Println("未登录账号")
return nil
}
RunRename(parseDriveId(c), c.Args().Get(0), c.Args().Get(1))
if c.NArg() == 2 {
RunRename(parseDriveId(c), c.Args().Get(0), c.Args().Get(1))
} else if c.NArg() == 3 {
// 批量重命名
RunRenameBatch(c.Bool("y"), parseDriveId(c), c.Args().Get(0), c.Args().Get(1), c.Args().Get(2))
}
return nil
},
Flags: []cli.Flag{
@@ -59,6 +106,10 @@ func CmdRename() cli.Command {
Usage: "网盘ID",
Value: "",
},
cli.BoolFlag{
Name: "y",
Usage: "跳过人工确认,对批量操作有效",
},
},
}
}
@@ -104,3 +155,121 @@ func RunRename(driveId string, oldName string, newName string) {
fmt.Printf("重命名文件成功:%s -> %s\n", path.Base(oldName), path.Base(newName))
activeUser.DeleteOneCache(path.Dir(newName))
}
// RunRenameBatch 批量重命名文件
func RunRenameBatch(skipConfirm bool, driveId string, expression, replacement, filePattern string) {
if len(expression) == 0 {
fmt.Println("旧文件名不能为空")
return
}
if !apiutil.CheckFileNameValid(replacement) {
fmt.Println("新文件名不能包含特殊字符:" + apiutil.FileNameSpecialChars)
return
}
if len(filePattern) == 0 {
fmt.Println("文件匹配模式不能为空")
return
}
if strings.ContainsAny(filePattern, "/") {
fmt.Println("文件匹配模式不能包含路径分隔符")
return
}
if strings.ContainsAny(filePattern, "\\") {
fmt.Println("文件匹配模式不能包含路径分隔符")
return
}
activeUser := GetActiveUser()
absolutePath := path.Clean(activeUser.PathJoin(driveId, filePattern))
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
if err1 != nil {
fmt.Println("查询文件出错:" + err1.Error())
return
}
if fileList == nil || len(fileList) == 0 {
fmt.Println("没有找到符合的文件")
return
}
// 文件列表按照名字排序
files := fileArray{}
for _, f := range fileList {
files = append(files, newFileItem(f))
}
if len(files) > 1 {
sort.Sort(files)
}
// 依次处理
var index = 1
for _, file := range files {
replacementStr := replaceNumStr(replacement, index)
index += 1
if expression == "*" {
// 旧的名称全部替换
file.newFileName = replacementStr
} else {
// 替换指定的名称片段
file.newFileName = strings.ReplaceAll(file.file.FileName, expression, replacementStr)
}
}
// 确认
if !skipConfirm {
fmt.Printf("以下文件将进行对应的重命名\n\n")
idx := 1
for _, file := range files {
fmt.Printf("%d) %s -> %s\n", idx, file.file.FileName, file.newFileName)
idx += 1
}
fmt.Printf("\n是否进行批量重命名,该操作不可逆(y/n): ")
confirm := ""
_, err := fmt.Scanln(&confirm)
if err != nil || (confirm != "y" && confirm != "Y") {
fmt.Println("用户取消了操作")
return
}
}
// 重命名
for _, file := range files {
b, e := activeUser.PanClient().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))
}
+23 -13
View File
@@ -32,7 +32,7 @@ func CmdRm() cli.Command {
UsageText: cmder.App().Name + " rm <文件/目录的路径1> <文件/目录2> <文件/目录3> ...",
Description: `
注意: 删除多个文件和目录时, 请确保每一个文件和目录都存在, 否则删除操作会失败.
被删除的文件或目录可在网盘文件回收站找回.
被删除的文件或目录可在网盘文件回收站找回。支持通配符匹配删除文件,通配符当前只能匹配文件名,不能匹配文件路径。
示例:
@@ -44,9 +44,12 @@ func CmdRm() cli.Command {
删除 /我的资源 整个目录 !!
aliyunpan rm /我的资源
删除 /我的资源 目录下面的所有.zip文件,使用通配符匹配
aliyunpan rm /我的资源/*.zip
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -80,25 +83,32 @@ func RunRemove(driveId string, paths ...string) {
for _, p := range paths {
absolutePath := path.Clean(activeUser.PathJoin(driveId, p))
fe, err := activeUser.PanClient().FileInfoByPath(driveId, absolutePath)
if err != nil {
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
if err1 != nil {
failedRmPaths = append(failedRmPaths, absolutePath)
continue
}
fe.Path = absolutePath
delFileInfos = append(delFileInfos, &aliyunpan.FileBatchActionParam{
DriveId:driveId,
FileId:fe.FileId,
})
fileId2FileEntity[fe.FileId] = fe
cacheCleanDirs = append(cacheCleanDirs, path.Dir(fe.Path))
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 {
for _, item := range fdr {
if !item.Success {
failedRmPaths = append(failedRmPaths, fileId2FileEntity[item.FileId].Path)
} else {
@@ -111,7 +121,7 @@ func RunRemove(driveId string, paths ...string) {
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "文件/目录"})
for k := range successDelFileEntity {
tb.Append([]string{strconv.Itoa(k), successDelFileEntity[k].Path})
tb.Append([]string{strconv.Itoa(k + 1), successDelFileEntity[k].Path})
}
tb.Render()
}
+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"))
}
+250 -91
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,6 +14,7 @@
package command
import (
"encoding/csv"
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
@@ -23,6 +24,8 @@ import (
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"os"
"path"
"path/filepath"
"strconv"
"time"
)
@@ -33,7 +36,7 @@ func CmdShare() cli.Command {
Usage: "分享文件/目录",
UsageText: cmder.App().Name + " share",
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
@@ -50,11 +53,17 @@ func CmdShare() cli.Command {
创建文件 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 {
@@ -84,10 +93,18 @@ func CmdShare() cli.Command {
sharePwd = c.String("sharePwd")
}
modeFlag := "1"
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)
@@ -95,7 +112,7 @@ func CmdShare() cli.Command {
} else {
sharePwd = ""
}
RunShareSet(parseDriveId(c), c.Args(), et, sharePwd)
RunShareSet(modeFlag, parseDriveId(c), c.Args(), et, sharePwd)
return nil
},
Flags: []cli.Flag{
@@ -111,8 +128,8 @@ func CmdShare() cli.Command {
},
cli.StringFlag{
Name: "mode",
Usage: "有效期,1-私密分享,2-公开分享",
Value: "1",
Usage: "模式,1-私密分享,2-公开分享,3-快传",
Value: "3",
},
cli.StringFlag{
Name: "sharePwd",
@@ -130,8 +147,7 @@ func CmdShare() cli.Command {
RunShareList()
return nil
},
Flags: []cli.Flag{
},
Flags: []cli.Flag{},
},
{
Name: "cancel",
@@ -148,66 +164,118 @@ func CmdShare() cli.Command {
return nil
},
},
// {
// Name: "mc",
// Aliases: []string{},
// Usage: "创建秒传链接",
// UsageText: cmder.App().Name + " share mc <文件/目录1> <文件/目录2> ...",
// Description: `
//创建文件秒传链接,秒传链接只能是文件,如果是文件夹则会创建文件夹包含的所有文件的秒传链接。秒传链接可以通过RapidUpload命令或者Import命令进行导入到自己的网盘。
//示例:
// 创建文件 1.mp4 的秒传链接
// aliyunpan share mc 1.mp4
//
// 创建文件 1.mp4 的秒传链接,但链接隐藏相对路径
// aliyunpan share mc -hp 1.mp4
//
// 创建文件夹 share_folder 下面所有文件的秒传链接
// aliyunpan share mc share_folder/
//`,
// Action: func(c *cli.Context) error {
// if c.NArg() < 1 {
// cli.ShowCommandHelp(c, c.Command.Name)
// return nil
// }
// if config.Config.ActiveUser() == nil {
// fmt.Println("未登录账号")
// return nil
// }
// hp := false
// if c.IsSet("hp") {
// hp = c.Bool("hp")
// }
// RunShareMc(parseDriveId(c), hp, c.Args())
// return nil
// },
// Flags: []cli.Flag{
// cli.StringFlag{
// Name: "driveId",
// Usage: "网盘ID",
// Value: "",
// },
// cli.BoolFlag{
// Name: "hp",
// Usage: "hide path, 隐藏相对目录",
// },
// },
// },
{
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(driveId string, paths []string, expiredTime string, sharePwd string) {
panClient := GetActivePanClient()
fileList, _, err := GetFileInfoByPaths(paths[:len(paths)]...)
if err != nil {
fmt.Println(err)
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 fileList {
for _, f := range allFileList {
fidList = append(fidList, f.FileId)
}
@@ -216,29 +284,47 @@ func RunShareSet(driveId string, paths []string, expiredTime string, sharePwd st
return
}
r, err1 := panClient.ShareLinkCreate(aliyunpan.ShareCreateParam{
DriveId: driveId,
SharePwd: sharePwd,
Expiration: expiredTime,
FileIdList: fidList,
})
if err1 != nil || r == nil {
if err1.Code == apierror.ApiCodeFileShareNotAllowed {
fmt.Printf("创建分享链接失败: 该文件类型不允许分享\n")
} else {
fmt.Printf("创建分享链接失败: %s\n", err)
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
}
return
}
fmt.Printf("创建分享链接成功\n")
if len(sharePwd) > 0 {
fmt.Printf("链接:%s 提取码:%s\n", r.ShareUrl, r.SharePwd)
} else {
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 执行列出分享列表
@@ -251,7 +337,7 @@ func RunShareList() {
}
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "ShARE_ID", "分享链接", "提取码", "文件名", "FILE_ID", "过期时间", "状态"})
tb.SetHeader([]string{"#", "ShARE_ID", "分享链接", "提取码", "文件名", "过期时间", "状态"})
now := time.Now()
for k, record := range records {
et := "永久有效"
@@ -259,20 +345,25 @@ func RunShareList() {
et = record.Expiration
}
status := "有效"
if record.FirstFile == nil {
status = "已删除"
} else {
cz := time.FixedZone("CST", 8*3600)
if len(record.Expiration) > 0 {
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", record.Expiration, cz)
if expiredTime.Unix() < now.Unix() {
status = "已过期"
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), record.ShareId, record.ShareUrl, record.SharePwd,
tb.Append([]string{strconv.Itoa(k + 1), record.ShareId, record.ShareUrl, record.SharePwd,
record.ShareName,
record.FileIdList[0],
//record.FileIdList[0],
et,
status})
}
@@ -300,13 +391,81 @@ func RunShareCancel(shareIdList []string) {
}
}
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 {
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 {
@@ -331,4 +490,4 @@ func RunShareMc(driveId string, hideRelativePath bool, panPaths []string) {
})
}
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已完成文件扫描和同步数据库的构建,可以启动任务同步了")
}
}
+35 -5
View File
@@ -18,6 +18,9 @@ import (
"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"
)
@@ -27,7 +30,7 @@ func CmdToken() cli.Command {
Usage: "Token相关操作",
UsageText: cmder.App().Name + " token",
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
@@ -73,27 +76,54 @@ func CmdToken() cli.Command {
// RunTokenUpdate 执行Token更新
func RunTokenUpdate(modeFlag string) {
cmder.ReloadConfigFunc(nil)
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 {
for _, user := range userList {
params.Result = "success"
if modeFlag == "1" {
if user.UserId != config.Config.ActiveUID {
continue
}
}
newToken,e := aliyunpan.GetAccessTokenFromRefreshToken(user.RefreshToken)
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())
}
}
cmder.SaveConfigFunc(nil)
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))
}
+189 -181
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,15 +17,16 @@ 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"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/tickstep/library-go/logger"
@@ -54,23 +55,25 @@ const (
type (
// UploadOptions 上传可选项
UploadOptions struct {
AllParallel int // 所有文件并发上传数量,即可以同时并发上传多少个文件
Parallel int // 单个文件并发上传数量
MaxRetry int
NoRapidUpload bool
ShowProgress bool
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
DriveId string
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行上传,支持正则表达式
BlockSize int64 // 分片大小
UseInternalUrl bool // 是否使用内置链接
AllParallel int // 所有文件并发上传数量,即可以同时并发上传多少个文件
Parallel int // 单个文件并发上传数量
MaxRetry int
MaxTimeoutSec int // http请求超时时间,单位秒
NoRapidUpload bool
ShowProgress bool
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
IsSkipSameName bool // 跳过已存在的文件,即使文件内容不一致(不检查SHA1)
DriveId string
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行上传,支持正则表达式
BlockSize int64 // 分片大小
UseInternalUrl bool // 是否使用内置链接
}
)
var UploadFlags = []cli.Flag{
cli.IntFlag{
Name: "p",
Usage: "本次操作文件上传并发数量,即可以同时并发上传多少个文件。0代表跟从配置文件设置",
Usage: "本次操作文件上传并发数量,即可以同时并发上传多少个文件。0代表跟从配置文件设置(取值范围:1 ~ 20)",
Value: 0,
},
cli.IntFlag{
@@ -78,6 +81,10 @@ var UploadFlags = []cli.Flag{
Usage: "上传失败最大重试次数",
Value: DefaultUploadMaxRetry,
},
cli.IntFlag{
Name: "timeout",
Usage: "上传请求超时时间,单位为秒。当遇到网络不好导致上传超时可以尝试调大该值,建议设置30秒以上",
},
cli.BoolFlag{
Name: "np",
Usage: "no progress 不展示上传进度条",
@@ -86,6 +93,10 @@ var UploadFlags = []cli.Flag{
Name: "ow",
Usage: "overwrite, 覆盖已存在的同名文件,注意已存在的文件会被移到回收站",
},
cli.BoolFlag{
Name: "skip",
Usage: "skip same name, 跳过已存在的同名文件,即使文件内容不一致(不检查SHA1)",
},
cli.BoolFlag{
Name: "norapid",
Usage: "不检测秒传。跳过费时的SHA1计算直接上传",
@@ -102,7 +113,7 @@ var UploadFlags = []cli.Flag{
},
cli.IntFlag{
Name: "bs",
Usage: "block size,上传分片大小,单位KB。推荐值:1024 ~ 10240",
Usage: "block size,上传分片大小,单位KB。推荐值:1024 ~ 10240。当上传极大单文件时候请适当调高该值",
Value: 10240,
},
}
@@ -114,7 +125,7 @@ func CmdUpload() cli.Command {
Usage: "上传文件/目录",
UsageText: cmder.App().Name + " upload <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>",
Description: `
上传指定的文件夹或者文件,上传的文件将会保存到 <目标目录>.
上传指定的文件夹或者文件,上传的文件将会保存到 <目标目录>。支持软链接文件,包括Linux/macOS(ln命令)和Windows(mklink命令)创建的符号链接文件。
示例:
1. 将本地的 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录
@@ -127,21 +138,27 @@ func CmdUpload() cli.Command {
3. 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录
aliyunpan upload C:/Users/Administrator/Desktop /视频
4. 使用相对路径
4. 将本地 200GB 极大文件 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录,需要调高上传分片大小
aliyunpan upload -bs 30720 C:/Users/Administrator/Desktop/1.mp4 /视频
5. 使用相对路径
aliyunpan upload 1.mp4 /视频
5. 覆盖上传,已存在的同名文件会被移到回收站
6. 覆盖上传,已存在的同名文件会被移到回收站
aliyunpan upload -ow 1.mp4 /视频
6. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
7. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
aliyunpan upload -exn "\.jpg$" C:/Users/Administrator/Video /视频
7. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
8. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
aliyunpan upload -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视频
8. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的 @eadir 文件夹
9. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的 @eadir 文件夹
aliyunpan upload -exn "^@eadir$" C:/Users/Administrator/Video /视频
10. 跳过已存在的同名文件,即使文件内容不一致(不检查SHA1)
aliyunpan upload -skip 1.mp4 /视频
参考:
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式。在正则表达式中,^表示匹配开头,$表示匹配结尾。
1)排除@eadir文件或者文件夹:-exn "^@eadir$"
@@ -151,7 +168,7 @@ func CmdUpload() cli.Command {
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() < 2 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -159,17 +176,41 @@ func CmdUpload() cli.Command {
}
subArgs := c.Args()
timeout := 0
if c.IsSet("timeout") {
timeout = c.Int("timeout")
if timeout < 0 {
timeout = 0
}
}
// 获取上传文件锁,保证上传操作单实例
//locker := filelocker.NewFileLocker(config.GetLockerDir() + "/aliyunpan-upload")
//if e := filelocker.LockFile(locker, 0755, true, 5*time.Second); e != nil {
// logger.Verboseln(e)
// fmt.Println("本应用其他实例正在执行上传,请先停止或者等待其完成")
// return nil
//}
RunUpload(subArgs[:c.NArg()-1], subArgs[c.NArg()-1], &UploadOptions{
AllParallel: c.Int("p"), // 多文件上传的时候,允许同时并行上传的文件数量
Parallel: 1, // 一个文件同时多少个线程并发上传的数量。阿里云盘只支持单线程按顺序进行文件part数据上传,所以只能是1
MaxRetry: c.Int("retry"),
NoRapidUpload: c.Bool("norapid"),
ShowProgress: !c.Bool("np"),
IsOverwrite: c.Bool("ow"),
DriveId: parseDriveId(c),
ExcludeNames: c.StringSlice("exn"),
BlockSize: int64(c.Int("bs") * 1024),
AllParallel: c.Int("p"), // 多文件上传的时候,允许同时并行上传的文件数量
Parallel: 1, // 一个文件同时多少个线程并发上传的数量。阿里云盘只支持单线程按顺序进行文件part数据上传,所以只能是1
MaxRetry: c.Int("retry"),
MaxTimeoutSec: timeout,
NoRapidUpload: c.Bool("norapid"),
ShowProgress: !c.Bool("np"),
IsOverwrite: c.Bool("ow"),
IsSkipSameName: c.Bool("skip"),
DriveId: parseDriveId(c),
ExcludeNames: c.StringSlice("exn"),
BlockSize: int64(c.Int("bs") * 1024),
})
// 释放文件锁
//if locker != nil {
// filelocker.UnlockFile(locker)
//}
return nil
},
Flags: UploadFlags,
@@ -200,7 +241,7 @@ func CmdRapidUpload() cli.Command {
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder" "aliyunpan://file1.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder"
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() <= 0 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -232,15 +273,23 @@ func CmdRapidUpload() cli.Command {
// 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
go func() {
for {
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) {
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
logger.Verboseln("update access token for upload task")
}
}
}()
}(&continueFlag)
if opt == nil {
opt = &UploadOptions{}
@@ -253,6 +302,10 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
opt.AllParallel = config.DefaultFileUploadParallelNum
}
}
if opt.AllParallel > config.MaxFileUploadParallelNum {
fmt.Printf("警告: 当前上传文件并发数过大,可能会被阿里风控导致上传失败,建议调小。\n")
}
if opt.Parallel <= 0 {
opt.Parallel = 1
}
@@ -261,6 +314,11 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
}
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)
@@ -292,6 +350,8 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
statistic = &panupload.UploadStatistic{}
folderCreateMutex = &sync.Mutex{}
pluginManger = plugins.NewPluginManager(config.GetPluginDir())
)
executor.SetParallel(opt.AllParallel)
statistic.StartTimer() // 开始计时
@@ -299,15 +359,20 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
// 全局速度统计
globalSpeedsStat := &speeds.Speeds{}
// 获取当前插件
plugin, _ := pluginManger.GetPlugin()
// 上传记录器
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/upload_file_records.csv")
// 遍历指定的文件并创建上传任务
for _, curPath := range localPaths {
var walkFunc filepath.WalkFunc
var db panupload.SyncDb
var walkFunc localfile.MyWalkFunc
curPath = filepath.Clean(curPath)
localPathDir := filepath.Dir(curPath)
// 是否排除上传
if isExcludeFile(curPath, opt) {
if utils.IsExcludeFile(curPath, &opt.ExcludeNames) {
fmt.Printf("排除文件: %s\n", curPath)
continue
}
@@ -317,109 +382,107 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
localPathDir = ""
}
if fi, err := os.Stat(curPath); err == nil && fi.IsDir() {
//使用绝对路径避免异常
dbpath, err := filepath.Abs(curPath)
walkFunc = func(file localfile.SymlinkFile, fi os.FileInfo, err error) error {
if err != nil {
dbpath = curPath
// skip this error file and continue recurse
logger.Verboseln("upload process file: ", file, " error: ", err)
return nil
}
dbpath += string(os.PathSeparator) + BackupMetaDirName
if di, err := os.Stat(dbpath); err == nil && di.IsDir() {
db, err = panupload.OpenSyncDb(dbpath+string(os.PathSeparator) + "db", BackupMetaBucketName)
if db != nil {
defer func(syncDb panupload.SyncDb) {
db.Close()
}(db)
} else {
fmt.Println(curPath, "同步数据库打开失败,跳过该目录的备份", err)
continue
}
}
}
walkFunc = func(file string, fi os.FileInfo, err error) error {
if err != nil {
return err
if os.PathSeparator == '\\' {
file.LogicPath = cmdutil.ConvertToWindowsPathSeparator(file.LogicPath)
file.RealPath = cmdutil.ConvertToWindowsPathSeparator(file.RealPath)
}
// 是否排除上传
if isExcludeFile(file, opt) {
fmt.Printf("排除文件: %s\n", file)
if utils.IsExcludeFile(file.LogicPath, &opt.ExcludeNames) {
fmt.Printf("排除文件: %s\n", file.LogicPath)
return filepath.SkipDir
}
if fi.Mode()&os.ModeSymlink != 0 { // 读取 symbol link
err = WalkAllFile(file+string(os.PathSeparator), walkFunc)
return err
}
subSavePath := strings.TrimPrefix(file, localPathDir)
subSavePath := strings.TrimPrefix(file.LogicPath, localPathDir)
// 针对 windows 的目录处理
if os.PathSeparator == '\\' {
subSavePath = cmdutil.ConvertToUnixPathSeparator(subSavePath)
}
subSavePath = path.Clean(savePath + aliyunpan.PathSeparator + subSavePath)
var ufm *panupload.UploadedFileMeta
if db != nil {
if ufm = db.Get(subSavePath); ufm.Size == fi.Size() && ufm.ModTime == fi.ModTime().Unix() {
logger.Verbosef("文件未修改跳过:%s\n", file)
return nil
}
// 插件回调
ft := "file"
if fi.IsDir() {
ft = "folder"
}
if fi.IsDir() { // 备份目录处理
if strings.HasPrefix(fi.Name(), BackupMetaDirName) {
pluginParam := &plugins.UploadFilePrepareParams{
LocalFilePath: file.LogicPath,
LocalFileName: fi.Name(),
LocalFileSize: fi.Size(),
LocalFileType: ft,
LocalFileUpdatedAt: fi.ModTime().Format("2006-01-02 15:04:05"),
DriveId: activeUser.ActiveDriveId,
DriveFilePath: strings.TrimPrefix(strings.TrimPrefix(subSavePath, savePath), "/"),
}
if uploadFilePrepareResult, er := plugin.UploadFilePrepareCallback(plugins.GetContext(activeUser), pluginParam); er == nil && uploadFilePrepareResult != nil {
if strings.Compare("yes", uploadFilePrepareResult.UploadApproved) != 0 {
// skip upload this file
fmt.Printf("插件禁止该文件上传: %s\n", file.LogicPath)
return filepath.SkipDir
}
//不存在同步数据库时跳过
if db == nil || ufm.FileId != "" {
return nil
if uploadFilePrepareResult.DriveFilePath != "" {
targetSavePanRelativePath := strings.TrimPrefix(uploadFilePrepareResult.DriveFilePath, "/")
subSavePath = path.Clean(savePath + aliyunpan.PathSeparator + targetSavePanRelativePath)
fmt.Printf("插件修改文件网盘保存路径为: %s\n", subSavePath)
}
panClient := activeUser.PanClient()
fmt.Println(subSavePath, "云盘文件夹预创建")
//首先尝试直接创建文件夹
if ufm = db.Get(path.Dir(subSavePath)); ufm.IsFolder == true && ufm.FileId != "" {
rs, err := panClient.Mkdir(opt.DriveId, ufm.FileId, fi.Name())
if err == nil && rs != nil && rs.FileId != "" {
db.Put(subSavePath, &panupload.UploadedFileMeta{FileId: rs.FileId, IsFolder: true, ModTime: fi.ModTime().Unix(), ParentId: rs.ParentFileId})
return nil
}
}
rs, err := panClient.MkdirRecursive(opt.DriveId, "", "", 0, strings.Split(path.Clean(subSavePath), "/"))
if err == nil && rs != nil && rs.FileId != "" {
db.Put(subSavePath, &panupload.UploadedFileMeta{FileId: rs.FileId, IsFolder: true, ModTime: fi.ModTime().Unix(), ParentId: rs.ParentFileId})
return nil
}
fmt.Println(subSavePath, "创建云盘文件夹失败", err)
return filepath.SkipDir
}
taskinfo := executor.Append(&panupload.UploadTaskUnit{
LocalFileChecksum: localfile.NewLocalFileEntity(file),
SavePath: subSavePath,
DriveId: opt.DriveId,
PanClient: activeUser.PanClient(),
UploadingDatabase: uploadDatabase,
FolderCreateMutex: folderCreateMutex,
Parallel: opt.Parallel,
NoRapidUpload: opt.NoRapidUpload,
BlockSize: opt.BlockSize,
UploadStatistic: statistic,
ShowProgress: opt.ShowProgress,
IsOverwrite: opt.IsOverwrite,
FolderSyncDb: db,
UseInternalUrl: opt.UseInternalUrl,
GlobalSpeedsStat: globalSpeedsStat,
}, opt.MaxRetry)
fmt.Printf("[%s] 加入上传队列: %s\n", taskinfo.Id(), file)
// 创建对应的文件上传任务
// 上传里面的文件会创建对应的缺失文件夹
if !fi.IsDir() {
taskinfo := executor.Append(&panupload.UploadTaskUnit{
LocalFileChecksum: localfile.NewLocalSymlinkFileEntity(file),
SavePath: subSavePath,
DriveId: opt.DriveId,
PanClient: activeUser.PanClient(),
UploadingDatabase: uploadDatabase,
FolderCreateMutex: folderCreateMutex,
Parallel: opt.Parallel,
NoRapidUpload: opt.NoRapidUpload,
BlockSize: opt.BlockSize,
UploadStatistic: statistic,
ShowProgress: opt.ShowProgress,
IsOverwrite: opt.IsOverwrite,
IsSkipSameName: opt.IsSkipSameName,
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
}
if err := WalkAllFile(curPath, walkFunc); err != nil {
fmt.Printf("警告: 遍历错误: %s\n", err)
file := localfile.NewSymlinkFile(curPath)
if err = localfile.WalkAllFile(file, walkFunc); err != nil {
if err != filepath.SkipDir {
fmt.Printf("警告: 遍历错误: %s\n", err)
}
}
}
@@ -441,7 +504,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
tb := cmdtable.NewTable(os.Stdout)
for e := failed.Shift(); e != nil; e = failed.Shift() {
item := e.(*taskframework.TaskInfoItem)
tb.Append([]string{item.Info.Id(), item.Unit.(*panupload.UploadTaskUnit).LocalFileChecksum.Path})
tb.Append([]string{item.Info.Id(), item.Unit.(*panupload.UploadTaskUnit).LocalFileChecksum.Path.LogicPath})
}
tb.Render()
}
@@ -449,61 +512,6 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
activeUser.DeleteCache(GetAllPathFolderByPath(savePath))
}
// 是否是排除上传的文件
func isExcludeFile(filePath string, opt *UploadOptions) bool {
if opt == nil || len(opt.ExcludeNames) == 0{
return false
}
for _,pattern := range opt.ExcludeNames {
fileName := path.Base(filePath)
m,_ := regexp.MatchString(pattern, fileName)
if m {
return true
}
}
return false
}
func WalkAllFile(dirPath string, walkFn filepath.WalkFunc) error {
info, err := os.Lstat(dirPath)
if err != nil {
err = walkFn(dirPath, nil, err)
} else {
err = walkAllFile(dirPath, info, walkFn)
}
return err
}
func walkAllFile(dirPath string, info os.FileInfo, walkFn filepath.WalkFunc) error {
if !info.IsDir() {
return walkFn(dirPath, info, nil)
}
files, err := ioutil.ReadDir(dirPath)
if err != nil {
return walkFn(dirPath, nil, err)
}
for _, fi := range files {
subFilePath := dirPath + "/" + fi.Name()
err := walkFn(subFilePath, fi, err)
if err != nil && err != filepath.SkipDir {
return err
}
if fi.IsDir() {
if err == filepath.SkipDir {
continue
}
err := walkAllFile(subFilePath, fi, walkFn)
if err != nil {
return err
}
}
}
return nil
}
// RunRapidUpload 秒传
func RunRapidUpload(driveId string, isOverwrite bool, fileMetaList []string, savePanPath string) {
activeUser := GetActiveUser()
@@ -516,8 +524,8 @@ func RunRapidUpload(driveId string, isOverwrite bool, fileMetaList []string, sav
items := []*RapidUploadItem{}
// parse file meta strings
for _,fileMeta := range fileMetaList {
item,e := newRapidUploadItem(fileMeta)
for _, fileMeta := range fileMetaList {
item, e := newRapidUploadItem(fileMeta)
if e != nil {
fmt.Println(e)
continue
@@ -535,7 +543,7 @@ func RunRapidUpload(driveId string, isOverwrite bool, fileMetaList []string, sav
}
// upload one by one
for _,item := range items {
for _, item := range items {
fmt.Println("准备秒传:", item.FilePath)
if ee := doRapidUpload(driveId, isOverwrite, item); ee != nil {
fmt.Println(ee)
@@ -576,7 +584,7 @@ func doRapidUpload(driveId string, isOverwrite bool, item *RapidUploadItem) erro
}
if efi != nil && efi.FileId != "" {
// existed, delete it
fileDeleteResult, err1 := panClient.FileDelete([]*aliyunpan.FileBatchActionParam{{DriveId:efi.DriveId, FileId:efi.FileId}})
fileDeleteResult, err1 := panClient.FileDelete([]*aliyunpan.FileBatchActionParam{{DriveId: efi.DriveId, FileId: efi.FileId}})
if err1 != nil || len(fileDeleteResult) == 0 {
return fmt.Errorf("无法删除文件,请稍后重试")
}
@@ -600,11 +608,11 @@ func doRapidUpload(driveId string, isOverwrite bool, item *RapidUploadItem) erro
if apierr != nil {
return fmt.Errorf("创建秒传任务失败:" + apierr.Error())
}
if uploadOpEntity.RapidUpload {
logger.Verboseln("秒传成功, 保存到网盘路径: ", path.Join(panDir, uploadOpEntity.FileName))
} else {
return fmt.Errorf("失败,文件未曾上传,无法秒传")
}
return nil
}
}
+7 -7
View File
@@ -29,7 +29,7 @@ func CmdLoglist() cli.Command {
Usage: "列出帐号列表",
Description: "列出所有已登录的阿里账号",
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
fmt.Println(config.Config.UserList.String())
return nil
@@ -50,8 +50,8 @@ func CmdSu() cli.Command {
aliyunpan su <uid or name>
`,
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() >= 2 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -85,8 +85,8 @@ func CmdSu() cli.Command {
return nil
}
if n, err := strconv.Atoi(index); err == nil && n >= 0 && n < numLogins {
uid = config.Config.UserList[n].UserId
if n, err1 := strconv.Atoi(index); err1 == nil && (n-1) >= 0 && (n-1) < numLogins {
uid = config.Config.UserList[n-1].UserId
} else {
fmt.Printf("切换用户失败, 请检查 # 值是否正确\n")
return nil
@@ -102,7 +102,7 @@ func CmdSu() cli.Command {
}
if switchedUser == nil {
switchedUser = cmder.TryLogin()
switchedUser = TryLogin()
}
if switchedUser != nil {
@@ -122,7 +122,7 @@ func CmdWho() cli.Command {
Usage: "获取当前帐号",
Description: "获取当前帐号的信息",
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
+212 -42
View File
@@ -16,66 +16,76 @@ 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)
)
const(
// 备份数据库桶分区标志
BackupMetaBucketName = "adrive"
saveConfigMutex *sync.Mutex = new(sync.Mutex)
// 备份数据文件夹目录名称,隐藏目录
BackupMetaDirName = ".adrive"
)
// GetFileInfoByPaths 获取指定文件路径的文件详情信息
func GetAppFileInfoByPaths(driveId string, paths ...string) (fileInfoList []*aliyunpan.FileEntity, failedPaths []string, error error) {
if len(paths) <= 0 {
return nil, nil, fmt.Errorf("请指定文件路径")
}
activeUser := GetActiveUser()
for idx := 0; idx < len(paths); idx++ {
absolutePath := path.Clean(activeUser.PathJoin(driveId, paths[idx]))
fe, err := activeUser.PanClient().FileInfoByPath(driveId, absolutePath)
ReloadConfigFunc = func(c *cli.Context) error {
err := config.Config.Reload()
if err != nil {
failedPaths = append(failedPaths, absolutePath)
continue
fmt.Printf("重载配置错误: %s\n", err)
}
fileInfoList = append(fileInfoList, fe)
return nil
}
SaveConfigFunc = func(c *cli.Context) error {
saveConfigMutex.Lock()
defer saveConfigMutex.Unlock()
err := config.Config.Save()
if err != nil {
fmt.Printf("保存配置错误: %s\n", err)
}
return nil
}
)
// RunTestShellPattern 执行测试通配符
func RunTestShellPattern(driveId string, pattern string) {
acUser := GetActiveUser()
files, err := acUser.PanClient().MatchPathByShellPattern(driveId, GetActiveUser().PathJoin(driveId, pattern))
if err != nil {
fmt.Println(err)
return
}
for _, f := range *files {
fmt.Printf("%s\n", f.Path)
}
return
}
// GetFileInfoByPaths 获取指定文件路径的文件详情信息
func GetFileInfoByPaths(paths ...string) (fileInfoList []*aliyunpan.FileEntity, failedPaths []string, error error) {
if len(paths) <= 0 {
return nil, nil, fmt.Errorf("请指定文件路径")
}
activeUser := GetActiveUser()
for idx := 0; idx < len(paths); idx++ {
absolutePath := path.Clean(activeUser.PathJoin(activeUser.ActiveDriveId, paths[idx]))
fe, err := activeUser.PanClient().FileInfoByPath(activeUser.ActiveDriveId, absolutePath)
// matchPathByShellPattern 通配符匹配路径,允许返回多个匹配结果
func matchPathByShellPattern(driveId string, patterns ...string) (files []*aliyunpan.FileEntity, e error) {
acUser := GetActiveUser()
for k := range patterns {
ps, err := acUser.PanClient().MatchPathByShellPattern(driveId, acUser.PathJoin(driveId, patterns[k]))
if err != nil {
failedPaths = append(failedPaths, absolutePath)
continue
return nil, err
}
fileInfoList = append(fileInfoList, fe)
files = append(files, *ps...)
}
return
return files, nil
}
func matchPathByShellPattern(driveId string, patterns ...string) (panpaths []string, err error) {
// makePathAbsolute 拼接路径,确定返回路径为绝对路径
func makePathAbsolute(driveId string, patterns ...string) (panpaths []string, err error) {
acUser := GetActiveUser()
for k := range patterns {
ps := acUser.PathJoin(driveId, patterns[k])
@@ -89,7 +99,7 @@ func RandomStr(count int) string {
STR_SET := "abcdefjhijklmnopqrstuvwxyz1234567890"
rand.Seed(time.Now().UnixNano())
str := strings.Builder{}
for i := 0; i < count; i++ {
for i := 0; i < count; i++ {
str.WriteByte(byte(STR_SET[rand.Intn(len(STR_SET))]))
}
return str.String()
@@ -100,7 +110,7 @@ func GetAllPathFolderByPath(pathStr string) []string {
dirs := []string{}
p := "/"
dirs = append(dirs, p)
for _,s := range dirNames {
for _, s := range dirNames {
p = path.Join(p, s)
dirs = append(dirs, p)
}
@@ -114,12 +124,12 @@ func EscapeStr(s string) string {
// UnescapeStr 反转义字符串
func UnescapeStr(s string) string {
r,_ := url.PathUnescape(s)
r, _ := url.PathUnescape(s)
return r
}
// RefreshTokenInNeed 刷新refresh token
func RefreshTokenInNeed(activeUser *config.PanUser) bool {
func RefreshTokenInNeed(activeUser *config.PanUser, deviceName string) bool {
if activeUser == nil {
return false
}
@@ -130,17 +140,177 @@ func RefreshTokenInNeed(activeUser *config.PanUser) bool {
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()) <= (10 * 60) { // 10min
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
}
+58 -15
View File
@@ -18,8 +18,11 @@ import (
"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 {
@@ -28,8 +31,8 @@ func CmdWebdav() cli.Command {
Usage: "在线网盘服务",
Description: "webdav在线网盘服务",
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
fmt.Print(`
本文命令可以让阿里云盘变身为webdav协议的文件服务器。这样你可以把阿里云盘挂载为Windows、Linux、Mac系统的磁盘,可以通过NAS系统做文件管理或文件同步等等。
@@ -60,8 +63,8 @@ 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" -pan_drive "File" -pan_dir_path "/webdav_folder"
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 {
@@ -69,25 +72,44 @@ aliyunpan webdav start -h
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,
PanDriveId: "",
PanUserId: "",
PanUser: nil,
UploadChunkSize: c.Int("bs") * 1024,
TransferUrlType: config.Config.TransferUrlType,
Address: "0.0.0.0",
Port: 23077,
Prefix: "/",
Users: []webdav.WebdavUser{{
Address: "0.0.0.0",
Port: 23077,
Prefix: "/",
Users: []webdav.WebdavUser{{
Username: "admin",
Password: "admin",
Scope: "/",
Mode: "rw",
}},
}
// pan user
panUserId := config.Config.ActiveUID
activeUser := GetActiveUser()
panUserId := activeUser.UserId
webdavServ.PanUserId = panUserId
webdavServ.PanUser = activeUser
@@ -138,15 +160,31 @@ aliyunpan webdav start -h
}
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",
webdavServ.Port, webdavServ.Users[0].Username, webdavServ.Users[0].Password, panDriveNameStr, webdavServ.Users[0].Scope)
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()
@@ -170,6 +208,11 @@ aliyunpan webdav start -h
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-相册。默认为文件网盘",
+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
}
+2 -2
View File
@@ -35,10 +35,10 @@ func (pu *PanUser) CacheFilesDirectoriesList(pathStr string) (fdl aliyunpan.File
return nil
}
fileListParam := &aliyunpan.FileListParam{
DriveId: pu.ActiveDriveId,
DriveId: pu.ActiveDriveId,
ParentFileId: fi.FileId,
}
fdl, apiError = pu.panClient.FileListGetAll(fileListParam)
fdl, apiError = pu.panClient.FileListGetAll(fileListParam, 100)
if apiError != nil {
return nil
}
+106 -6
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,6 +19,7 @@ import (
"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"
@@ -42,12 +43,24 @@ const (
// 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 (
@@ -58,6 +71,9 @@ var (
Config = NewConfig(configFilePath)
AppVersion string
// IsAppInCliMode 是否在交互模式
IsAppInCliMode = false
)
type UpdateCheckInfo struct {
@@ -83,10 +99,17 @@ type PanConfig struct {
SaveDir string `json:"saveDir"` // 下载储存路径
Proxy string `json:"proxy"` // 代理
LocalAddrs string `json:"localAddrs"` // 本地网卡地址
Proxy string `json:"proxy"` // 代理
LocalAddrs string `json:"localAddrs"` // 本地网卡地址
PreferIPType string `json:"preferIPType"` // 优先IP类型,IPv4或者IPv6
UpdateCheckInfo UpdateCheckInfo `json:"updateCheckInfo"`
VideoFileExtensions string `json:"videoFileExtensions"`
FileRecordConfig string `json:"fileRecordConfig"` // 上传、下载、同步文件的记录,包括失败和成功的
DeviceId string `json:"deviceId"` // 客户端ID,用于标识登录客户端,阿里单个账号最多允许10个客户端同时登录
DeviceName string `json:"deviceName"` // 客户端名称,默认为:Chrome浏览器
configFilePath string
configFile *os.File
fileMu sync.Mutex
@@ -179,6 +202,15 @@ func (c *PanConfig) init() error {
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
}
@@ -189,8 +221,8 @@ func (c *PanConfig) lazyOpenConfigFile() (err error) {
}
c.fileMu.Lock()
os.MkdirAll(filepath.Dir(c.configFilePath), 0700)
c.configFile, err = os.OpenFile(c.configFilePath, os.O_CREATE|os.O_RDWR, 0600)
os.MkdirAll(filepath.Dir(c.configFilePath), 0755)
c.configFile, err = os.OpenFile(c.configFilePath, os.O_CREATE|os.O_RDWR, 0755)
c.fileMu.Unlock()
if err != nil {
@@ -235,6 +267,9 @@ func (c *PanConfig) loadConfigFromFile() (err error) {
if err != nil {
return ErrConfigContentsParseError
}
if c.DeviceName == "" {
c.DeviceName = DefaultDeviceName
}
return nil
}
@@ -256,6 +291,11 @@ func (c *PanConfig) initDefaultConfig() {
}
}
c.ConfigVer = ConfigVersion
c.VideoFileExtensions = DefaultVideoFileExtensions
c.DeviceId = RandomDeviceId() // 生成默认客户端ID
c.DeviceName = DefaultDeviceName
c.FileRecordConfig = "1" // 默认开启
c.PreferIPType = "ipv4" // 默认优先IPv4
}
// GetConfigDir 获取配置路径
@@ -302,6 +342,42 @@ func GetConfigDir() string {
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 {
@@ -314,7 +390,7 @@ func (c *PanConfig) ActiveUser() *PanUser {
if u.UserId == c.ActiveUID {
if u.PanClient() == nil {
// restore client
user, err := SetupUserByCookie(&u.WebToken)
user, err := SetupUserByCookie(&u.WebToken, c.DeviceId, c.DeviceName)
if err != nil {
logger.Verboseln("setup user error")
return nil
@@ -333,6 +409,15 @@ func (c *PanConfig) ActiveUser() *PanUser {
} 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 {
@@ -423,3 +508,18 @@ func (c *PanConfig) HTTPClient(ua string) *requester.HTTPClient {
}
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
}
+40 -5
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -36,6 +36,17 @@ func (c *PanConfig) SetLocalAddrs(localAddrs string) {
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)
@@ -66,8 +77,29 @@ func (c *PanConfig) SetMaxUploadRateByStr(sizeStr string) error {
return nil
}
// SetFileRecorderConfig 设置文件记录器
func (c *PanConfig) SetFileRecorderConfig(config string) error {
if config == "1" || config == "2" {
c.FileRecordConfig = config
}
return nil
}
// SetDeviceId 设置客户端ID
func (c *PanConfig) SetDeviceId(deviceId string) error {
if deviceId == "" {
return nil
}
c.DeviceId = deviceId
return nil
}
// PrintTable 输出表格
func (c *PanConfig) PrintTable() {
fileRecorderLabel := "禁用"
if c.FileRecordConfig == "1" {
fileRecorderLabel = "开启"
}
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"名称", "值", "建议值", "描述"})
tb.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
@@ -76,12 +108,15 @@ func (c *PanConfig) PrintTable() {
[]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{"max_download_rate", showMaxRate(c.MaxDownloadRate), "", "限制单个文件最大下载速度, 0代表不限制"},
[]string{"max_upload_rate", showMaxRate(c.MaxUploadRate), "", "限制单个文件最大上传速度, 0代表不限制"},
[]string{"transfer_url_type", strconv.Itoa(c.TransferUrlType), "1-默认,2-阿里云ECS", "上传下载URL类别。除非在阿里云ECS(暂只支持经典网络)服务器中使用,不然请设置1"},
[]string{"savedir", c.SaveDir, "", "下载文件的储存目录"},
[]string{"proxy", c.Proxy, "", "设置代理, 支持 http/socks5 代理,例如:http://127.0.0.1:8888"},
[]string{"local_addrs", c.LocalAddrs, "", "设置本地网卡地址, 多个地址用逗号隔开,例如:127.0.0.1,192.168.100.126"},
[]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()
}
+53 -4
View File
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -48,14 +48,29 @@ func (d DriveInfoList) GetAlbumDriveId() string {
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"`
@@ -72,7 +87,7 @@ type PanUser struct {
type PanUserList []*PanUser
func SetupUserByCookie(webToken *aliyunpan.WebLoginToken) (user *PanUser, err *apierror.ApiError) {
func SetupUserByCookie(webToken *aliyunpan.WebLoginToken, deviceId, deviceName string) (user *PanUser, err *apierror.ApiError) {
tryRefreshWebToken := true
if webToken == nil {
@@ -80,7 +95,17 @@ func SetupUserByCookie(webToken *aliyunpan.WebLoginToken) (user *PanUser, err *a
}
doLoginAct:
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{})
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,
@@ -119,13 +144,26 @@ doLoginAct:
// drive list
u.DriveList = DriveInfoList{
{DriveId: userInfo.FileDriveId, DriveTag: "File", DriveName: "文件"},
{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
}
@@ -143,6 +181,8 @@ func (pu *PanUser) PathJoin(driveId, p string) string {
if di != nil {
if di.IsFileDrive() {
wd = pu.Workdir
} else if di.IsResourceDrive() {
wd = pu.ResourceWorkdir
} else if di.IsAlbumDrive() {
wd = pu.AlbumWorkdir
}
@@ -217,6 +257,11 @@ func (pu *PanUser) IsAlbumDriveActive() bool {
return d != nil && d.IsAlbumDrive()
}
func (pu *PanUser) IsResourceDriveActive() bool {
d := pu.GetActiveDriveInfo()
return d != nil && d.IsResourceDrive()
}
func (di *DriveInfo) IsFileDrive() bool {
return di.DriveTag == "File"
}
@@ -224,3 +269,7 @@ func (di *DriveInfo) IsFileDrive() bool {
func (di *DriveInfo) IsAlbumDrive() bool {
return di.DriveTag == "Album"
}
func (di *DriveInfo) IsResourceDrive() bool {
return di.DriveTag == "Resource"
}
+17 -3
View File
@@ -21,9 +21,11 @@ import (
"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 {
@@ -34,7 +36,7 @@ func (pl *PanUserList) String() string {
tb.SetHeader([]string{"#", "uid", "用户名", "昵称"})
for k, userInfo := range *pl {
tb.Append([]string{strconv.Itoa(k), userInfo.UserId, userInfo.AccountName, userInfo.Nickname})
tb.Append([]string{strconv.Itoa(k + 1), userInfo.UserId, userInfo.AccountName, userInfo.Nickname})
}
tb.Render()
@@ -95,7 +97,7 @@ func DecryptString(text string) string {
if text == "" {
return ""
}
d, _ := hex.DecodeString(text)
d, _ := hex.DecodeString(text)
// use the machine unique id as the key
// but in some OS, this key will be changed if you reinstall the OS
@@ -120,4 +122,16 @@ func IsFolderExist(pathStr string) bool {
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()
}
+4
View File
@@ -25,3 +25,7 @@ func TestEncryptString(t *testing.T) {
func TestDecryptString(t *testing.T) {
fmt.Println(DecryptString("75b3c8d21607440c0e8a70f4a4861c8669774cc69c70ce2a2c8acb815b6d5d3b"))
}
func TestRandomDeviceId(t *testing.T) {
fmt.Println(RandomDeviceId())
}
+2 -1
View File
@@ -41,7 +41,8 @@ type Config struct {
InstanceStatePath string // 断点续传信息路径
TryHTTP bool // 是否尝试使用 http 连接
ShowProgress bool // 是否展示下载进度条
UseInternalUrl bool // 是否使用内置链接
UseInternalUrl bool // 是否使用内置链接
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
}
//NewConfig 返回默认配置
+30 -11
View File
@@ -28,6 +28,7 @@ import (
"github.com/tickstep/library-go/requester/rio/speeds"
"io"
"net/http"
"strings"
"sync"
"time"
)
@@ -50,10 +51,10 @@ type (
onDownloadStatusEvent DownloadStatusFunc //状态处理事件
monitorCancelFunc context.CancelFunc
globalSpeedsStat *speeds.Speeds // 全局速度统计
globalSpeedsStat *speeds.Speeds // 全局速度统计
fileInfo *aliyunpan.FileEntity // 下载的文件信息
driveId string
fileInfo *aliyunpan.FileEntity // 下载的文件信息
driveId string
loadBalancerCompareFunc LoadBalancerCompareFunc // 负载均衡检测函数
durlCheckFunc DURLCheckFunc // 下载url检测函数
statusCodeBodyCheckFunc StatusCodeBodyCheckFunc
@@ -76,9 +77,9 @@ type (
//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,
config: config,
writer: writer,
panClient: p,
globalSpeedsStat: globalSpeedsStat,
}
return
@@ -216,7 +217,7 @@ func (der *Downloader) checkLoadBalancers() *LoadBalancerResponseList {
}
loadBalancer := &LoadBalancerResponse{
URL: req.URL.String(),
URL: req.URL.String(),
}
loadBalancerResponses = append(loadBalancerResponses, loadBalancer)
@@ -286,6 +287,12 @@ func (der *Downloader) checkLoadBalancers() *LoadBalancerResponseList {
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
@@ -300,7 +307,7 @@ func (der *Downloader) Execute() error {
var (
isInstance = bii != nil // 是否存在断点信息
status *transfer.DownloadStatus
single = false // 默认开启多线程下载
single = false // 默认开启多线程下载
)
if !isInstance {
bii = &transfer.DownloadInstanceInfo{}
@@ -374,15 +381,27 @@ func (der *Downloader) Execute() error {
var apierr *apierror.ApiError
durl, apierr := der.panClient.GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
DriveId: der.driveId,
FileId: der.fileInfo.FileId,
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 == "" {
if durl == nil || durl.Url == "" || strings.HasPrefix(durl.Url, aliyunpan.IllegalDownloadUrlPrefix) {
logger.Verbosef("无法获取有效的下载链接: %+v\n", durl)
cmdutil.Trigger(der.onCancelEvent)
der.removeInstanceState() // 移除断点续传文件
@@ -397,7 +416,7 @@ func (der *Downloader) Execute() error {
continue
}
logger.Verbosef("work id: %d, download url: %s\n", k, durl)
logger.Verbosef("work id: %d, download url: %v\n", k, durl)
client := requester.NewHTTPClient()
client.SetKeepAlive(true)
client.SetTimeout(10 * time.Minute)
+2
View File
@@ -63,6 +63,8 @@ const (
StatusCodeCanceled
//StatusCodeDownloadUrlExpired 下载链接已过期
StatusCodeDownloadUrlExpired
//StatusCodeIllegalDownloadFile 文件非法,不允许下载
StatusCodeIllegalDownloadFile
)
//GetStatusText 根据状态码获取状态信息
+4
View File
@@ -16,6 +16,7 @@ package downloader
import (
"io"
"os"
"runtime"
)
type (
@@ -32,6 +33,9 @@ type (
// NewDownloaderWriterByFilename 创建下载器数据输出接口, 类似于os.OpenFile
func NewDownloaderWriterByFilename(name string, flag int, perm os.FileMode) (writer Writer, file *os.File, err error) {
if runtime.GOOS == "windows" {
// TODO: name进行特殊字符转换
}
file, err = os.OpenFile(name, flag, perm)
if err != nil {
return
+11 -11
View File
@@ -16,8 +16,8 @@ package uploader
import (
"bufio"
"fmt"
"github.com/tickstep/library-go/requester/rio/speeds"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/requester/rio/speeds"
"io"
"os"
"sync"
@@ -33,13 +33,13 @@ type (
}
fileBlock struct {
readRange transfer.Range
readed int64
readerAt io.ReaderAt
speedsStatRef *speeds.Speeds
readRange transfer.Range
readed int64
readerAt io.ReaderAt
speedsStatRef *speeds.Speeds
globalSpeedsStatRef *speeds.Speeds
rateLimit *speeds.RateLimit
mu sync.Mutex
rateLimit *speeds.RateLimit
mu sync.Mutex
}
bufioFileBlock struct {
@@ -66,11 +66,11 @@ func SplitBlock(fileSize, blockSize int64) (blockList []*BlockState) {
// NewBufioSplitUnit io.ReaderAt实现SplitUnit接口, 有Buffer支持
func NewBufioSplitUnit(readerAt io.ReaderAt, readRange transfer.Range, speedsStat *speeds.Speeds, rateLimit *speeds.RateLimit, globalSpeedsStat *speeds.Speeds) SplitUnit {
su := &fileBlock{
readerAt: readerAt,
readRange: readRange,
speedsStatRef: speedsStat,
readerAt: readerAt,
readRange: readRange,
speedsStatRef: speedsStat,
globalSpeedsStatRef: globalSpeedsStat,
rateLimit: rateLimit,
rateLimit: rateLimit,
}
return &bufioFileBlock{
fileBlock: su,
+10 -1
View File
@@ -13,12 +13,21 @@
// limitations under the License.
package uploader
import "fmt"
var (
UploadUrlExpired = fmt.Errorf("UrlExpired")
UploadPartNotSeq = fmt.Errorf("PartNotSequential")
UploadTerminate = fmt.Errorf("UploadErrorTerminate")
UploadPartAlreadyExist = fmt.Errorf("PartAlreadyExist")
)
type (
// MultiError 多线程上传的错误
MultiError struct {
Err error
// IsRetry 是否重试,
Terminated bool
Terminated bool
NeedStartOver bool // 是否从头开始上传
}
)
+20 -18
View File
@@ -16,11 +16,12 @@ package uploader
import (
"context"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"github.com/tickstep/library-go/requester/rio"
"github.com/tickstep/library-go/requester/rio/speeds"
"github.com/tickstep/aliyunpan/internal/utils"
"sync"
"time"
)
@@ -44,13 +45,13 @@ type (
instanceState *InstanceState
multiUpload MultiUpload // 上传体接口
file rio.ReaderAtLen64 // 上传
config *MultiUploaderConfig
workers workerList
speedsStat *speeds.Speeds
rateLimit *speeds.RateLimit
globalSpeedsStat *speeds.Speeds // 全局速度统计
multiUpload MultiUpload // 上传体接口
file rio.ReaderAtLen64 // 上传
config *MultiUploaderConfig
workers workerList
speedsStat *speeds.Speeds
rateLimit *speeds.RateLimit
globalSpeedsStat *speeds.Speeds // 全局速度统计
executeTime time.Time
finished chan struct{}
@@ -71,12 +72,12 @@ type (
)
// NewMultiUploader 初始化上传
func NewMultiUploader(multiUpload MultiUpload, file rio.ReaderAtLen64, config *MultiUploaderConfig, uploadOpEntity *aliyunpan.CreateFileUploadResult, globalSpeedsStat *speeds.Speeds) *MultiUploader {
func NewMultiUploader(multiUpload MultiUpload, file rio.ReaderAtLen64, config *MultiUploaderConfig, uploadOpEntity *aliyunpan.CreateFileUploadResult, globalSpeedsStat *speeds.Speeds) *MultiUploader {
return &MultiUploader{
multiUpload: multiUpload,
file: file,
config: config,
UploadOpEntity: uploadOpEntity,
multiUpload: multiUpload,
file: file,
config: config,
UploadOpEntity: uploadOpEntity,
globalSpeedsStat: globalSpeedsStat,
}
}
@@ -123,7 +124,7 @@ func (muer *MultiUploader) check() {
}
// Execute 执行上传
func (muer *MultiUploader) Execute() {
func (muer *MultiUploader) Execute() error {
muer.check()
muer.lazyInit()
@@ -136,13 +137,13 @@ func (muer *MultiUploader) Execute() {
// 分配任务
if muer.instanceState != nil {
muer.workers = muer.getWorkerListByInstanceState(muer.instanceState)
uploaderVerbose.Infof("upload task CREATED from instance state\n")
logger.Verboseln("upload task CREATED from instance state\n")
} else {
muer.workers = muer.getWorkerListByInstanceState(&InstanceState{
BlockList: SplitBlock(muer.file.Len(), muer.config.BlockSize),
})
uploaderVerbose.Infof("upload task CREATED: block size: %d, num: %d\n", muer.config.BlockSize, len(muer.workers))
logger.Verboseln("upload task CREATED: block size: %d, num: %d\n", muer.config.BlockSize, len(muer.workers))
}
// 开始上传
@@ -171,6 +172,7 @@ func (muer *MultiUploader) Execute() {
utils.TriggerOnSync(muer.onSuccessEvent)
}
utils.TriggerOnSync(muer.onFinishEvent)
return err
}
// InstanceState 返回断点续传信息
@@ -178,8 +180,8 @@ func (muer *MultiUploader) InstanceState() *InstanceState {
blockStates := make([]*BlockState, 0, len(muer.workers))
for _, wer := range muer.workers {
blockStates = append(blockStates, &BlockState{
ID: wer.id,
Range: wer.splitUnit.Range(),
ID: wer.id,
Range: wer.splitUnit.Range(),
UploadDone: wer.uploadDone,
})
}
+78 -72
View File
@@ -15,8 +15,9 @@ package uploader
import (
"context"
"github.com/tickstep/aliyunpan/internal/waitgroup"
"github.com/oleiade/lane"
"github.com/tickstep/aliyunpan/internal/waitgroup"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"os"
"strconv"
@@ -27,7 +28,7 @@ type (
id int
partOffset int64
splitUnit SplitUnit
uploadDone bool
uploadDone bool
}
workerList []*worker
@@ -66,78 +67,83 @@ func (muer *MultiUploader) upload() (uperr error) {
uploadClient.SetKeepAlive(true)
for {
// 阿里云盘只支持分片按顺序上传,这里正常应该是parallel = 1
// 阿里云盘只支持分片按顺序上传,这里必须是parallel = 1
wg := waitgroup.NewWaitGroup(muer.config.Parallel)
for {
e := uploadDeque.Shift()
if e == nil { // 任务为空
wg.AddDelta()
uperr = nil
e := uploadDeque.Shift()
if e == nil { // 任务为空
break
}
wer := e.(*worker)
go func() { // 异步上传
defer wg.Done()
var (
ctx, cancel = context.WithCancel(context.Background())
doneChan = make(chan struct{})
uploadDone bool
terr error
)
go func() {
if !wer.uploadDone {
logger.Verboseln("begin to upload part: " + strconv.Itoa(wer.id))
uploadDone, terr = muer.multiUpload.UploadFile(ctx, int(wer.id), wer.partOffset, wer.splitUnit.Range().End, wer.splitUnit, uploadClient)
} else {
uploadDone = true
}
close(doneChan)
}()
select { // 监听上传进程,循环阻塞
case <-muer.canceled:
cancel()
return
case <-doneChan:
// continue
logger.Verboseln("multiUpload worker upload file done")
}
cancel()
if terr != nil {
logger.Verboseln("upload file part err: %+v", terr)
if me, ok := terr.(*MultiError); ok {
if me.Terminated { // 终止
muer.closeCanceledOnce.Do(func() { // 只关闭一次
close(muer.canceled)
})
uperr = me.Err
return
} else if me.NeedStartOver {
logger.Verboseln("upload start over: %d\n", wer.id)
// 从头开始上传
muer.closeCanceledOnce.Do(func() { // 只关闭一次
close(muer.canceled)
})
uperr = me.Err
return
}
}
logger.Verboseln("upload err: %s, id: %d\n", terr, wer.id)
wer.splitUnit.Seek(0, os.SEEK_SET)
uploadDeque.Prepend(wer) // 放回上传队列首位
return
}
wer.uploadDone = uploadDone
// 通知更新
if muer.updateInstanceStateChan != nil && len(muer.updateInstanceStateChan) < cap(muer.updateInstanceStateChan) {
muer.updateInstanceStateChan <- struct{}{}
}
}()
wg.Wait()
if uperr != nil {
if uperr == UploadPartNotSeq {
// 分片出现乱序,需要重新上传,取消本次所有剩余的分片的上传
break
}
wer := e.(*worker)
wg.AddDelta()
go func() {
defer wg.Done()
var (
ctx, cancel = context.WithCancel(context.Background())
doneChan = make(chan struct{})
uploadDone bool
terr error
)
go func() {
if !wer.uploadDone {
uploaderVerbose.Info("begin to upload part: " + strconv.Itoa(wer.id))
uploadDone, terr = muer.multiUpload.UploadFile(ctx, int(wer.id), wer.partOffset, wer.splitUnit.Range().End, wer.splitUnit, uploadClient)
} else {
uploadDone = true
}
close(doneChan)
}()
select {
case <-muer.canceled:
cancel()
return
case <-doneChan:
// continue
uploaderVerbose.Info("multiUpload worker upload file done")
}
cancel()
if terr != nil {
if me, ok := terr.(*MultiError); ok {
if me.Terminated { // 终止
muer.closeCanceledOnce.Do(func() { // 只关闭一次
close(muer.canceled)
})
uperr = me.Err
return
} else if me.NeedStartOver {
uploaderVerbose.Warnf("upload start over: %d\n", wer.id)
// 从头开始上传
uploadDeque = lane.NewDeque()
for _,item := range muer.workers {
item.uploadDone = false
uploadDeque.Append(item)
}
return
}
}
uploaderVerbose.Warnf("upload err: %s, id: %d\n", terr, wer.id)
wer.splitUnit.Seek(0, os.SEEK_SET)
uploadDeque.Append(wer)
return
}
wer.uploadDone = uploadDone
// 通知更新
if muer.updateInstanceStateChan != nil && len(muer.updateInstanceStateChan) < cap(muer.updateInstanceStateChan) {
muer.updateInstanceStateChan <- struct{}{}
}
}()
}
wg.Wait()
// 没有任务了
if uploadDeque.Size() == 0 {
break
@@ -165,11 +171,11 @@ func (muer *MultiUploader) upload() (uperr error) {
if allSuccess {
e := muer.multiUpload.CommitFile()
if e != nil {
uploaderVerbose.Warn("upload file commit failed: " + e.Error())
logger.Verboseln("upload file commit failed: " + e.Error())
return e
}
} else {
uploaderVerbose.Warn("upload file not all success: " + muer.UploadOpEntity.FileId)
logger.Verboseln("upload file not all success: " + muer.UploadOpEntity.FileId)
}
return
-6
View File
@@ -14,10 +14,8 @@
package uploader
import (
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"github.com/tickstep/library-go/requester/rio"
"net/http"
@@ -51,10 +49,6 @@ type (
}
)
var (
uploaderVerbose = logger.New("UPLOADER", config.EnvVerbose)
)
// NewUploader 返回 uploader 对象, url: 上传地址, readerlen64: 实现 rio.ReaderLen64 接口的对象, 例如文件
func NewUploader(url string, readerlen64 rio.ReaderLen64) (uploader *Uploader) {
uploader = &Uploader{
@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,13 +19,18 @@ import (
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"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"
"github.com/tickstep/aliyunpan/internal/localfile"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/tickstep/aliyunpan/internal/taskframework"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/requester/rio/speeds"
"io"
"net/http"
@@ -38,6 +43,8 @@ import (
)
type (
FileSourceType string
// DownloadTaskUnit 下载的任务单元
DownloadTaskUnit struct {
taskInfo *taskframework.TaskInfo // 任务信息
@@ -47,7 +54,7 @@ type (
ParentTaskExecutor *taskframework.TaskExecutor
DownloadStatistic *DownloadStatistic // 下载统计
GlobalSpeedsStat *speeds.Speeds // 全局速度统计
GlobalSpeedsStat *speeds.Speeds // 全局速度统计
// 可选项
VerbosePrinter *logger.CmdVerbose
@@ -57,12 +64,16 @@ type (
IsOverwrite bool // 是否覆盖已存在的文件
NoCheck bool // 不校验文件
FilePanPath string // 要下载的网盘文件路径
SavePath string // 文件保存在本地的路径
OriginSaveRootPath string // 文件保存在本地的根目录路径
DriveId string
FilePanPath string // 要下载的网盘文件路径
SavePath string // 文件保存在本地的路径
OriginSaveRootPath string // 文件保存在本地的根目录路径
DriveId string
fileInfo *aliyunpan.FileEntity // 文件或目录详情
fileInfo *aliyunpan.FileEntity // 文件或目录详情
downloadFileSource FileSourceType // 下载项类型,File-普通文件(备份盘/资源库),Album-相册文件
// 下载文件记录器
FileRecorder *log.FileRecorder
}
)
@@ -81,8 +92,20 @@ const (
StrDownloadChecksumFailed = "检测文件有效性失败"
// DefaultDownloadMaxRetry 默认下载失败最大重试次数
DefaultDownloadMaxRetry = 3
// BackupFileSource 备份盘文件
BackupFileSource FileSourceType = "backup"
// ResourceFileSource 资源库文件
ResourceFileSource FileSourceType = "resource"
// AlbumFileSource 相册文件
AlbumFileSource FileSourceType = "album"
)
func (dtu *DownloadTaskUnit) SetFileInfo(fileType FileSourceType, info *aliyunpan.FileEntity) {
dtu.downloadFileSource = fileType
dtu.fileInfo = info
}
func (dtu *DownloadTaskUnit) SetTaskInfo(info *taskframework.TaskInfo) {
dtu.taskInfo = info
}
@@ -93,32 +116,64 @@ func (dtu *DownloadTaskUnit) verboseInfof(format string, a ...interface{}) {
}
}
// download 执行下载
// download 执行下载文件(非目录)
func (dtu *DownloadTaskUnit) download() (err error) {
var (
writer downloader.Writer
file *os.File
)
dtu.Cfg.InstanceStatePath = dtu.SavePath + DownloadSuffix
// 创建下载的目录
// 获取SavePath所在的目录
dir := filepath.Dir(dtu.SavePath)
fileInfo, err := os.Stat(dir)
if err != nil {
// 目录不存在, 创建
err = os.MkdirAll(dir, 0777)
//fileInfo, err := os.Stat(dir)
//if err != nil {
// // 目录不存在, 创建
// err = os.MkdirAll(dir, 0777)
// if err != nil {
// return err
// }
//} else if !fileInfo.IsDir() {
// // SavePath所在的目录不是目录
// return fmt.Errorf("%s, path %s: not a directory", StrDownloadInitError, dir)
//}
// 支持本地符号链接文件,整体逻辑和上面注释代码一致
savePathSymlinkFile := localfile.SymlinkFile{
LogicPath: dtu.SavePath,
RealPath: "",
}
originSaveRootSymlinkFile := localfile.NewSymlinkFile(dtu.OriginSaveRootPath)
suffixPath := localfile.GetSuffixPath(dir, dtu.OriginSaveRootPath)
saveDirPathSymlinkFile, saveDirPathFileInfo, err := localfile.RetrieveRealPathFromLogicSuffixPath(originSaveRootSymlinkFile, suffixPath)
if err != nil && !os.IsExist(err) {
// 本地保存目录不存在,需要创建对应的保存目录
realSaveDirPath := saveDirPathSymlinkFile.RealPath
suffixPath = localfile.GetSuffixPath(filepath.Dir(dtu.SavePath), saveDirPathSymlinkFile.LogicPath) // 获取后缀不存在的路径
if suffixPath != "" {
realSaveDirPath = filepath.Join(realSaveDirPath, suffixPath) // 拼接
}
// 创建保存目录
err = os.MkdirAll(realSaveDirPath, 0777)
if err != nil {
return err
}
} else if !fileInfo.IsDir() {
// 拼接完整的保存文件路径
savePathSymlinkFile.RealPath = filepath.Join(realSaveDirPath, filepath.Base(localfile.CleanPath(dtu.SavePath)))
} else if !saveDirPathFileInfo.IsDir() {
// SavePath所在的目录不是目录
return fmt.Errorf("%s, path %s: not a directory", StrDownloadInitError, dir)
return fmt.Errorf("%s, path %s: not a directory", StrDownloadInitError, saveDirPathSymlinkFile.RealPath)
} else {
// 保存目录已存在,直接拼接成文件完整保存路径
savePathSymlinkFile.RealPath = filepath.Join(saveDirPathSymlinkFile.RealPath, filepath.Base(localfile.CleanPath(dtu.SavePath)))
}
savePathSymlinkFile, _, _ = localfile.RetrieveRealPath(savePathSymlinkFile)
// 下载配置文件存储路径
dtu.Cfg.InstanceStatePath = savePathSymlinkFile.RealPath + DownloadSuffix
// 打开文件
writer, file, err = downloader.NewDownloaderWriterByFilename(dtu.SavePath, os.O_CREATE|os.O_WRONLY, 0666)
writer, file, err = downloader.NewDownloaderWriterByFilename(savePathSymlinkFile.RealPath, os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
return fmt.Errorf("%s, %s", StrDownloadInitError, err)
}
@@ -145,12 +200,12 @@ func (dtu *DownloadTaskUnit) download() (err error) {
if dtu.IsPrintStatus {
// 输出所有的worker状态
var (
tb = cmdtable.NewTable(builder)
tb = cmdtable.NewTable(builder)
)
tb.SetHeader([]string{"#", "status", "range", "left", "speeds", "error"})
workersCallback(func(key int, worker *downloader.Worker) bool {
wrange := worker.GetRange()
tb.Append([]string{fmt.Sprint(worker.ID()), worker.GetStatus().StatusText(), wrange.ShowDetails(), strconv.FormatInt(wrange.Len(), 10), converter.ConvertFileSize(worker.GetSpeedsPerSecond(), 2)+"/s", fmt.Sprint(worker.Err())})
tb.Append([]string{fmt.Sprint(worker.ID()), worker.GetStatus().StatusText(), wrange.ShowDetails(), strconv.FormatInt(wrange.Len(), 10), converter.ConvertFileSize(worker.GetSpeedsPerSecond(), 2) + "/s", fmt.Sprint(worker.Err())})
return true
})
@@ -169,9 +224,11 @@ func (dtu *DownloadTaskUnit) download() (err error) {
}
if dtu.Cfg.ShowProgress {
fmt.Fprintf(builder, "\r[%s] ↓ %s/%s %s/s(%s/s) in %s, left %s ............", dtu.taskInfo.Id(),
downloadedPercentage := fmt.Sprintf("%.2f%%", float64(status.Downloaded())/float64(status.TotalSize())*100)
fmt.Fprintf(builder, "\r[%s] ↓ %s/%s(%s) %s/s(%s/s) in %s, left %s ............", dtu.taskInfo.Id(),
converter.ConvertFileSize(status.Downloaded(), 2),
converter.ConvertFileSize(status.TotalSize(), 2),
downloadedPercentage,
converter.ConvertFileSize(status.SpeedsPerSecond(), 2),
converter.ConvertFileSize(dtu.GlobalSpeedsStat.GetSpeeds(), 2),
status.TimeElapsed()/1e7*1e7, leftStr,
@@ -235,7 +292,7 @@ func (dtu *DownloadTaskUnit) download() (err error) {
return nil
}
//panHTTPClient 获取包含特定User-Agent的HTTPClient
// panHTTPClient 获取包含特定User-Agent的HTTPClient
func (dtu *DownloadTaskUnit) panHTTPClient() (client *requester.HTTPClient) {
client = requester.NewHTTPClient()
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
@@ -249,11 +306,13 @@ func (dtu *DownloadTaskUnit) panHTTPClient() (client *requester.HTTPClient) {
return client
}
// handleError 下载错误处理器
func (dtu *DownloadTaskUnit) handleError(result *taskframework.TaskUnitRunResult) {
switch value := result.Err.(type) {
case *apierror.ApiError:
switch value.ErrCode() {
case apierror.ApiCodeFileNotFoundCode:
case apierror.ApiCodeForbiddenFileInTheRecycleBin:
result.NeedRetry = false
break
default:
@@ -270,10 +329,10 @@ func (dtu *DownloadTaskUnit) handleError(result *taskframework.TaskUnitRunResult
result.NeedRetry = true
}
}
time.Sleep(1*time.Second)
time.Sleep(1 * time.Second)
}
//checkFileValid 检测文件有效性
// checkFileValid 检测文件有效性
func (dtu *DownloadTaskUnit) checkFileValid(result *taskframework.TaskUnitRunResult) (ok bool) {
if dtu.NoCheck {
// 不检测文件有效性
@@ -328,9 +387,28 @@ func (dtu *DownloadTaskUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunRes
}
func (dtu *DownloadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunResult) {
// 执行插件
dtu.pluginCallback("success")
// 下载文件数据记录
if config.Config.FileRecordConfig == "1" {
if dtu.fileInfo.IsFile() {
if dtu.FileRecorder != nil {
dtu.FileRecorder.Append(&log.FileRecordItem{
Status: "成功",
TimeStr: utils.NowTimeStr(),
FileSize: dtu.fileInfo.FileSize,
FilePath: dtu.fileInfo.Path,
})
}
}
}
}
func (dtu *DownloadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunResult) {
// 失败
dtu.pluginCallback("fail")
// 失败
if lastRunResult.Err == nil {
// result中不包含Err, 忽略输出
@@ -340,9 +418,37 @@ func (dtu *DownloadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunRe
fmt.Printf("[%s] %s, %s\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err)
}
func (dtu *DownloadTaskUnit) pluginCallback(result string) {
if dtu.fileInfo == nil {
return
}
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
plugin, _ := pluginManger.GetPlugin()
pluginParam := &plugins.DownloadFileFinishParams{
DriveId: dtu.fileInfo.DriveId,
DriveFilePath: dtu.fileInfo.Path,
DriveFileName: dtu.fileInfo.FileName,
DriveFileSize: dtu.fileInfo.FileSize,
DriveFileType: "file",
DriveFileSha1: dtu.fileInfo.ContentHash,
DriveFileUpdatedAt: dtu.fileInfo.UpdatedAt,
DownloadResult: result,
LocalFilePath: dtu.SavePath,
}
if er := plugin.DownloadFileFinishCallback(plugins.GetContext(config.Config.ActiveUser()), pluginParam); er != nil {
logger.Verboseln("插件DownloadFileFinishCallback调用失败: {}", er)
} else {
logger.Verboseln("插件DownloadFileFinishCallback调用成功")
}
}
func (dtu *DownloadTaskUnit) OnComplete(lastRunResult *taskframework.TaskUnitRunResult) {
}
func (dtu *DownloadTaskUnit) OnCancel(lastRunResult *taskframework.TaskUnitRunResult) {
}
func (dtu *DownloadTaskUnit) RetryWait() time.Duration {
return functions.RetryWait(dtu.taskInfo.Retry())
}
@@ -351,45 +457,106 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
result = &taskframework.TaskUnitRunResult{}
// 获取文件信息
var apierr *apierror.ApiError
if dtu.fileInfo == nil || dtu.taskInfo.Retry() > 0 {
// 没有获取文件信息
// 如果是动态添加的下载任务, 是会写入文件信息的
// 如果该任务重试过, 则应该再获取一次文件信息
dtu.fileInfo, apierr = dtu.PanClient.FileInfoByPath(dtu.DriveId, dtu.FilePanPath)
if apierr != nil {
// 如果不是未登录或文件不存在, 则不重试
result.ResultMessage = "获取下载路径信息错误"
result.Err = apierr
dtu.handleError(result)
return
if dtu.downloadFileSource != AlbumFileSource { // 相簿文件信息是传递进来的,无法在这里获取
if dtu.fileInfo == nil || dtu.taskInfo.Retry() > 0 {
// 没有获取文件信息
// 如果是动态添加的下载任务, 是会写入文件信息的
// 如果该任务重试过, 则应该再获取一次文件信息
dtu.fileInfo, apierr = dtu.PanClient.FileInfoByPath(dtu.DriveId, dtu.FilePanPath)
if apierr != nil {
// 如果不是未登录或文件不存在, 则不重试
result.ResultMessage = "获取下载路径信息错误"
result.Err = apierr
dtu.handleError(result)
return
}
time.Sleep(1 * time.Second)
}
} else {
if dtu.taskInfo.Retry() > 0 {
// 延时避免风控
time.Sleep(2 * time.Second)
}
time.Sleep(1*time.Second)
}
// 输出文件信息
fmt.Print("\n")
fmt.Printf("[%s] ----\n%s\n", dtu.taskInfo.Id(), dtu.fileInfo.String())
// 调用插件
ft := "file"
if dtu.fileInfo.IsFolder() {
ft = "folder"
}
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
plugin, _ := pluginManger.GetPlugin()
localFilePath := strings.TrimPrefix(dtu.SavePath, dtu.OriginSaveRootPath)
localFilePath = strings.TrimPrefix(strings.TrimPrefix(localFilePath, "\\"), "/")
pluginParam := &plugins.DownloadFilePrepareParams{
DriveId: dtu.fileInfo.DriveId,
DriveFilePath: dtu.fileInfo.Path,
DriveFileName: dtu.fileInfo.FileName,
DriveFileSize: dtu.fileInfo.FileSize,
DriveFileType: ft,
DriveFileSha1: dtu.fileInfo.ContentHash,
DriveFileUpdatedAt: dtu.fileInfo.UpdatedAt,
LocalFilePath: localFilePath,
}
if downloadFilePrepareResult, er := plugin.DownloadFilePrepareCallback(plugins.GetContext(config.Config.ActiveUser()), pluginParam); er == nil && downloadFilePrepareResult != nil {
if strings.Compare("yes", downloadFilePrepareResult.DownloadApproved) != 0 {
// skip download this file
fmt.Printf("插件取消了该文件下载: %s\n", dtu.fileInfo.Path)
result.Succeed = false
result.Cancel = true
return
}
if downloadFilePrepareResult.LocalFilePath != "" {
targetSaveRelativePath := strings.TrimPrefix(downloadFilePrepareResult.LocalFilePath, "/")
targetSaveRelativePath = strings.TrimPrefix(targetSaveRelativePath, "\\")
dtu.SavePath = path.Clean(dtu.OriginSaveRootPath + string(os.PathSeparator) + targetSaveRelativePath)
fmt.Printf("插件修改文件下载保存路径为: %s\n", dtu.SavePath)
}
}
// 如果是一个目录, 将子文件和子目录加入队列
if dtu.fileInfo.IsFolder() {
_, err := os.Stat(dtu.SavePath)
//_, err := os.Stat(dtu.SavePath)
//if err != nil && !os.IsExist(err) {
// os.MkdirAll(dtu.SavePath, 0777) // 首先在本地创建目录, 保证空目录也能被保存
//}
// 支持本地符号逻辑文件,整体逻辑等效上面的注释代码
originSaveRootSymlinkFile := localfile.NewSymlinkFile(dtu.OriginSaveRootPath)
suffixPath := localfile.GetSuffixPath(dtu.SavePath, dtu.OriginSaveRootPath)
savePathSymlinkFile, _, err := localfile.RetrieveRealPathFromLogicSuffixPath(originSaveRootSymlinkFile, suffixPath)
if err != nil && !os.IsExist(err) {
os.MkdirAll(dtu.SavePath, 0777) // 首先在本地创建目录, 保证空目录也能被保存
realSavePath := savePathSymlinkFile.RealPath
suffixPath = localfile.GetSuffixPath(dtu.SavePath, savePathSymlinkFile.LogicPath) // 获取后缀不存在的路径
if suffixPath != "" {
realSavePath = filepath.Join(realSavePath, suffixPath)
}
err1 := os.MkdirAll(realSavePath, 0777) // 首先在本地创建目录, 保证空目录也能被保存
if err1 != nil {
result.ResultMessage = "创建目录错误"
result.Succeed = false
result.Err = err1
result.NeedRetry = false
return
}
}
// 获取该目录下的文件列表
fileList,apierr := dtu.PanClient.FileListGetAll(&aliyunpan.FileListParam{
fileList, apierr := dtu.PanClient.FileListGetAll(&aliyunpan.FileListParam{
DriveId: dtu.DriveId,
ParentFileId: dtu.fileInfo.FileId,
})
}, 1000)
if apierr != nil {
// retry one more time
time.Sleep(3*time.Second)
time.Sleep(3 * time.Second)
fileList,apierr = dtu.PanClient.FileListGetAll(&aliyunpan.FileListParam{
fileList, apierr = dtu.PanClient.FileListGetAll(&aliyunpan.FileListParam{
DriveId: dtu.DriveId,
ParentFileId: dtu.fileInfo.FileId,
})
}, 1000)
if apierr != nil {
logger.Verbosef("[%s] get download file list for %s error: %s\n",
dtu.taskInfo.Id(), dtu.FilePanPath, apierr)
@@ -408,11 +575,18 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
result.NeedRetry = true
return
}
time.Sleep(1*time.Second)
time.Sleep(1 * time.Second)
// 创建对应的任务进行下载
for k := range fileList {
fileList[k].Path = path.Join(dtu.FilePanPath, fileList[k].FileName)
// 是否排除下载
if utils.IsExcludeFile(fileList[k].Path, &dtu.Cfg.ExcludeNames) {
fmt.Printf("排除文件: %s\n", fileList[k].Path)
continue
}
if fileList[k].IsFolder() {
logger.Verbosef("[%s] create sub folder download task: %s\n",
dtu.taskInfo.Id(), fileList[k].Path)
@@ -438,7 +612,13 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
fmt.Printf("[%s] 准备下载: %s\n", dtu.taskInfo.Id(), dtu.FilePanPath)
if !dtu.IsOverwrite && FileExist(dtu.SavePath) {
//if !dtu.IsOverwrite && FileExist(dtu.SavePath) {
// fmt.Printf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
// result.Succeed = true // 执行成功
// return
//}
// 支持符号文件,逻辑和注释代码一致
if !dtu.IsOverwrite && SymlinkFileExist(dtu.SavePath, dtu.OriginSaveRootPath) {
fmt.Printf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
result.Succeed = true // 执行成功
return
@@ -464,6 +644,14 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
return result
}
// 文件下载成功,更改文件修改时间
if dtu.downloadFileSource == AlbumFileSource {
// 只有相册文件才需要更改时间
if err := os.Chtimes(dtu.SavePath, utils.ParseTimeStr(dtu.fileInfo.CreatedAt), utils.ParseTimeStr(dtu.fileInfo.CreatedAt)); err != nil {
logger.Verbosef(err.Error())
}
}
// 统计下载
dtu.DownloadStatistic.AddTotalSize(dtu.fileInfo.FileSize)
// 下载成功
+27 -1
View File
@@ -15,6 +15,7 @@ package pandownload
import (
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/internal/localfile"
"os"
)
@@ -26,7 +27,8 @@ func CheckFileValid(filePath string, fileInfo *aliyunpan.FileEntity) error {
return nil
}
// FileExist 检查文件是否存在,
// FileExist 检查文件是否存在
//
// 只有当文件存在, 文件大小不为0或断点续传文件不存在时, 才判断为存在
func FileExist(path string) bool {
if info, err := os.Stat(path); err == nil {
@@ -40,3 +42,27 @@ func FileExist(path string) bool {
return false
}
// SymlinkFileExist 检查文件是否存在
//
// 逻辑和 FileExist 一致,增加符号链接文件的支持
func SymlinkFileExist(fullPath, rootPath string) bool {
originSaveRootSymlinkFile := localfile.NewSymlinkFile(rootPath)
suffixPath := localfile.GetSuffixPath(fullPath, rootPath)
savePathSymlinkFile, savePathFileInfo, err := localfile.RetrieveRealPathFromLogicSuffixPath(originSaveRootSymlinkFile, suffixPath)
if err != nil || savePathFileInfo == nil {
return false
} else {
if savePathFileInfo.IsDir() {
return true
} else {
if savePathFileInfo.Size() == 0 {
return false
}
if _, err = os.Stat(savePathSymlinkFile.RealPath + DownloadSuffix); err != nil {
return true
}
}
}
return false
}
+3 -3
View File
@@ -5,9 +5,9 @@ import (
"encoding/json"
"fmt"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/crypto"
"github.com/tickstep/library-go/getip"
"github.com/tickstep/library-go/ids"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"net/url"
@@ -49,7 +49,7 @@ func NewLoginHelper(webHost string) *LoginHelper {
// GetQRCodeLoginUrl 获取登录二维码链接
func (h *LoginHelper) GetQRCodeLoginUrl(keyStr string) (*QRCodeUrlResult, error) {
if keyStr == "" {
keyStr = ids.GetUniqueId("", 32)
keyStr = utils.GetUniqueKeyStr()
}
fullUrl := strings.Builder{}
ipAddr, err := getip.IPInfoFromTechainBaidu()
@@ -194,7 +194,7 @@ func (h *LoginHelper) ParseSecureRefreshToken(keyStr, secureRefreshToken string)
}()
if len(keyStr) == 0 {
keyStr = ids.GetUniqueId("", 32)
keyStr = utils.GetUniqueKeyStr()
}
if secureRefreshToken == "" {
@@ -1,46 +0,0 @@
// Copyright (c) 2020 tickstep & chenall
//
// 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 panupload
type SyncDb interface {
//读取记录,返回值不会是nil
Get(key string) (ufm *UploadedFileMeta)
//删除单条记录
Del(key string) error
//根据前辍删除数据库记录,比如删除一个目录时可以连同子目录一起删除
DelWithPrefix(prefix string) error
Put(key string, value *UploadedFileMeta) error
Close() error
//读取数据库指定路径前辍的第一条记录(也作为循环获取的初始化,配置Next函数使用)
First(prefix string) (*UploadedFileMeta, error)
//获取指定路径前辍的的下一条记录
Next(prefix string) (*UploadedFileMeta, error)
//是否进行自动数据库清理
//注: 清理规则,所有以 prefix 前辍开头并且未更新的记录都将被清理,只有在必要的时候才开启这个功能。
AutoClean(prefix string, cleanFlag bool)
}
type autoCleanInfo struct {
PreFix string
SyncTime int64
}
func OpenSyncDb(file string, bucket string) (SyncDb, error) {
return openBoltDb(file, bucket)
}
type dbTableField struct {
Path string
Data []byte
}
@@ -1,185 +0,0 @@
// Copyright (c) 2020 tickstep & chenall
//
// 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 panupload
import (
"bytes"
"fmt"
jsoniter "github.com/json-iterator/go"
"github.com/tickstep/bolt"
"github.com/tickstep/library-go/logger"
"time"
)
type boltDB struct {
db *bolt.DB
bucket string
next map[string]*boltDBScan
cleanInfo *autoCleanInfo
}
type boltDBScan struct {
entries []*boltKV
off int
size int
}
type boltKV struct {
k []byte
v []byte
}
func openBoltDb(file string, bucket string) (SyncDb, error) {
db, err := bolt.Open(file + "_bolt.db", 0600, &bolt.Options{Timeout: 5 * time.Second})
if err != nil {
return nil, err
}
logger.Verboseln("open boltDB ok")
return &boltDB{db: db, bucket: bucket, next: make(map[string]*boltDBScan)}, nil
}
func (db *boltDB) Get(key string) (data *UploadedFileMeta) {
data = &UploadedFileMeta{Path: key}
db.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(db.bucket))
if b == nil {
return nil
}
v := b.Get([]byte(key))
return jsoniter.Unmarshal(v, data)
})
return data
}
func (db *boltDB) Del(key string) error {
return db.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(db.bucket))
if b == nil {
return nil
}
return b.Delete([]byte(key))
})
}
func (db *boltDB) AutoClean(prefix string, cleanFlag bool) {
if !cleanFlag {
db.cleanInfo = nil
} else if db.cleanInfo == nil {
db.cleanInfo = &autoCleanInfo{
PreFix: prefix,
SyncTime: time.Now().Unix(),
}
}
}
func (db *boltDB) clean() (count uint) {
for ufm, err := db.First(db.cleanInfo.PreFix); err == nil; ufm, err = db.Next(db.cleanInfo.PreFix) {
if ufm.LastSyncTime != db.cleanInfo.SyncTime {
db.DelWithPrefix(ufm.Path)
}
}
return
}
func (db *boltDB) DelWithPrefix(prefix string) error {
return db.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(db.bucket))
if b == nil {
return nil
}
c := b.Cursor()
for k, _ := c.Seek([]byte(prefix)); k != nil && bytes.HasPrefix(k, []byte(prefix)); k, _ = c.Next() {
b.Delete(k)
}
return nil
})
}
func (db *boltDB) First(prefix string) (*UploadedFileMeta, error) {
db.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(db.bucket))
if b == nil {
return nil
}
c := b.Cursor()
db.next[prefix] = &boltDBScan{
entries: []*boltKV{},
off: 0,
size: 0,
}
for k, v := c.Seek([]byte(prefix)); k != nil && bytes.HasPrefix(k, []byte(prefix)); k, v = c.Next() {
//fmt.Printf("key=%s, value=%s\n", k, v)
if len(k) > 0 {
db.next[prefix].entries = append(db.next[prefix].entries, &boltKV{
k: k,
v: v,
})
}
}
db.next[prefix].off = 0
db.next[prefix].size = len(db.next[prefix].entries)
return nil
})
return db.Next(prefix)
}
func (db *boltDB) Next(prefix string) (*UploadedFileMeta, error) {
data := &UploadedFileMeta{}
if _,ok := db.next[prefix]; ok {
if db.next[prefix].off >= db.next[prefix].size {
return nil, fmt.Errorf("no any more record")
}
kv := db.next[prefix].entries[db.next[prefix].off]
db.next[prefix].off++
if kv != nil {
jsoniter.Unmarshal(kv.v, &data)
data.Path = string(kv.k)
return data, nil
}
}
return nil, fmt.Errorf("no any more record")
}
func (db *boltDB) Put(key string, value *UploadedFileMeta) error {
if db.cleanInfo != nil {
value.LastSyncTime = db.cleanInfo.SyncTime
}
return db.db.Update(func(tx *bolt.Tx) error {
data, err := jsoniter.Marshal(value)
if err != nil {
return err
}
b := tx.Bucket([]byte(db.bucket))
if b == nil {
b,err = tx.CreateBucket([]byte(db.bucket))
if err != nil {
return err
}
}
return b.Put([]byte(key), data)
})
}
func (db *boltDB) Close() error {
if db.cleanInfo != nil {
db.clean()
}
if db.db != nil {
return db.db.Close()
}
return nil
}
+66 -47
View File
@@ -16,12 +16,12 @@ package panupload
import (
"context"
"encoding/xml"
"fmt"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"io"
"net/http"
"strconv"
"time"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
@@ -33,35 +33,17 @@ type (
PanUpload struct {
panClient *aliyunpan.PanClient
targetPath string
driveId string
driveId string
// 网盘上传参数
uploadOpEntity *aliyunpan.CreateFileUploadResult
useInternalUrl bool
}
UploadedFileMeta struct {
IsFolder bool `json:"isFolder,omitempty"` // 是否目录
Path string `json:"-"` // 本地路径,不记录到数据库
SHA1 string `json:"sha1,omitempty"` // 文件的 SHA1
FileId string `json:"id,omitempty"` //文件、目录ID
ParentId string `json:"parentId,omitempty"` //父文件夹ID
Size int64 `json:"length,omitempty"` // 文件大小
ModTime int64 `json:"modtime,omitempty"` // 修改日期
LastSyncTime int64 `json:"synctime,omitempty"` //最后同步时间
}
EmptyReaderLen64 struct {
}
)
var (
uploadUrlExpired = fmt.Errorf("UrlExpired")
uploadPartNotSeq = fmt.Errorf("PartNotSequential")
uploadTerminate = fmt.Errorf("UploadErrorTerminate")
uploadPartAlreadyExist = fmt.Errorf("PartAlreadyExist")
)
func (e EmptyReaderLen64) Read(p []byte) (n int, err error) {
return 0, io.EOF
}
@@ -72,9 +54,9 @@ func (e EmptyReaderLen64) Len() int64 {
func NewPanUpload(panClient *aliyunpan.PanClient, targetPath, driveId string, uploadOpEntity *aliyunpan.CreateFileUploadResult, useInternalUrl bool) uploader.MultiUpload {
return &PanUpload{
panClient: panClient,
targetPath: targetPath,
driveId: driveId,
panClient: panClient,
targetPath: targetPath,
driveId: driveId,
uploadOpEntity: uploadOpEntity,
useInternalUrl: useInternalUrl,
}
@@ -98,10 +80,10 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
if pu.useInternalUrl {
uploadUrl = pu.uploadOpEntity.PartInfoList[partseq].InternalUploadURL
}
if isUrlExpired(uploadUrl) {
if IsUrlExpired(uploadUrl) {
// get renew upload url
infoList := make([]aliyunpan.FileUploadPartInfoParam, 0)
for _,item := range pu.uploadOpEntity.PartInfoList {
for _, item := range pu.uploadOpEntity.PartInfoList {
infoList = append(infoList, aliyunpan.FileUploadPartInfoParam{
PartNumber: item.PartNumber,
})
@@ -114,8 +96,9 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
}
newUploadInfo, err := pu.panClient.GetUploadUrl(refreshUploadParam)
if err != nil {
logger.Verboseln(err)
return false, &uploader.MultiError{
Err: uploadUrlExpired,
Err: uploader.UploadUrlExpired,
Terminated: false,
}
}
@@ -127,6 +110,7 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
var resp *http.Response
var respError error = nil
respErr = nil
var err error
// do http upload request
if uploadClient == nil {
@@ -134,11 +118,14 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
uploadClient.SetTimeout(0)
uploadClient.SetKeepAlive(true)
}
resp, _ = uploadClient.Req(httpMethod, fullUrl, r, headers)
resp, err = uploadClient.Req(httpMethod, fullUrl, r, headers)
if err != nil {
logger.Verbosef("分片上传出错: 分片%d => %s\n", partseq, err)
}
if resp != nil {
if blen, e := strconv.Atoi(resp.Header.Get("content-length")); e == nil {
if blen > 0 {
if blen > 0 {
buf := make([]byte, blen)
resp.Body.Read(buf)
logger.Verbosef("分片上传出错: 分片%d => %s\n", partseq, string(buf))
@@ -146,25 +133,25 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
errResp := &apierror.ErrorXmlResp{}
if err := xml.Unmarshal(buf, errResp); err == nil {
if errResp.Code != "" {
if "PartNotSequential" == errResp.Code {
respError = uploadPartNotSeq
if "PartNotSequential" == errResp.Code || "NoSuchUpload" == errResp.Code {
respError = uploader.UploadPartNotSeq
respErr = &uploader.MultiError{
Err: uploadPartNotSeq,
Terminated: false,
Err: uploader.UploadPartNotSeq,
Terminated: false,
NeedStartOver: true,
}
return resp, respError
} else if "AccessDenied" == errResp.Code && "Request has expired." == errResp.Message {
respError = uploadUrlExpired
respError = uploader.UploadUrlExpired
respErr = &uploader.MultiError{
Err: uploadUrlExpired,
Err: uploader.UploadUrlExpired,
Terminated: false,
}
return resp, respError
} else if "PartAlreadyExist" == errResp.Code {
respError = uploadPartAlreadyExist
respError = uploader.UploadPartAlreadyExist
respErr = &uploader.MultiError{
Err: uploadPartAlreadyExist,
Err: uploader.UploadPartAlreadyExist,
Terminated: false,
}
return resp, respError
@@ -179,13 +166,13 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
// 不可恢复的错误
switch resp.StatusCode {
case 400, 401, 403, 413, 600:
respError = uploadTerminate
respError = uploader.UploadTerminate
respErr = &uploader.MultiError{
Terminated: true,
}
}
} else {
respError = uploadTerminate
respError = uploader.UploadTerminate
respErr = &uploader.MultiError{
Terminated: true,
}
@@ -201,13 +188,13 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
apiError := pu.panClient.UploadFileData(uploadUrl, uploadFunc)
if respErr != nil {
if respErr.Err == uploadUrlExpired {
if respErr.Err == uploader.UploadUrlExpired {
// URL过期,获取新的URL
guur, er := pu.panClient.GetUploadUrl(&aliyunpan.GetUploadUrlParam{
DriveId: pu.driveId,
FileId: pu.uploadOpEntity.FileId,
UploadId: pu.uploadOpEntity.UploadId,
PartInfoList: []aliyunpan.FileUploadPartInfoParam{{PartNumber:(partseq+1)}}, // 阿里云盘partNum从1开始计数,partSeq从0开始
DriveId: pu.driveId,
FileId: pu.uploadOpEntity.FileId,
UploadId: pu.uploadOpEntity.UploadId,
PartInfoList: []aliyunpan.FileUploadPartInfoParam{{PartNumber: (partseq + 1)}}, // 阿里云盘partNum从1开始计数,partSeq从0开始
})
if er != nil {
return false, &uploader.MultiError{
@@ -222,11 +209,11 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
uploadUrl = pu.uploadOpEntity.PartInfoList[partseq].InternalUploadURL
}
apiError = pu.panClient.UploadFileData(uploadUrl, uploadFunc)
} else if respErr.Err == uploadPartAlreadyExist {
} else if respErr.Err == uploader.UploadPartAlreadyExist {
// already upload
// success
return true, nil
} else if respErr.Err == uploadPartNotSeq {
} else if respErr.Err == uploader.UploadPartNotSeq {
// 上传分片乱序了,需要重新从0分片开始上传
// 先直接返回,后续再优化
return false, respErr
@@ -247,12 +234,44 @@ func (pu *PanUpload) CommitFile() (cerr error) {
var er *apierror.ApiError
_, er = pu.panClient.CompleteUploadFile(&aliyunpan.CompleteUploadFileParam{
DriveId: pu.driveId,
FileId: pu.uploadOpEntity.FileId,
DriveId: pu.driveId,
FileId: pu.uploadOpEntity.FileId,
UploadId: pu.uploadOpEntity.UploadId,
})
if er != nil && er.Code == apierror.ApiCodeDeviceSessionSignatureInvalid {
_, e := pu.panClient.CreateSession(nil)
if e == nil {
// retry
_, er = pu.panClient.CompleteUploadFile(&aliyunpan.CompleteUploadFileParam{
DriveId: pu.driveId,
FileId: pu.uploadOpEntity.FileId,
UploadId: pu.uploadOpEntity.UploadId,
})
} else {
logger.Verboseln("CreateSession failed")
}
}
if er != nil {
return er
}
// 视频文件触发云端转码请求
pu.triggerVideoTranscodeAction()
return nil
}
// TriggerVideoTranscodeAction 触发视频文件转码成功
func (pu *PanUpload) triggerVideoTranscodeAction() {
// 视频文件触发云端转码请求
if pu.uploadOpEntity != nil && IsVideoFile(pu.uploadOpEntity.FileName) {
time.Sleep(3 * time.Second)
_, er1 := pu.panClient.VideoGetPreviewPlayInfo(&aliyunpan.VideoGetPreviewPlayInfoParam{
DriveId: pu.driveId,
FileId: pu.uploadOpEntity.FileId,
})
if er1 == nil {
logger.Verboseln("触发视频文件转码成功:" + pu.uploadOpEntity.FileName)
}
}
}
@@ -111,7 +111,7 @@ func (ud *UploadingDatabase) UpdateUploading(meta *localfile.LocalFileMeta, stat
if uploading.LocalFileMeta == nil {
continue
}
if uploading.LocalFileMeta.EqualLengthMD5(meta) || uploading.LocalFileMeta.Path == meta.Path {
if uploading.LocalFileMeta.EqualLengthMD5(meta) || uploading.LocalFileMeta.Path.LogicPath == meta.Path.LogicPath {
ud.UploadingList[k].State = state
return
}
@@ -161,7 +161,7 @@ func (ud *UploadingDatabase) Search(meta *localfile.LocalFileMeta) *uploader.Ins
if uploading.LocalFileMeta.EqualLengthSHA1(meta) {
return uploading.State
}
if uploading.LocalFileMeta.Path == meta.Path {
if uploading.LocalFileMeta.Path.LogicPath == meta.Path.LogicPath {
// 移除旧的信息
// 目前只是比较了文件大小
if meta.Length != uploading.LocalFileMeta.Length {
@@ -190,7 +190,7 @@ func (ud *UploadingDatabase) clearModTimeChange() {
continue
}
info, err := os.Stat(uploading.LocalFileMeta.Path)
info, err := os.Stat(uploading.LocalFileMeta.Path.RealPath)
if err != nil {
ud.deleteIndex(i)
i--
+141 -79
View File
@@ -15,6 +15,8 @@ package panupload
import (
"fmt"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/tickstep/library-go/logger"
"os"
"path"
@@ -48,7 +50,6 @@ type (
SavePath string // 保存路径
DriveId string // 网盘ID,例如:文件网盘,相册网盘
FolderCreateMutex *sync.Mutex
FolderSyncDb SyncDb //文件备份状态数据库
PanClient *aliyunpan.PanClient
UploadingDatabase *UploadingDatabase // 数据库
@@ -63,14 +64,18 @@ type (
panFile string
state *uploader.InstanceState
ShowProgress bool
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
ShowProgress bool
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
IsSkipSameName bool // 跳过已存在的文件,即使文件内容不一致(不检查SHA1)
// 是否使用内置链接
UseInternalUrl bool
// 全局速度统计
GlobalSpeedsStat *speeds.Speeds
// 上传文件记录器
FileRecorder *log.FileRecorder
}
)
@@ -118,11 +123,11 @@ func (utu *UploadTaskUnit) prepareFile() {
return
}
if utu.LocalFileChecksum.Length > MaxRapidUploadSize {
fmt.Printf("[%s] 文件超过20GB, 无法使用秒传功能, 跳过秒传...\n", utu.taskInfo.Id())
utu.Step = StepUploadUpload
return
}
//if utu.LocalFileChecksum.Length > MaxRapidUploadSize {
// fmt.Printf("[%s] 文件超过20GB, 无法使用秒传功能, 跳过秒传...\n", utu.taskInfo.Id())
// utu.Step = StepUploadUpload
// return
//}
// 下一步: 秒传
utu.Step = StepUploadRapidUpload
}
@@ -175,9 +180,11 @@ func (utu *UploadTaskUnit) upload() (result *taskframework.TaskUnitRunResult) {
}
if utu.ShowProgress {
fmt.Printf("\r[%s] ↑ %s/%s %s/s(%s/s) in %s ............", utu.taskInfo.Id(),
uploadedPercentage := fmt.Sprintf("%.2f%%", float64(status.Uploaded())/float64(status.TotalSize())*100)
fmt.Printf("\r[%s] ↑ %s/%s(%s) %s/s(%s/s) in %s ............", utu.taskInfo.Id(),
converter.ConvertFileSize(status.Uploaded(), 2),
converter.ConvertFileSize(status.TotalSize(), 2),
uploadedPercentage,
converter.ConvertFileSize(status.SpeedsPerSecond(), 2),
converter.ConvertFileSize(utu.GlobalSpeedsStat.GetSpeeds(), 2),
status.TimeElapsed(),
@@ -217,8 +224,12 @@ func (utu *UploadTaskUnit) upload() (result *taskframework.TaskUnitRunResult) {
}
return
})
muer.Execute()
er := muer.Execute()
if er != nil {
result.ResultMessage = StrUploadFailed
result.NeedRetry = true
result.Err = er
}
return
}
@@ -233,41 +244,56 @@ func (utu *UploadTaskUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunResul
}
func (utu *UploadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunResult) {
//文件上传成功
if utu.FolderSyncDb == nil || lastRunResult == ResultLocalFileNotUpdated { //不需要更新数据库
return
}
ufm := &UploadedFileMeta{
IsFolder: false,
SHA1: utu.LocalFileChecksum.SHA1,
ModTime: utu.LocalFileChecksum.ModTime,
Size: utu.LocalFileChecksum.Length,
}
// 执行插件
utu.pluginCallback("success")
if utu.LocalFileChecksum.UploadOpEntity != nil {
ufm.FileId = utu.LocalFileChecksum.UploadOpEntity.FileId
ufm.ParentId = utu.LocalFileChecksum.UploadOpEntity.ParentFileId
} else {
efi, _ := utu.PanClient.FileInfoByPath(utu.DriveId, utu.SavePath)
if efi != nil {
ufm.FileId = efi.FileId
ufm.ParentId = efi.ParentFileId
}
// 上传文件数据记录
if config.Config.FileRecordConfig == "1" {
utu.FileRecorder.Append(&log.FileRecordItem{
Status: "成功",
TimeStr: utils.NowTimeStr(),
FileSize: utu.LocalFileChecksum.LocalFileMeta.Length,
FilePath: utu.LocalFileChecksum.Path.LogicPath,
})
}
utu.FolderSyncDb.Put(utu.SavePath, ufm)
}
func (utu *UploadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunResult) {
// 失败
utu.pluginCallback("fail")
}
var ResultLocalFileNotUpdated = &taskframework.TaskUnitRunResult{ResultCode: 1, Succeed: true, ResultMessage: "本地文件未更新,无需上传!"}
var ResultUpdateLocalDatabase = &taskframework.TaskUnitRunResult{ResultCode: 2, Succeed: true, ResultMessage: "本地文件和云端文件MD5一致,无需上传!"}
func (utu *UploadTaskUnit) pluginCallback(result string) {
if utu.LocalFileChecksum == nil {
return
}
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
plugin, _ := pluginManger.GetPlugin()
_, fileName := filepath.Split(utu.LocalFileChecksum.Path.LogicPath)
pluginParam := &plugins.UploadFileFinishParams{
LocalFilePath: utu.LocalFileChecksum.Path.LogicPath,
LocalFileName: fileName,
LocalFileSize: utu.LocalFileChecksum.LocalFileMeta.Length,
LocalFileType: "file",
LocalFileUpdatedAt: time.Unix(utu.LocalFileChecksum.LocalFileMeta.ModTime, 0).Format("2006-01-02 15:04:05"),
LocalFileSha1: utu.LocalFileChecksum.LocalFileMeta.SHA1,
UploadResult: result,
DriveId: utu.DriveId,
DriveFilePath: utu.panDir + "/" + utu.panFile,
}
if er := plugin.UploadFileFinishCallback(plugins.GetContext(config.Config.ActiveUser()), pluginParam); er != nil {
logger.Verboseln("插件UploadFileFinishCallback调用失败: {}", er)
} else {
logger.Verboseln("插件UploadFileFinishCallback调用成功")
}
}
func (utu *UploadTaskUnit) OnComplete(lastRunResult *taskframework.TaskUnitRunResult) {
// 任务结束,可能成功也可能失败
}
func (utu *UploadTaskUnit) OnCancel(lastRunResult *taskframework.TaskUnitRunResult) {
}
func (utu *UploadTaskUnit) RetryWait() time.Duration {
return functions.RetryWait(utu.taskInfo.Retry())
}
@@ -283,7 +309,7 @@ func (utu *UploadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
timeStart := time.Now()
result = &taskframework.TaskUnitRunResult{}
fmt.Printf("[%s] %s 准备上传: %s => %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path, utu.SavePath)
fmt.Printf("[%s] %s 准备上传: %s => %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path.LogicPath, utu.SavePath)
defer func() {
var msg string
@@ -302,19 +328,17 @@ func (utu *UploadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
var apierr *apierror.ApiError
var rs *aliyunpan.MkdirResult
var efi *aliyunpan.FileEntity
var appCreateUploadFileParam *aliyunpan.CreateFileUploadParam
var sha1Str string
var contentHashName string
var checkNameMode string
var saveFilePath string
var testFileMeta = &UploadedFileMeta{}
var uploadOpEntity *aliyunpan.CreateFileUploadResult
var proofCode = ""
var localFileInfo os.FileInfo
var localFile *os.File
timeStart2 := time.Now()
timeStart3 := time.Now()
timeStart4 := time.Now()
var newBlockSize int64
switch utu.Step {
case StepUploadPrepareUpload:
@@ -327,69 +351,83 @@ func (utu *UploadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
StepUploadPrepareUpload:
// 创建上传任务
if utu.FolderSyncDb != nil {
//启用了备份功能,强制使用覆盖同名文件功能
utu.IsOverwrite = true
testFileMeta = utu.FolderSyncDb.Get(utu.SavePath)
}
// 创建云盘文件夹
timeStart2 = time.Now()
// utu.FolderCreateMutex.Lock()
saveFilePath = path.Dir(utu.SavePath)
if saveFilePath != "/" {
utu.FolderCreateMutex.Lock()
fmt.Printf("[%s] %s 正在检测和创建云盘文件夹: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), saveFilePath)
//同步功能先尝试从数据库获取
if utu.FolderSyncDb != nil {
timeStart3 = time.Now()
utu.FolderCreateMutex.Lock()
if test := utu.FolderSyncDb.Get(saveFilePath); test.FileId != "" && test.IsFolder {
rs = &aliyunpan.MkdirResult{FileId: test.FileId}
fe, apierr1 := utu.PanClient.FileInfoByPath(utu.DriveId, saveFilePath)
time.Sleep(1 * time.Second)
needToCreateFolder := false
if apierr1 != nil && apierr1.Code == apierror.ApiCodeFileNotFoundCode {
needToCreateFolder = true
} else {
if fe == nil {
needToCreateFolder = true
} else {
rs = &aliyunpan.MkdirResult{}
rs.FileId = fe.FileId
}
utu.FolderCreateMutex.Unlock()
logger.Verbosef("[%s] %s 检测和创建云盘文件夹完毕[from db], 耗时 %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utils.ConvertTime(time.Now().Sub(timeStart3)))
}
if rs == nil {
timeStart4 = time.Now()
if needToCreateFolder {
logger.Verbosef("[%s] %s 创建云盘文件夹: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), saveFilePath)
utu.FolderCreateMutex.Lock()
// rs, apierr = utu.PanClient.MkdirRecursive(utu.DriveId, "", "", 0, strings.Split(path.Clean(saveFilePath), "/"))
// 可以直接创建的,不用循环创建
rs, apierr = utu.PanClient.Mkdir(utu.DriveId, "root", saveFilePath)
utu.FolderCreateMutex.Unlock()
if apierr != nil && apierr.Code == apierror.ApiCodeDeviceSessionSignatureInvalid {
_, e := utu.PanClient.CreateSession(nil)
if e == nil {
// retry
rs, apierr = utu.PanClient.Mkdir(utu.DriveId, "root", saveFilePath)
} else {
logger.Verboseln("CreateSession failed")
}
}
if apierr != nil || rs.FileId == "" {
result.Err = apierr
result.ResultMessage = "创建云盘文件夹失败"
utu.FolderCreateMutex.Unlock()
return
}
logger.Verbosef("[%s] %s 创建云盘文件夹, 耗时 %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utils.ConvertTime(time.Now().Sub(timeStart4)))
logger.Verbosef("[%s] %s 创建云盘文件夹成功\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
}
utu.FolderCreateMutex.Unlock()
} else {
rs = &aliyunpan.MkdirResult{}
rs.FileId = ""
}
// time.Sleep(time.Duration(2) * time.Second)
// utu.FolderCreateMutex.Unlock()
logger.Verbosef("[%s] %s 检测和创建云盘文件夹完毕, 耗时 %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utils.ConvertTime(time.Now().Sub(timeStart2)))
time.Sleep(time.Duration(2) * time.Second)
sha1Str = ""
proofCode = ""
contentHashName = "sha1"
checkNameMode = "auto_rename"
// 如果启用了 覆盖/跳过 已存在的文件,则需要提前检查文件是否存在
if utu.IsOverwrite || utu.IsSkipSameName {
efi, apierr = utu.PanClient.FileInfoByPath(utu.DriveId, utu.SavePath)
if apierr != nil && apierr.Code != apierror.ApiCodeFileNotFoundCode {
result.Err = apierr
result.ResultMessage = "检测同名文件失败"
return
}
}
if utu.IsSkipSameName {
if efi != nil && efi.FileId != "" {
result.Succeed = true
result.Extra = efi
fmt.Printf("[%s] %s 检测到同名文件,跳过上传: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
return
}
}
if !utu.NoRapidUpload {
// 计算文件SHA1
fmt.Printf("[%s] %s 正在计算文件SHA1: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path)
fmt.Printf("[%s] %s 正在计算文件SHA1: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path.LogicPath)
utu.LocalFileChecksum.Sum(localfile.CHECKSUM_SHA1)
if testFileMeta.SHA1 == utu.LocalFileChecksum.SHA1 {
return ResultUpdateLocalDatabase
}
sha1Str = utu.LocalFileChecksum.SHA1
if utu.LocalFileChecksum.Length == 0 {
sha1Str = aliyunpan.DefaultZeroSizeFileContentHash
}
// proof code
localFile, _ = os.Open(utu.LocalFileChecksum.Path)
localFile, _ = os.Open(utu.LocalFileChecksum.Path.RealPath)
localFileInfo, _ = localFile.Stat()
proofCode = aliyunpan.CalcProofCode(utu.PanClient.GetAccessToken(), rio.NewFileReaderAtLen64(localFile), localFileInfo.Size())
localFile.Close()
@@ -402,12 +440,6 @@ StepUploadPrepareUpload:
if utu.IsOverwrite {
// 标记覆盖旧同名文件
// 检查同名文件是否存在
efi, apierr := utu.PanClient.FileInfoByPath(utu.DriveId, utu.SavePath)
if apierr != nil && apierr.Code != apierror.ApiCodeFileNotFoundCode {
result.Err = apierr
result.ResultMessage = "检测同名文件失败"
return
}
if efi != nil && efi.FileId != "" {
if strings.ToUpper(efi.ContentHash) == strings.ToUpper(sha1Str) {
result.Succeed = true
@@ -425,14 +457,21 @@ StepUploadPrepareUpload:
return
}
time.Sleep(time.Duration(500) * time.Millisecond)
fmt.Printf("[%s] %s 检测到同名文件,已移动到回收站: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
fmt.Printf("[%s] %s 检测到同名文件,文件内容不一致,已将旧文件移动到回收站: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
}
}
// 自动调整BlockSize大小
newBlockSize = utils.ResizeUploadBlockSize(utu.LocalFileChecksum.Length, utu.BlockSize)
if newBlockSize != utu.BlockSize {
logger.Verboseln("resize upload block size to: " + converter.ConvertFileSize(newBlockSize, 2))
utu.BlockSize = newBlockSize
}
// 创建上传任务
appCreateUploadFileParam = &aliyunpan.CreateFileUploadParam{
DriveId: utu.DriveId,
Name: filepath.Base(utu.LocalFileChecksum.Path),
Name: filepath.Base(utu.SavePath),
Size: utu.LocalFileChecksum.Length,
ContentHash: sha1Str,
ContentHashName: contentHashName,
@@ -444,9 +483,23 @@ StepUploadPrepareUpload:
}
uploadOpEntity, apierr = utu.PanClient.CreateUploadFile(appCreateUploadFileParam)
if apierr != nil && apierr.Code == apierror.ApiCodeDeviceSessionSignatureInvalid {
_, e := utu.PanClient.CreateSession(nil)
if e == nil {
// retry
uploadOpEntity, apierr = utu.PanClient.CreateUploadFile(appCreateUploadFileParam)
} else {
logger.Verboseln("CreateSession failed")
}
}
if apierr != nil {
result.Err = apierr
result.ResultMessage = "创建上传任务失败:" + apierr.Error()
if apierr.Code == apierror.ApiCodeTooManyRequests || apierr.Code == apierror.ApiCodeBadGateway {
logger.Verboseln("create upload file error: " + result.ResultMessage)
// 重试
result.NeedRetry = true
}
return
}
@@ -466,6 +519,15 @@ stepUploadRapidUpload:
stepUploadUpload:
// 正常上传流程
uploadResult := utu.upload()
if uploadResult != nil && uploadResult.Err != nil {
if uploadResult.Err == uploader.UploadPartNotSeq {
fmt.Printf("[%s] %s 文件分片上传顺序错误,开始重新上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
// 需要重新从0开始上传
uploadResult = nil
utu.LocalFileChecksum.UploadOpEntity = nil
utu.state = nil
goto StepUploadPrepareUpload
}
}
return uploadResult
}
+24 -6
View File
@@ -18,7 +18,9 @@ import (
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"net/url"
"path"
"strconv"
"strings"
"time"
)
@@ -45,18 +47,34 @@ func getBlockSize(fileSize int64) int64 {
return MinUploadBlockSize
}
// isUrlExpired 上传链接是否已过期。过期返回True
func isUrlExpired(urlStr string) bool {
// IsUrlExpired 上传链接是否已过期。过期返回True
func IsUrlExpired(urlStr string) bool {
u, err := url.Parse(urlStr)
if err != nil {
return true
}
expiredTimeSecStr := u.Query().Get("x-oss-expires")
expiredTimeSec,_ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
if (time.Now().Unix() - 10) >= expiredTimeSec {
expiredTimeSec, _ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
if (expiredTimeSec - time.Now().Unix()) <= 300 { // 小于5分钟
// expired
return true
}
return false
}
}
func IsVideoFile(fileName string) bool {
if fileName == "" {
return false
}
extName := strings.ToLower(path.Ext(fileName))
if strings.Index(extName, ".") == 0 {
extName = strings.TrimPrefix(extName, ".")
}
extList := config.Config.GetVideoExtensionList()
for _, ext := range extList {
if ext == extName {
return true
}
}
return false
}
+1 -3
View File
@@ -14,7 +14,6 @@
package functions
import (
"github.com/tickstep/library-go/expires"
"sync/atomic"
"time"
)
@@ -36,9 +35,8 @@ func (s *Statistic) TotalSize() int64 {
func (s *Statistic) StartTimer() {
s.startTime = time.Now()
expires.StripMono(&s.startTime)
}
func (s *Statistic) Elapsed() time.Duration {
return time.Now().Sub(s.startTime)
}
}
+3 -3
View File
@@ -43,11 +43,11 @@ func (lfm *LocalFileMeta) EqualLengthSHA1(m *LocalFileMeta) bool {
// CompleteAbsPath 补齐绝对路径
func (lfm *LocalFileMeta) CompleteAbsPath() {
if filepath.IsAbs(lfm.Path) {
if filepath.IsAbs(lfm.Path.LogicPath) {
return
}
absPath, err := filepath.Abs(lfm.Path)
absPath, err := filepath.Abs(lfm.Path.LogicPath)
if err != nil {
return
}
@@ -55,7 +55,7 @@ func (lfm *LocalFileMeta) CompleteAbsPath() {
if os.PathSeparator == '\\' {
absPath = strings.ReplaceAll(absPath, "\\", "/")
}
lfm.Path = absPath
lfm.Path.LogicPath = absPath
}
// GetFileSum 获取文件的大小, md5, crc32
+15 -11
View File
@@ -46,12 +46,12 @@ const (
type (
// LocalFileMeta 本地文件元信息
LocalFileMeta struct {
Path string `json:"path,omitempty"` // 本地路径
Length int64 `json:"length,omitempty"` // 文件大小
MD5 string `json:"md5,omitempty"` // 文件的 md5
CRC32 uint32 `json:"crc32,omitempty"` // 文件的 crc32
SHA1 string `json:"sha1,omitempty"` // 文件的 sha1
ModTime int64 `json:"modtime"` // 修改日期
Path SymlinkFile `json:"path,omitempty"` // 本地路径
Length int64 `json:"length,omitempty"` // 文件大小
MD5 string `json:"md5,omitempty"` // 文件的 md5
CRC32 uint32 `json:"crc32,omitempty"` // 文件的 crc32
SHA1 string `json:"sha1,omitempty"` // 文件的 sha1
ModTime int64 `json:"modtime"` // 修改日期
// 网盘上传参数
UploadOpEntity *aliyunpan.CreateFileUploadResult `json:"uploadOpEntity"`
@@ -69,14 +69,18 @@ type (
}
)
func NewLocalFileEntity(localPath string) *LocalFileEntity {
return NewLocalFileEntityWithBufSize(localPath, DefaultBufSize)
func NewLocalSymlinkFileEntity(file SymlinkFile) *LocalFileEntity {
return NewLocalFileEntityWithBufSize(file, DefaultBufSize)
}
func NewLocalFileEntityWithBufSize(localPath string, bufSize int) *LocalFileEntity {
func NewLocalFileEntity(localPath string) *LocalFileEntity {
return NewLocalFileEntityWithBufSize(NewSymlinkFile(localPath), DefaultBufSize)
}
func NewLocalFileEntityWithBufSize(file SymlinkFile, bufSize int) *LocalFileEntity {
return &LocalFileEntity{
LocalFileMeta: LocalFileMeta{
Path: localPath,
Path: file,
},
bufSize: bufSize,
}
@@ -89,7 +93,7 @@ func (lfc *LocalFileEntity) OpenPath() error {
}
var err error
lfc.file, err = os.Open(lfc.Path)
lfc.file, err = os.Open(lfc.Path.RealPath)
if err != nil {
return err
}
+203
View File
@@ -0,0 +1,203 @@
package localfile
import (
"fmt"
"io/fs"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
)
// SymlinkFile 软链接文件,Linux/macOS的ln,Windows的mklink命令创建的文件链接。对于非软链接文件而言,真实的路径和逻辑路径是一样的。
type SymlinkFile struct {
// LogicPath 逻辑路径
LogicPath string `json:"logicPath"`
// RealPath 真正的文件路径,即文件的本体
RealPath string `json:"realPath"`
}
func (s *SymlinkFile) String() string {
sb := &strings.Builder{}
fmt.Fprintf(sb, "{\"logicPath\":%s, \"realPath\": %s}", s.LogicPath, s.RealPath)
return sb.String()
}
func NewSymlinkFile(filePath string) SymlinkFile {
p := CleanPath(filePath)
return SymlinkFile{
LogicPath: p,
RealPath: p,
}
}
type MyWalkFunc func(path SymlinkFile, info fs.FileInfo, err error) error
// CleanPath 规范化文件路径,分隔符全部转换成Unix文件分隔符"/"。同时清除后缀无用的"/"路径
func CleanPath(p string) string {
if p == "" || p == "/" {
return p
}
p = path.Clean(strings.ReplaceAll(p, "\\", "/"))
if p == "." {
p = ""
}
return p
}
// GetSuffixPath 获取相对路径。即fullFilePath相对rootFilePath的相对路径
func GetSuffixPath(fullFilePath, rootFilePath string) string {
rootFilePath = CleanPath(rootFilePath)
fullFilePath = CleanPath(fullFilePath)
return strings.TrimPrefix(strings.TrimPrefix(fullFilePath, rootFilePath), "/")
}
// RetrieveRealPath 递归调用找到软链接文件的真实文件对应的路径信息
func RetrieveRealPath(file SymlinkFile) (SymlinkFile, os.FileInfo, error) {
info, err := os.Lstat(file.RealPath)
if err == nil {
if info.Mode()&os.ModeSymlink != 0 {
// 软链接文件
if f, e := os.Readlink(file.RealPath); e == nil {
file.RealPath = strings.ReplaceAll(f, "\\", "/")
return RetrieveRealPath(file)
}
}
}
return file, info, err
}
// RetrieveRealPathFromLogicPath 遍历路径获取逻辑路径真正的文件路径。如果逻辑路径不完全存在,则返回已经存在的那部分路径
//
// logicFilePath - 目标逻辑路径
// 由于目标逻辑路径期间有可能会经过多次符号逻辑文件,同时有部分逻辑路径可能是不存在的,所以需要按照逻辑文件起始开始进行遍历,直至完成逻辑文件路径的所有遍历,
// 或者直到不存在的逻辑文件部分,然后返回。
//
// 例如逻辑文件路径:/Volumes/Downloads/dev/sync_drive_config.json。
//
// 如果/Volumes/Downloads存在而后面部分不存在,则最终结果返回/Volumes/Downloads对应的文件信息,同时返回error
func RetrieveRealPathFromLogicPath(logicFilePath string) (SymlinkFile, os.FileInfo, error) {
logicFilePath = CleanPath(logicFilePath)
if logicFilePath == "/" {
return RetrieveRealPath(NewSymlinkFile(logicFilePath))
}
pathParts := strings.Split(logicFilePath, "/")
exitedSymlinkFile := NewSymlinkFile("")
var exitedFileInfo os.FileInfo
sf := NewSymlinkFile("")
var fi os.FileInfo
var err error
for _, p := range pathParts {
if p == "" {
continue
}
if strings.Contains(p, ":") {
// windows volume label, e.g: C:/ D:/
sf.LogicPath += p
sf.RealPath += p
exitedSymlinkFile = sf
continue
}
sf.LogicPath += "/" + p
sf.RealPath += "/" + p
sf, fi, err = RetrieveRealPath(sf)
if err != nil {
// may be permission deny or not existed
return exitedSymlinkFile, exitedFileInfo, err
}
exitedSymlinkFile = sf
exitedFileInfo = fi
}
return exitedSymlinkFile, exitedFileInfo, nil
}
// RetrieveRealPathFromLogicSuffixPath 通过指定根目录和后缀逻辑路径,获取文件对应的真实路径信息
//
// 整体功能和 RetrieveRealPathFromLogicPath 一致,将逻辑路径分成根路径和后缀路径两部分,并且根路径必须存在。
func RetrieveRealPathFromLogicSuffixPath(rootFile SymlinkFile, logicSuffixPath string) (SymlinkFile, os.FileInfo, error) {
if rootFile.LogicPath == "" || rootFile.RealPath == "" {
return rootFile, nil, fmt.Errorf("root file path error")
}
rootFile.LogicPath = CleanPath(rootFile.LogicPath)
rootFile.RealPath = CleanPath(rootFile.RealPath)
rootFileInfo, e := os.Lstat(rootFile.RealPath)
if e != nil {
return rootFile, nil, e
}
logicSuffixPath = CleanPath(logicSuffixPath)
pathParts := strings.Split(logicSuffixPath, "/")
exitedSymlinkFile := rootFile
exitedFileInfo := rootFileInfo
sf := rootFile
var fi os.FileInfo
var err error
for _, p := range pathParts {
if p == "" {
continue
}
sf.LogicPath += "/" + p
sf.RealPath += "/" + p
sf, fi, err = RetrieveRealPath(sf)
if err != nil {
// may be permission deny or not existed
return exitedSymlinkFile, exitedFileInfo, err
}
exitedSymlinkFile = sf
exitedFileInfo = fi
}
return exitedSymlinkFile, exitedFileInfo, nil
}
// WalkAllFile 遍历本地文件,支持软链接(符号逻辑)文件(Linux & Windows & macOS)
func WalkAllFile(file SymlinkFile, walkFn MyWalkFunc) error {
file.LogicPath = path.Clean(strings.ReplaceAll(file.LogicPath, "\\", "/"))
file.RealPath = path.Clean(strings.ReplaceAll(file.RealPath, "\\", "/"))
file, info, err := RetrieveRealPath(file)
if err != nil {
err = walkFn(file, nil, err)
} else {
err = walkAllFile(file, info, walkFn)
}
return err
}
func walkAllFile(file SymlinkFile, info os.FileInfo, walkFn MyWalkFunc) error {
if !info.IsDir() {
return walkFn(file, info, nil)
}
files, err1 := ioutil.ReadDir(file.RealPath)
if err1 != nil {
return walkFn(file, nil, err1)
}
for _, fi := range files {
subFile := SymlinkFile{
LogicPath: path.Join(path.Clean(file.LogicPath), "/", fi.Name()),
RealPath: path.Join(path.Clean(file.RealPath), "/", fi.Name()),
}
subFile, fi, err1 = RetrieveRealPath(subFile)
err := walkFn(subFile, fi, err1)
if err != nil && err != filepath.SkipDir {
return err
}
if fi == nil {
continue
}
if fi.IsDir() {
if err == filepath.SkipDir {
continue
}
err = walkAllFile(subFile, fi, walkFn)
if err != nil {
return err
}
}
}
return nil
}
+63
View File
@@ -0,0 +1,63 @@
package localfile
import (
"fmt"
"github.com/tickstep/aliyunpan/internal/utils"
"os"
"path/filepath"
"testing"
)
func TestMyWalkFile(t *testing.T) {
count := 0
walkFunc := func(file SymlinkFile, fi os.FileInfo, err error) error {
if err != nil {
fmt.Println(err)
return err
}
count += 1
fmt.Println("file: ", utils.ObjectToJsonStr(file, false))
//fmt.Println("file: ", file)
return nil
}
//curPath := "D:\\smb\\feny\\goprojects\\dev\\lks"
curPath := "/Volumes/Downloads/dev/lks"
file := NewSymlinkFile(curPath)
if err := WalkAllFile(file, walkFunc); err != nil {
if err != filepath.SkipDir {
fmt.Printf("警告: 遍历错误: %s\n", err)
}
}
fmt.Println("count: ", count)
}
func TestRetrieveRealPath(t *testing.T) {
curPath := "/Volumes/Downloads/dev/lks/test"
file := NewSymlinkFile(curPath)
sf, _, e := RetrieveRealPath(file)
if e != nil {
fmt.Println(e)
}
fmt.Println(sf)
}
func TestRetrieveRealPathFromLogicPath(t *testing.T) {
curPath := "/Volumes/Downloads/dev/lks/test/未命名文件夹cmd/sync_drive_config.json"
sf, _, e := RetrieveRealPathFromLogicPath(curPath)
if e != nil {
fmt.Println(e)
}
fmt.Println(sf)
}
func TestRetrieveRealPathFromLogicSuffixPath(t *testing.T) {
rootPath := NewSymlinkFile("/Volumes/Downloads/dev/测试同步盘/new_lks")
rootPath, _, _ = RetrieveRealPath(rootPath)
suffixPath := "test/未命名文件夹cmd/sync_drive_config.json"
sf, _, e := RetrieveRealPathFromLogicSuffixPath(rootPath, suffixPath)
if e != nil {
fmt.Println(e)
}
fmt.Println(sf)
}
+76
View File
@@ -0,0 +1,76 @@
package log
import (
"encoding/csv"
"fmt"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"os"
"path/filepath"
"sync"
)
type (
FileRecordItem struct {
Status string `json:"status"`
TimeStr string `json:"timeStr"`
FileSize int64 `json:"fileSize"`
FilePath string `json:"filePath"`
}
FileRecorder struct {
Path string `json:"path"`
locker *sync.Mutex
}
)
// NewFileRecorder 创建文件记录器
func NewFileRecorder(filePath string) *FileRecorder {
return &FileRecorder{
Path: filePath,
locker: &sync.Mutex{},
}
}
// Append 增加数据记录
func (f *FileRecorder) Append(item *FileRecordItem) error {
f.locker.Lock()
defer f.locker.Unlock()
savePath := f.Path
folder := filepath.Dir(savePath)
if b, err := utils.PathExists(folder); err == nil && !b {
os.MkdirAll(folder, 0755)
}
var fp *os.File
var write *csv.Writer
if b, err := utils.PathExists(savePath); err == nil && b {
file, err1 := os.OpenFile(savePath, os.O_APPEND, 0755)
if err1 != nil {
logger.Verbosef("打开文件["+savePath+"]失败,%v", err1)
return err1
}
fp = file
write = csv.NewWriter(fp) //创建一个新的写入文件流
} else {
file, err1 := os.OpenFile(savePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) // 创建文件句柄
if err1 != nil {
logger.Verbosef("创建文件["+savePath+"]失败,%v", err1)
return err1
}
fp = file
fp.WriteString("\xEF\xBB\xBF") // 写入UTF-8 BOM
write = csv.NewWriter(fp) //创建一个新的写入文件流
write.Write([]string{"状态", "时间", "文件大小", "文件路径"})
}
if fp == nil || write == nil {
return fmt.Errorf("open recorder file error")
}
defer fp.Close()
data := []string{item.Status, item.TimeStr, converter.ConvertFileSize(item.FileSize, 2), item.FilePath}
write.Write(data)
write.Flush()
return nil
}
+16
View File
@@ -0,0 +1,16 @@
package log
import (
"testing"
)
func TestCsvFile(t *testing.T) {
savePath := "D:\\smb\\feny\\goprojects\\dev\\logs\\file_upload_records.csv"
recorder := NewFileRecorder(savePath)
recorder.Append(&FileRecordItem{
Status: "成功",
TimeStr: "2022-12-19 16:46:36",
FileSize: 453450,
FilePath: "D:\\smb\\feny\\goprojects\\dev\\myfile.mp4",
})
}
+39 -8
View File
@@ -17,10 +17,12 @@ import (
"archive/zip"
"bytes"
"fmt"
jsoniter "github.com/json-iterator/go"
"github.com/tickstep/aliyunpan/cmder/cmdliner"
"github.com/tickstep/aliyunpan/cmder/cmdutil"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/cachepool"
"github.com/tickstep/library-go/checkaccess"
"github.com/tickstep/library-go/converter"
@@ -28,7 +30,7 @@ import (
"github.com/tickstep/library-go/jsonhelper"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"io/ioutil"
"net/http"
"path/filepath"
"regexp"
@@ -50,14 +52,14 @@ type info struct {
}
type tsResp struct {
Code int `json:"code"`
Code int `json:"code"`
Data interface{} `json:"data"`
Msg string `json:"msg"`
Msg string `json:"msg"`
}
func getReleaseFromTicstep(client *requester.HTTPClient, showPrompt bool) *ReleaseInfo {
tsReleaseInfo := &ReleaseInfo{}
tsResp := &tsResp{Data: tsReleaseInfo}
tsRespObj := &tsResp{Data: tsReleaseInfo}
fullUrl := strings.Builder{}
ipAddr, err := getip.IPInfoFromTechainBaidu()
if err != nil {
@@ -75,15 +77,32 @@ func getReleaseFromTicstep(client *requester.HTTPClient, showPrompt bool) *Relea
}
return nil
}
err = jsonhelper.UnmarshalData(resp.Body, tsResp)
respBytes, _ := ioutil.ReadAll(resp.Body)
// 解析响应返回
tsRespEntity := &tsResp{}
if err = jsoniter.Unmarshal(respBytes, tsRespEntity); err == nil {
if tsRespEntity.Code != 0 {
if showPrompt {
fmt.Printf("错误: %s\n", tsRespEntity.Msg)
}
logger.Verboseln(string(respBytes))
return nil
}
}
// 解析可用版本返回
err = jsoniter.Unmarshal(respBytes, tsRespObj)
if err != nil {
if showPrompt {
fmt.Printf("json数据解析失败: %s\n", err)
}
return nil
}
if tsResp.Code == 0 {
return tsReleaseInfo
if tsRespObj.Code == 0 {
if tsReleaseInfo != nil && len(tsReleaseInfo.Assets) > 0 {
return tsReleaseInfo
}
}
return nil
}
@@ -278,7 +297,15 @@ func CheckUpdate(version string, yes bool) {
// 开始下载
buf := cachepool.RawMallocByteSlice(int(target.size))
resp, err := client.Req("GET", target.downloadURL, nil, nil)
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
logger.Verboseln("下载文件:" + req.URL.String())
req.Header.Del("Referer") // aliyundrive会检测盗链行为,所以删除Referer
return nil
}
header := map[string]string{}
header["Referer"] = ""
header["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 Edg/106.0.1370.42"
resp, err := client.Req("GET", target.downloadURL, nil, header)
if err != nil {
fmt.Printf("下载更新文件发生错误: %s\n", err)
return
@@ -286,6 +313,10 @@ func CheckUpdate(version string, yes bool) {
total, _ := strconv.Atoi(resp.Header.Get("Content-Length"))
if total > 0 {
if int64(total) != target.size {
if es, e := ioutil.ReadAll(resp.Body); e == nil {
// 发生错误
logger.Verboseln(string(es))
}
fmt.Printf("下载更新文件发生错误: %s\n", err)
return
}
+53
View File
@@ -0,0 +1,53 @@
package plugins
type (
IdlePlugin struct {
Name string
}
)
func NewIdlePlugin() *IdlePlugin {
return &IdlePlugin{
Name: "IdlePlugin",
}
}
func (p *IdlePlugin) Start() error {
return nil
}
func (p *IdlePlugin) UploadFilePrepareCallback(context *Context, params *UploadFilePrepareParams) (*UploadFilePrepareResult, error) {
return nil, nil
}
func (p *IdlePlugin) UploadFileFinishCallback(context *Context, params *UploadFileFinishParams) error {
return nil
}
func (p *IdlePlugin) DownloadFilePrepareCallback(context *Context, params *DownloadFilePrepareParams) (*DownloadFilePrepareResult, error) {
return nil, nil
}
func (p *IdlePlugin) DownloadFileFinishCallback(context *Context, params *DownloadFileFinishParams) error {
return nil
}
func (p *IdlePlugin) SyncScanLocalFilePrepareCallback(context *Context, params *SyncScanLocalFilePrepareParams) (*SyncScanLocalFilePrepareResult, error) {
return nil, nil
}
func (p *IdlePlugin) SyncScanPanFilePrepareCallback(context *Context, params *SyncScanPanFilePrepareParams) (*SyncScanPanFilePrepareResult, error) {
return nil, nil
}
func (p *IdlePlugin) SyncFileFinishCallback(context *Context, params *SyncFileFinishParams) error {
return nil
}
func (p *IdlePlugin) UserTokenRefreshFinishCallback(context *Context, params *UserTokenRefreshFinishParams) error {
return nil
}
func (p *IdlePlugin) Stop() error {
return nil
}
+246
View File
@@ -0,0 +1,246 @@
package plugins
import (
"fmt"
"github.com/dop251/goja"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/logger"
"strings"
)
type (
JsPlugin struct {
Name string
vm *goja.Runtime
}
)
func NewJsPlugin() *JsPlugin {
return &JsPlugin{
Name: "JsPlugin",
vm: nil,
}
}
// jsLog 支持js中的console.log方法
func jsLog(call goja.FunctionCall) goja.Value {
str := call.Argument(0)
buf := &strings.Builder{}
fmt.Fprintf(buf, "JAVASCRIPT: %+v", str.Export())
logger.Verboseln(buf.String())
return str
}
func (js *JsPlugin) Start() error {
js.Name = "JsPlugin"
js.vm = goja.New()
js.vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
// 内置log
console := js.vm.NewObject()
console.Set("log", jsLog)
js.vm.Set("console", console) // console.log()
// 内置系统函数PluginUtil
pluginObj := js.vm.NewObject()
js.vm.Set("PluginUtil", pluginObj)
// PluginUtil.Http
httpObj := js.vm.NewObject()
pluginObj.Set("Http", httpObj)
httpObj.Set("get", HttpGet) // PluginUtil.Http.get()
httpObj.Set("post", HttpPost) // PluginUtil.Http.post()
// PluginUtil.LocalFS
localFS := js.vm.NewObject()
pluginObj.Set("LocalFS", localFS)
localFS.Set("deleteFile", DeleteLocalFile) // PluginUtil.LocalFS.deleteFile()
// PluginUtil.Email
emailObj := js.vm.NewObject()
pluginObj.Set("Email", emailObj)
emailObj.Set("sendTextMail", SendTextMail) // PluginUtil.Email.sendTextMail()
emailObj.Set("sendHtmlMail", SendHtmlMail) // PluginUtil.Email.sendHtmlMail()
// PluginUtil.KV
PersistenceFilePath = config.GetPluginKvFile()
kvObj := js.vm.NewObject()
pluginObj.Set("KV", kvObj)
kvObj.Set("getString", GetString) // PluginUtil.KV.getString()
kvObj.Set("putString", PutString) // PluginUtil.KV.putString()
return nil
}
// LoadScript 加载脚本
func (js *JsPlugin) LoadScript(script string) error {
_, err := js.vm.RunString(script)
if err != nil {
logger.Verboseln("JS代码有问题!{}", err)
return err
}
return nil
}
func (js *JsPlugin) isHandlerFuncExisted(fnName string) bool {
ret := js.vm.Get(fnName)
if ret != nil {
return true
}
return false
}
// UploadFilePrepareCallback 上传文件前的回调函数
func (js *JsPlugin) UploadFilePrepareCallback(context *Context, params *UploadFilePrepareParams) (*UploadFilePrepareResult, error) {
var fn func(*Context, *UploadFilePrepareParams) (*UploadFilePrepareResult, error)
if !js.isHandlerFuncExisted("uploadFilePrepareCallback") {
return nil, nil
}
err := js.vm.ExportTo(js.vm.Get("uploadFilePrepareCallback"), &fn)
if err != nil {
logger.Verboseln("Js函数映射到 Go 函数失败!")
return nil, nil
}
r, er := fn(context, params)
if er != nil {
logger.Verboseln(er)
return nil, er
}
return r, nil
}
// UploadFileFinishCallback 上传文件结束的回调函数
func (js *JsPlugin) UploadFileFinishCallback(context *Context, params *UploadFileFinishParams) error {
var fn func(*Context, *UploadFileFinishParams) error
if !js.isHandlerFuncExisted("uploadFileFinishCallback") {
return nil
}
err := js.vm.ExportTo(js.vm.Get("uploadFileFinishCallback"), &fn)
if err != nil {
logger.Verboseln("Js函数映射到 Go 函数失败!")
return nil
}
er := fn(context, params)
if er != nil {
logger.Verboseln(er)
return nil
}
return nil
}
// DownloadFilePrepareCallback 下载文件前的回调函数
func (js *JsPlugin) DownloadFilePrepareCallback(context *Context, params *DownloadFilePrepareParams) (*DownloadFilePrepareResult, error) {
var fn func(*Context, *DownloadFilePrepareParams) (*DownloadFilePrepareResult, error)
if !js.isHandlerFuncExisted("downloadFilePrepareCallback") {
return nil, nil
}
err := js.vm.ExportTo(js.vm.Get("downloadFilePrepareCallback"), &fn)
if err != nil {
logger.Verboseln("Js函数映射到 Go 函数失败!")
return nil, nil
}
r, er := fn(context, params)
if er != nil {
logger.Verboseln(er)
return nil, er
}
return r, nil
}
// DownloadFileFinishCallback 下载文件结束的回调函数
func (js *JsPlugin) DownloadFileFinishCallback(context *Context, params *DownloadFileFinishParams) error {
var fn func(*Context, *DownloadFileFinishParams) error
if !js.isHandlerFuncExisted("downloadFileFinishCallback") {
return nil
}
err := js.vm.ExportTo(js.vm.Get("downloadFileFinishCallback"), &fn)
if err != nil {
logger.Verboseln("Js函数映射到 Go 函数失败!")
return nil
}
er := fn(context, params)
if er != nil {
logger.Verboseln(er)
return nil
}
return nil
}
// SyncScanLocalFilePrepareCallback 同步备份-扫描本地文件的回调函数
func (js *JsPlugin) SyncScanLocalFilePrepareCallback(context *Context, params *SyncScanLocalFilePrepareParams) (*SyncScanLocalFilePrepareResult, error) {
var fn func(*Context, *SyncScanLocalFilePrepareParams) (*SyncScanLocalFilePrepareResult, error)
if !js.isHandlerFuncExisted("syncScanLocalFilePrepareCallback") {
return nil, nil
}
err := js.vm.ExportTo(js.vm.Get("syncScanLocalFilePrepareCallback"), &fn)
if err != nil {
logger.Verboseln("Js函数映射到 Go 函数失败!")
return nil, nil
}
r, er := fn(context, params)
if er != nil {
logger.Verboseln(er)
return nil, er
}
return r, nil
}
// SyncScanPanFilePrepareCallback 同步备份-扫描本地文件的回调函数
func (js *JsPlugin) SyncScanPanFilePrepareCallback(context *Context, params *SyncScanPanFilePrepareParams) (*SyncScanPanFilePrepareResult, error) {
var fn func(*Context, *SyncScanPanFilePrepareParams) (*SyncScanPanFilePrepareResult, error)
if !js.isHandlerFuncExisted("syncScanPanFilePrepareCallback") {
return nil, nil
}
err := js.vm.ExportTo(js.vm.Get("syncScanPanFilePrepareCallback"), &fn)
if err != nil {
logger.Verboseln("Js函数映射到 Go 函数失败!")
return nil, nil
}
r, er := fn(context, params)
if er != nil {
logger.Verboseln(er)
return nil, er
}
return r, nil
}
// SyncFileFinishCallback 同步备份-同步文件完成的回调函数
func (js *JsPlugin) SyncFileFinishCallback(context *Context, params *SyncFileFinishParams) error {
var fn func(*Context, *SyncFileFinishParams) error
if !js.isHandlerFuncExisted("syncFileFinishCallback") {
return nil
}
err := js.vm.ExportTo(js.vm.Get("syncFileFinishCallback"), &fn)
if err != nil {
logger.Verboseln("Js函数映射到 Go 函数失败!")
return nil
}
er := fn(context, params)
if er != nil {
logger.Verboseln(er)
return nil
}
return nil
}
func (js *JsPlugin) UserTokenRefreshFinishCallback(context *Context, params *UserTokenRefreshFinishParams) error {
var fn func(*Context, *UserTokenRefreshFinishParams) error
if !js.isHandlerFuncExisted("userTokenRefreshFinishCallback") {
return nil
}
err := js.vm.ExportTo(js.vm.Get("userTokenRefreshFinishCallback"), &fn)
if err != nil {
logger.Verboseln("Js函数映射到 Go 函数失败!")
return nil
}
er := fn(context, params)
if er != nil {
logger.Verboseln(er)
return nil
}
return nil
}
func (js *JsPlugin) Stop() error {
return nil
}
+181
View File
@@ -0,0 +1,181 @@
// 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 plugins
type (
// Context 插件回调函数上下文信息
Context struct {
AppName string `json:"appName"`
Version string `json:"version"`
UserId string `json:"userId"`
Nickname string `json:"nickname"`
FileDriveId string `json:"fileDriveId"`
AlbumDriveId string `json:"albumDriveId"`
}
// UploadFilePrepareParams 上传文件前的回调函数-参数
UploadFilePrepareParams struct {
LocalFilePath string `json:"localFilePath"`
LocalFileName string `json:"localFileName"`
LocalFileSize int64 `json:"localFileSize"`
LocalFileType string `json:"localFileType"`
LocalFileUpdatedAt string `json:"localFileUpdatedAt"`
DriveId string `json:"driveId"`
DriveFilePath string `json:"driveFilePath"`
}
// UploadFilePrepareResult 上传文件前的回调函数-返回结果
UploadFilePrepareResult struct {
// UploadApproved 确认该文件是否上传。yes-上传 no-不上传
UploadApproved string `json:"uploadApproved"`
// DriveFilePath 保存网盘的修改后的路径。注意该路径是相对路径
DriveFilePath string `json:"driveFilePath"`
}
// UploadFileFinishParams 上传文件结束的回调函数-参数
UploadFileFinishParams struct {
LocalFilePath string `json:"localFilePath"`
LocalFileName string `json:"localFileName"`
LocalFileSize int64 `json:"localFileSize"`
LocalFileType string `json:"localFileType"`
LocalFileUpdatedAt string `json:"localFileUpdatedAt"`
LocalFileSha1 string `json:"localFileSha1"`
UploadResult string `json:"uploadResult"`
DriveId string `json:"driveId"`
DriveFilePath string `json:"driveFilePath"`
}
// DownloadFilePrepareParams 下载文件前的回调函数-参数
DownloadFilePrepareParams struct {
DriveId string `json:"driveId"`
DriveFileName string `json:"driveFileName"`
DriveFilePath string `json:"driveFilePath"`
DriveFileSha1 string `json:"driveFileSha1"`
DriveFileSize int64 `json:"driveFileSize"`
DriveFileType string `json:"driveFileType"`
DriveFileUpdatedAt string `json:"driveFileUpdatedAt"`
LocalFilePath string `json:"localFilePath"`
}
// DownloadFilePrepareResult 上传文件前的回调函数-返回结果
DownloadFilePrepareResult struct {
// DownloadApproved 确认该文件是否下载。yes-下载 no-不下载
DownloadApproved string `json:"downloadApproved"`
// LocalFilePath 保存本地的修改后的路径。注意该路径是相对路径
LocalFilePath string `json:"localFilePath"`
}
// DownloadFileFinishParams 下载文件结束的回调函数-参数
DownloadFileFinishParams struct {
DriveId string `json:"driveId"`
DriveFileName string `json:"driveFileName"`
DriveFilePath string `json:"driveFilePath"`
DriveFileSha1 string `json:"driveFileSha1"`
DriveFileSize int64 `json:"driveFileSize"`
DriveFileType string `json:"driveFileType"`
DriveFileUpdatedAt string `json:"driveFileUpdatedAt"`
DownloadResult string `json:"downloadResult"`
LocalFilePath string `json:"localFilePath"`
}
// SyncScanLocalFilePrepareParams 同步备份-扫描本地文件前参数
SyncScanLocalFilePrepareParams struct {
LocalFilePath string `json:"localFilePath"`
LocalFileName string `json:"localFileName"`
LocalFileSize int64 `json:"localFileSize"`
LocalFileType string `json:"localFileType"`
LocalFileUpdatedAt string `json:"localFileUpdatedAt"`
DriveId string `json:"driveId"`
}
// SyncScanLocalFilePrepareResult 同步备份-扫描本地文件-返回结果
SyncScanLocalFilePrepareResult struct {
// SyncScanLocalApproved 该文件是否确认扫描,yes-允许扫描,no-禁止扫描
SyncScanLocalApproved string `json:"syncScanLocalApproved"`
}
// SyncScanPanFilePrepareParams 同步备份-扫描云盘文件前参数
SyncScanPanFilePrepareParams struct {
DriveId string `json:"driveId"`
DriveFileName string `json:"driveFileName"`
DriveFilePath string `json:"driveFilePath"`
DriveFileSha1 string `json:"driveFileSha1"`
DriveFileSize int64 `json:"driveFileSize"`
DriveFileType string `json:"driveFileType"`
DriveFileUpdatedAt string `json:"driveFileUpdatedAt"`
}
// SyncScanPanFilePrepareResult 同步备份-扫描云盘文件-返回结果
SyncScanPanFilePrepareResult struct {
// SyncScanPanApproved 该文件是否确认扫描,yes-允许扫描,no-禁止扫描
SyncScanPanApproved string `json:"syncScanPanApproved"`
}
// SyncFileFinishParams 同步备份-文件同步结束-回调参数
SyncFileFinishParams struct {
Action string `json:"action"`
ActionResult string `json:"actionResult"`
DriveId string `json:"driveId"`
FileName string `json:"fileName"`
FilePath string `json:"filePath"`
FileSha1 string `json:"fileSha1"`
FileSize int64 `json:"fileSize"`
FileType string `json:"fileType"`
FileUpdatedAt string `json:"fileUpdatedAt"`
}
// UserTokenRefreshFinishParams 用户Token刷新完成后回调函数
UserTokenRefreshFinishParams struct {
Result string `json:"result"`
Message string `json:"message"`
OldToken string `json:"oldToken"`
NewToken string `json:"newToken"`
UpdatedAt string `json:"updatedAt"`
}
// Plugin 插件接口
Plugin interface {
// Start 启动
Start() error
// UploadFilePrepareCallback 上传文件前的回调函数
UploadFilePrepareCallback(context *Context, params *UploadFilePrepareParams) (*UploadFilePrepareResult, error)
// UploadFileFinishCallback 上传文件结束的回调函数
UploadFileFinishCallback(context *Context, params *UploadFileFinishParams) error
// DownloadFilePrepareCallback 下载文件前的回调函数
DownloadFilePrepareCallback(context *Context, params *DownloadFilePrepareParams) (*DownloadFilePrepareResult, error)
// DownloadFileFinishCallback 下载文件结束的回调函数
DownloadFileFinishCallback(context *Context, params *DownloadFileFinishParams) error
// SyncScanLocalFilePrepareCallback 同步备份-扫描本地文件的回调函数
SyncScanLocalFilePrepareCallback(context *Context, params *SyncScanLocalFilePrepareParams) (*SyncScanLocalFilePrepareResult, error)
// SyncScanPanFilePrepareCallback 同步备份-扫描本地文件的回调函数
SyncScanPanFilePrepareCallback(context *Context, params *SyncScanPanFilePrepareParams) (*SyncScanPanFilePrepareResult, error)
// SyncFileFinishCallback 同步备份-同步文件完成的回调函数
SyncFileFinishCallback(context *Context, params *SyncFileFinishParams) error
// UserTokenRefreshFinishCallback 用户Token刷新完成后回调函数
UserTokenRefreshFinishCallback(context *Context, params *UserTokenRefreshFinishParams) error
// Stop 停止
Stop() error
}
)
var ()
+97
View File
@@ -0,0 +1,97 @@
package plugins
import (
"fmt"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/logger"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
)
type (
PluginManager struct {
PluginPath string
}
)
func GetContext(user *config.PanUser) *Context {
if user == nil {
return &Context{
AppName: "aliyunpan",
Version: config.AppVersion,
UserId: "",
Nickname: "",
FileDriveId: "",
AlbumDriveId: "",
}
}
return &Context{
AppName: "aliyunpan",
Version: config.AppVersion,
UserId: user.UserId,
Nickname: user.Nickname,
FileDriveId: user.DriveList.GetFileDriveId(),
AlbumDriveId: user.DriveList.GetFileDriveId(),
}
}
func NewPluginManager(pluginDir string) *PluginManager {
return &PluginManager{
PluginPath: pluginDir,
}
}
func (p *PluginManager) SetPluginPath(pluginPath string) error {
if fi, err := os.Stat(pluginPath); err == nil && fi.IsDir() {
p.PluginPath = filepath.Clean(pluginPath)
} else {
return fmt.Errorf("path must be a folder")
}
return nil
}
func (p *PluginManager) GetPlugin() (Plugin, error) {
// js plugins folder
// only support js plugins right now
jsPluginPath := path.Clean(p.PluginPath + string(os.PathSeparator) + "js")
if fi, err := os.Stat(jsPluginPath); err == nil && fi.IsDir() {
jsPlugin := NewJsPlugin()
if jsPlugin.Start() != nil {
logger.Verbosef("初始化JS脚本错误\n")
return interface{}(NewIdlePlugin()).(Plugin), nil
}
jsPluginValid := false
if files, e := ioutil.ReadDir(jsPluginPath); e == nil {
for _, f := range files {
if !f.IsDir() {
if strings.HasPrefix(strings.ToLower(f.Name()), ".") || strings.HasPrefix(strings.ToLower(f.Name()), "~") {
continue
}
if strings.HasSuffix(strings.ToLower(f.Name()), ".js") {
// this is a js file
bytes, re := ioutil.ReadFile(path.Clean(jsPluginPath + string(os.PathSeparator) + f.Name()))
if re != nil {
logger.Verbosef("读取JS脚本错误: %s\n", re)
continue
}
var script = string(bytes)
if jsPlugin.LoadScript(script) == nil {
jsPluginValid = true
logger.Verbosef("加载JS脚本成功: %s\n", f.Name())
}
}
}
}
}
if jsPluginValid {
return interface{}(jsPlugin).(Plugin), nil
}
}
// default idle plugins
return interface{}(NewIdlePlugin()).(Plugin), nil
}
+76
View File
@@ -0,0 +1,76 @@
package plugins
import (
"fmt"
jsoniter "github.com/json-iterator/go"
"testing"
)
func TestPluginUpload(t *testing.T) {
pluginManager := NewPluginManager("D:\\smb\\feny\\goprojects\\dev")
plugin, err := pluginManager.GetPlugin()
if err != nil {
fmt.Println(err)
}
ctx := &Context{
AppName: "aliyunpan",
Version: "v0.1.3",
UserId: "11001d48564f43b3bc5662874f04bb11",
Nickname: "tickstep",
FileDriveId: "19519121",
AlbumDriveId: "29519122",
}
params := &UploadFilePrepareParams{
LocalFilePath: "D:\\Program Files\\aliyunpan\\Downloads\\11001d48564f43b3bc5662874f04bb11\\token.bat",
LocalFileName: "token.bat",
LocalFileSize: 125330,
LocalFileType: "file",
LocalFileUpdatedAt: "2022-04-14 07:05:12",
DriveId: "19519221",
DriveFilePath: "aliyunpan/Downloads/11001d48564f43b3bc5662874f04bb11/token.bat",
}
b, _ := jsoniter.Marshal(ctx)
fmt.Println(string(b))
b, _ = jsoniter.Marshal(params)
fmt.Println(string(b))
r, e := plugin.UploadFilePrepareCallback(ctx, params)
if e != nil {
fmt.Println(e)
}
fmt.Println(r)
}
func TestPluginDownload(t *testing.T) {
pluginManager := NewPluginManager("/Volumes/Downloads/dev/config/plugin")
plugin, err := pluginManager.GetPlugin()
if err != nil {
fmt.Println(err)
}
ctx := &Context{
AppName: "aliyunpan",
Version: "v0.1.3",
UserId: "11001d48564f43b3bc5662874f04bb11",
Nickname: "tickstep",
FileDriveId: "19519121",
AlbumDriveId: "29519122",
}
params := &DownloadFilePrepareParams{
DriveId: "19519221",
DriveFilePath: "/test/aliyunpan/Downloads/token.bat",
DriveFileName: "token.bat",
DriveFileSize: 125330,
DriveFileType: "file",
DriveFileSha1: "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
DriveFileUpdatedAt: "2022-04-14 07:05:12",
LocalFilePath: "aliyunpan\\Downloads\\token.bat",
}
b, _ := jsoniter.Marshal(ctx)
fmt.Println(string(b))
b, _ = jsoniter.Marshal(params)
fmt.Println(string(b))
r, e := plugin.DownloadFilePrepareCallback(ctx, params)
if e != nil {
fmt.Println(e)
}
fmt.Println(r)
}

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