Compare commits
670 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d05d19acd8 | |||
| 5179f914ed | |||
| 2feaa3cf82 | |||
| cedd43d402 | |||
| e62bdbb9f1 | |||
| acc408f0a0 | |||
| db28ea6732 | |||
| a9bc1076a5 | |||
| e20ea479c4 | |||
| 42f044ba38 | |||
| 88f1ff2688 | |||
| ca09039e48 | |||
| cf38ad51fd | |||
| 30cbced50c | |||
| 0946939325 | |||
| da13b046b8 | |||
| 268e4a30d1 | |||
| a0ca0f8089 | |||
| 2d99a39606 | |||
| 2965179ae8 | |||
| 0565482121 | |||
| 1822d3c2b2 | |||
| 6672aa4cd4 | |||
| c69a27c9b1 | |||
| 4feacbcdbf | |||
| 1916dda8f8 | |||
| 8b5eb14eb6 | |||
| d3d9690d55 | |||
| 89a6ef326c | |||
| 35dfb35429 | |||
| 3c7ebb5f62 | |||
| f796f40664 | |||
| 0d9700c84c | |||
| 09ee162ba4 | |||
| ff0e3dbfa9 | |||
| 4c5f45e5ab | |||
| 02326b0bd7 | |||
| 350fec4275 | |||
| 70a14dd1a6 | |||
| 23dad72914 | |||
| 9fa547ddc8 | |||
| 2898037ec5 | |||
| 70c14fd66f | |||
| 8913597716 | |||
| 9b276f2caf | |||
| e8bda159fa | |||
| e1ac6a95b9 | |||
| cef9c6c7c2 | |||
| 17e8e182ee | |||
| 4b9ded5b9e | |||
| f939f515f4 | |||
| 0e0486914c | |||
| a86a464e67 | |||
| c932e3a231 | |||
| f79645bfa7 | |||
| 7786dc6c40 | |||
| d7b4d0c846 | |||
| e669063be9 | |||
| e63ba4478c | |||
| 23617d810b | |||
| 3d87c6244f | |||
| 4f7941caf5 | |||
| 095d06d99c | |||
| 7fbe082e58 | |||
| 34a6242bde | |||
| 71df114d8a | |||
| 622984ee26 | |||
| 04ec8658e4 | |||
| 6f0d03bd58 | |||
| 94c24ebf16 | |||
| fb07969bb1 | |||
| 9f83bad7d6 | |||
| 91b1e724fa | |||
| 787d3e1099 | |||
| 87f951b5f5 | |||
| fdafcaed42 | |||
| 9bef162dad | |||
| 6b638d7817 | |||
| ce6d0266bf | |||
| acdc2c93ee | |||
| e9702ea449 | |||
| 212a8ef764 | |||
| 057362d5a9 | |||
| ae3590f84a | |||
| 770cd0356f | |||
| 12bca5683c | |||
| 68f4c44a55 | |||
| 3aae75a46e | |||
| 44e7402b27 | |||
| 499b08dd72 | |||
| 29ba6903e6 | |||
| 6d1687e519 | |||
| d8d00600d8 | |||
| 304c4d4f93 | |||
| 4c75a3d6ee | |||
| 34d58d3ebc | |||
| 8410f5977c | |||
| 9730bb8a23 | |||
| 6ed0e6bae0 | |||
| 2b283b727e | |||
| f3d352800b | |||
| 2eaba0c3c8 | |||
| 4a59f246f2 | |||
| d45ef8c1b9 | |||
| ed26496bb7 | |||
| 1e2bcee3e7 | |||
| 1415e81eec | |||
| 7ebaf6ec17 | |||
| 1e205b52a3 | |||
| 790056a488 | |||
| 68c89ec317 | |||
| 2849dbb472 | |||
| 68437a7a4c | |||
| 05f695a882 | |||
| fbfa313c1e | |||
| ff97c172b6 | |||
| 47baab44a7 | |||
| 0b5bd60702 | |||
| 6f45a8048d | |||
| 7132342ed3 | |||
| d85758b8b0 | |||
| 76e657db74 | |||
| 7870077565 | |||
| 0a52136c2c | |||
| 210747fe14 | |||
| 1f3131f718 | |||
| 630866798b | |||
| f56d103849 | |||
| 97ee446b32 | |||
| c05148df16 | |||
| 7dfd89c29d | |||
| 0d823de18f | |||
| 605380ce70 | |||
| e1428f2097 | |||
| b368060ffb | |||
| 504d38db90 | |||
| 4a0ad141dc | |||
| a7a386e9a6 | |||
| 6506acba9a | |||
| 1e6004d367 | |||
| 9796673138 | |||
| b506bcc84d | |||
| 989abc71ee | |||
| ebed4ff62f | |||
| 465d8fa4e9 | |||
| e9ae974017 | |||
| 54dd4e766b | |||
| 4727b9696d | |||
| 4769d17281 | |||
| 03f3d37fb8 | |||
| d1a577810c | |||
| 670b5c5dad | |||
| 9d084610e5 | |||
| 9962e63c81 | |||
| f7b8d26964 | |||
| 280421fd88 | |||
| 02b6d529d0 | |||
| 561c50956f | |||
| 6a39dd78f6 | |||
| a5ff0780b7 | |||
| 1d2342efb5 | |||
| 2e3916be7e | |||
| 68b3a4aba6 | |||
| 82c62a2d51 | |||
| a02638272f | |||
| 87d3770e5b | |||
| 8ae323eaa6 | |||
| 21770fec89 | |||
| 291fbd54c8 | |||
| 281ba818cd | |||
| df53c7f44b | |||
| 5cab8cc6a5 | |||
| 25202861c5 | |||
| 3fcbbe9b40 | |||
| ae3be4ce6f | |||
| ae81b59926 | |||
| c0da3de6c6 | |||
| 134531d64a | |||
| 0064a23f64 | |||
| 425dddf92b | |||
| 31aaadfcc1 | |||
| 1f10f1e982 | |||
| a9f9f18f09 | |||
| 5e90f96577 | |||
| 5b008b935d | |||
| bdc0a15bdc | |||
| 26c7201f4e | |||
| 7da4fedc54 | |||
| 957a1a1bd1 | |||
| 92133d185e | |||
| 239b794f8b | |||
| b8f35e8647 | |||
| c465277a8d | |||
| 281552eefa | |||
| d26d4d273e | |||
| f531c7bc70 | |||
| 443eac7589 | |||
| 6aecb98595 | |||
| 62c50726a5 | |||
| a7034e300f | |||
| c86afbd764 | |||
| d83f87942c | |||
| b79cd7bafb | |||
| 4c0c8a4ff9 | |||
| bb231dd45e | |||
| bec492fe65 | |||
| 69b76a15ca | |||
| 3bdc27ce66 | |||
| 9dd2482b58 | |||
| e8b87a02dc | |||
| 787aa53ce7 | |||
| 4a623b443c | |||
| 136dfb082f | |||
| e4d73d8b42 | |||
| 47123f2b86 | |||
| 9d6dfd0d0a | |||
| 3c0da7fc1d | |||
| 2fe5cc5414 | |||
| e261e04978 | |||
| e5f63c60e9 | |||
| c350cadba4 | |||
| 4e8c4d6e9c | |||
| a282aabbe7 | |||
| c81855f9c2 | |||
| 65b7f28aa6 | |||
| 8afdb8f701 | |||
| ead6ecc218 | |||
| a42f14f013 | |||
| 54fff27fbe | |||
| b0a1ea8591 | |||
| 1b78dff61e | |||
| f838ebfe24 | |||
| 7f5e19812e | |||
| bbff32c8c6 | |||
| 6a7da9f180 | |||
| 06184249d0 | |||
| b983d4c335 | |||
| d061a28559 | |||
| e91e66aadf | |||
| 71f55424d0 | |||
| c6c4d871ef | |||
| 50e6200607 | |||
| c363d90e72 | |||
| 048a6a5438 | |||
| 19afa16bfe | |||
| b559f41e47 | |||
| 33a5484e4a | |||
| 34254bf822 | |||
| 66540c9457 | |||
| c522b530d7 | |||
| 3d130949c0 | |||
| 1365471639 | |||
| 151574e154 | |||
| 55e989d821 | |||
| 0edf7d0af2 | |||
| ccc285ce5e | |||
| 9992ffb3bc | |||
| 25d916b744 | |||
| 471ee93417 | |||
| 78e290c2a4 | |||
| 23fe54ce0b | |||
| 8d2c40f1ce | |||
| fa144a45a1 | |||
| 2740a15cbf | |||
| 3be79d1181 | |||
| ab215736a9 | |||
| 45877a4408 | |||
| 50a05a6bec | |||
| e33d23831a | |||
| cbf372bde0 | |||
| 8d14617397 | |||
| 12d866b28a | |||
| 85d79a9657 | |||
| 828c1fe39a | |||
| c5a5cb906d | |||
| 4ba61c19c8 | |||
| 571baa1482 | |||
| 4575dfdaa8 | |||
| efbf0b7bdb | |||
| 416c5cd57e | |||
| e0e8bc3284 | |||
| 5b22c4400a | |||
| c9c33217d2 | |||
| 79dcf40792 | |||
| d618686da9 | |||
| 5f52145378 | |||
| bd958fb8df | |||
| 8fc8d9b6f7 | |||
| 70ddb7e0c1 | |||
| 0acd6a43d6 | |||
| 97a22812a2 | |||
| 118e521d63 | |||
| f8c4961608 | |||
| ed38002e65 | |||
| b74d39da41 | |||
| aabab869c4 | |||
| bb8dad7262 | |||
| 9f9fb6a320 | |||
| 84d7ec923f | |||
| 07746fc15c | |||
| c5d15b058a | |||
| ea9279e45e | |||
| 6abab46db0 | |||
| 6e8ee94285 | |||
| 1208ccad08 | |||
| d23802e8cc | |||
| 59646b07fe | |||
| 34dd8a98fe | |||
| b6780931cc | |||
| fb705365b1 | |||
| 1c6f6c772c | |||
| 7726888365 | |||
| eda44658c9 | |||
| 0b6c77b96f | |||
| f88e921b04 | |||
| c3dadd00b7 | |||
| 90a889f0eb | |||
| 5935f78824 | |||
| 56ba938c72 | |||
| 6ade77c4bf | |||
| ecacda2596 | |||
| 695acce3f4 | |||
| 98de5b931c | |||
| a1577f19c5 | |||
| f1fb75cfe4 | |||
| 7ee48fa726 | |||
| 15084b3ac2 | |||
| 4a4796218e | |||
| 9ccaf8a25d | |||
| dc469e87d2 | |||
| 67c288318e | |||
| 36a5eed5af | |||
| fb24f703c2 | |||
| 0d203ece9a | |||
| 9be7080ae3 | |||
| b8aeead58c | |||
| a15f704672 | |||
| 4173889120 | |||
| 077d18f471 | |||
| 1c454060c1 | |||
| a455af2084 | |||
| 5f6cdcab64 | |||
| 9db89a64b0 | |||
| b600dd17c7 | |||
| 3b18f9af78 | |||
| 00e1c3e3d4 | |||
| 2e4a6d94df | |||
| bf91a47009 | |||
| a2f33f32bf | |||
| f511e17091 | |||
| 3ad528bc8e | |||
| 1c80403bc3 | |||
| 8c08a322a1 | |||
| a2a46bdeeb | |||
| ae077101f1 | |||
| 41fd68411e | |||
| a81d97c8bc | |||
| 13451ccf4c | |||
| 643d35a122 | |||
| 79833da4fc | |||
| f90829aaab | |||
| 072e7e9261 | |||
| b27e9b24d7 | |||
| 8f79a7e807 | |||
| 81d7bd22c9 | |||
| 47e8b004b8 | |||
| 238d692f73 | |||
| b85be20cba | |||
| a283320eff | |||
| c9eb8e06db | |||
| bd8b1f43e6 | |||
| 65b0dedb9b | |||
| f3b1b84439 | |||
| fe370ca953 | |||
| 43f7883155 | |||
| 2668e79564 | |||
| 25c550b4d8 | |||
| 4c5f217257 | |||
| 93b55014d0 | |||
| 7307c7f37f | |||
| 955a563b94 | |||
| 666842f972 | |||
| fe22d18aa7 | |||
| 66d4b14ff9 | |||
| 08c9d58ef8 | |||
| 4e7545e6e6 | |||
| ee9c5db8b3 | |||
| 9c965b5b64 | |||
| 83d2895f78 | |||
| e8c86672ff | |||
| cb4c9fbeb7 | |||
| 0c80fca74e | |||
| 3faf4d3f43 | |||
| d7e4772417 | |||
| 8b1515c2cf | |||
| fa3ffeefd9 | |||
| e8f73e5a37 | |||
| 1cf3db39b1 | |||
| 15bef9fcc3 | |||
| 3e1b87f5f2 | |||
| 5c19a07a31 | |||
| 5782bd7cd9 | |||
| a8850e2479 | |||
| 7978788ccd | |||
| 8a6723cb06 | |||
| f303cc7338 | |||
| a2afb9990c | |||
| 9016c12c26 | |||
| d0ad94aa65 | |||
| 594e635cde | |||
| 6787269751 | |||
| 5f10365604 | |||
| 0a7f520a8c | |||
| 2d3874ea7f | |||
| 2e35149ac5 | |||
| 74e5a6f409 | |||
| f98d201f1e | |||
| aa17816ed1 | |||
| 0668e743f5 | |||
| d8da7cefc3 | |||
| bf75461a8b | |||
| 8f7d4920ae | |||
| fa475768fa | |||
| c74848b5bc | |||
| 83914ad5e1 | |||
| 43f4b23981 | |||
| 4833c787ac | |||
| f13177214e | |||
| d90894898f | |||
| ac5eb94305 | |||
| ba3ae4028b | |||
| 416b2e2e6a | |||
| 23fa29ef54 | |||
| 6031beee6d | |||
| 5f62a93ce4 | |||
| 811ebdf805 | |||
| a218f47f9e | |||
| b22a79fdb2 | |||
| 8456aa7b35 | |||
| 797239361c | |||
| b3a7a375d0 | |||
| 3fa8c3eee4 | |||
| 8dd7c6b7cd | |||
| 99cfb7d09d | |||
| bf4027601c | |||
| 714c912c54 | |||
| ab49739513 | |||
| b4e405d0f8 | |||
| 6ce355ead8 | |||
| 884d8d356c | |||
| dd4f8931bf | |||
| f6daf215b9 | |||
| 7d0da94671 | |||
| 95dcd3307f | |||
| d376c19ede | |||
| b8949b11f0 | |||
| e8ca54cf4b | |||
| 8632c268f0 | |||
| 1c04ab9c7b | |||
| 779f21de65 | |||
| 7b720dbb5e | |||
| a867359da3 | |||
| f003ff9f44 | |||
| 84a833db5b | |||
| b64c07f079 | |||
| ac93c7e184 | |||
| bc2ab0625f | |||
| 3d1aa1c8bb | |||
| 6b0c815597 | |||
| a914eb5338 | |||
| d707895acc | |||
| 5b877c8fd8 | |||
| 8db035d4e5 | |||
| 5efbd9c306 | |||
| 6c1ab9d6e0 | |||
| dd72192db6 | |||
| 621925709d | |||
| 57e2ecdb72 | |||
| 6669b64027 | |||
| 117a4f73db | |||
| ed11869074 | |||
| 379ce404e4 | |||
| b36b14e03a | |||
| 8ac48bbda5 | |||
| ba407cb74b | |||
| 3b802d5cc6 | |||
| 09051e1e7a | |||
| f458dc5353 | |||
| 1d8e16f5c9 | |||
| 3ef5e67d76 | |||
| a88487dffd | |||
| 855b640d9f | |||
| 911b07cc8a | |||
| bc86c25549 | |||
| 13daf9480b | |||
| 9f1e565dae | |||
| e5bf8da4de | |||
| 37f3b67002 | |||
| 86ae72f76e | |||
| 5a018dd4ce | |||
| 09740df140 | |||
| b5dde87145 | |||
| d1836d3df8 | |||
| 38a7f63cc6 | |||
| 350c879f66 | |||
| 12a598015f | |||
| b4049b35e5 | |||
| 1b2c8ae1ce | |||
| 232ab535ed | |||
| 979b3d9bde | |||
| debaf655f0 | |||
| f1c6379e33 | |||
| f692e2d18b | |||
| d0e7715f4d | |||
| dbae151f22 | |||
| ad5c44aaf9 | |||
| 2ec8592685 | |||
| f804a42a19 | |||
| ab6290e7a3 | |||
| 8955eeec92 | |||
| f2bcc977cc | |||
| 437c2e31b9 | |||
| 25d7ff406c | |||
| 37f4bf05e4 | |||
| a582cba76a | |||
| f9dbb6ffa2 | |||
| d7434d29a6 | |||
| 7f4675331f | |||
| 24765ba2af | |||
| 01b9929c4b | |||
| 557b36617a | |||
| 232155c5f3 | |||
| 847546424b | |||
| 8e422c85a4 | |||
| 7c6145aee4 | |||
| b645f6caba | |||
| 45c4a30f22 | |||
| 594ae354e1 | |||
| 2da542fc22 | |||
| 52fbe51499 | |||
| 90cec4b827 | |||
| f6bdb4ae60 | |||
| 85c9cf0ecb | |||
| 32b57bf71f | |||
| 9c7f9a81f6 | |||
| c8404260cc | |||
| 24083e190f | |||
| 311f1fdd39 | |||
| ab15ddf54e | |||
| b0a2ee41ae | |||
| 6c89a01900 | |||
| 5a5ab464be | |||
| 8750761aee | |||
| a318796f3b | |||
| 5572f844ed | |||
| 0887b9f160 | |||
| 182c28fdb3 | |||
| 56a06d495e | |||
| c1115dfcc7 | |||
| 70443455de | |||
| e8c370cc86 | |||
| e2fd95c557 | |||
| 85eb4ce2c4 | |||
| 37e7bd07cf | |||
| cc1fc526ef | |||
| 9bffdc1cff | |||
| b910776209 | |||
| ebbba140e3 | |||
| b6b71b45e3 | |||
| ea82ec8320 | |||
| 04e414b65d | |||
| 7e0d2155a8 | |||
| 35ca753ad6 | |||
| 7f53b9642c | |||
| d36a764525 | |||
| 159b403879 | |||
| 93eb1797c5 | |||
| 1bbb6e2860 | |||
| 1af752ab98 | |||
| b29f017bb3 | |||
| bed142f490 | |||
| 5e052308cc | |||
| 11c4c632d7 | |||
| 2660ded630 | |||
| 645ef7f538 | |||
| a85cfe20ab | |||
| ce69a563a1 | |||
| 69189a8305 | |||
| d4873b94f7 | |||
| dc248af2eb | |||
| d7e7aeb59c | |||
| 3011402bb1 | |||
| c8f8e13407 | |||
| 3a4582a68d | |||
| 7be3b16b25 | |||
| 416e167a8a | |||
| 9b5fe1da16 | |||
| f41f70ed6b | |||
| 8f1a32aa88 | |||
| 45de57b30b | |||
| 7327da6795 | |||
| dbe0efacbd | |||
| 4ae36af2b7 | |||
| 261b9d350c | |||
| b5e5fd7924 | |||
| 5d10cb48ed | |||
| b055dc368c | |||
| 81a4c1f765 | |||
| 4c89bbc652 | |||
| 4cf51f24fe | |||
| 4b5a7e9f07 | |||
| b63320ec54 | |||
| 45acb61b68 | |||
| 583b6fe48d | |||
| 7c44f4e4a3 | |||
| 779ebd532f | |||
| 797ac7bbe6 | |||
| b770e272e8 | |||
| 094ad8f6a8 | |||
| ddc8de2c17 | |||
| d6f130a1b6 | |||
| 7516ab8fff | |||
| 6590d34273 | |||
| 36bb42bb3e | |||
| d7fac821ed | |||
| 45f527f66d | |||
| 9ccbb4e9a8 | |||
| e8d998197f | |||
| aceee66709 | |||
| e48e95cd79 | |||
| b0c8b14edb | |||
| 7f919569ad | |||
| bc8f9358c0 | |||
| 39c75a13ed | |||
| 91b760670e | |||
| cfddc3db30 | |||
| b9cbf78da4 | |||
| 0c15220190 | |||
| 4a421326b6 | |||
| a39fe17b93 | |||
| 999e5d7f39 | |||
| 32e552c6ff | |||
| b7b157947a | |||
| f70a5b2fa0 | |||
| a4a74f062e | |||
| d867267756 | |||
| 5a88effab9 | |||
| 43686488a6 | |||
| 2cdbf64b7a | |||
| 2a8b813e31 | |||
| 445a3ac3f5 | |||
| 37ec0437b1 | |||
| 127b7f0662 | |||
| 04a1548d5d | |||
| 3d22751c7a | |||
| a28ed463e7 | |||
| 25b51dd763 | |||
| bf26202290 | |||
| 53bdf34691 | |||
| 6fdebc88a5 | |||
| c1cb3a60dd | |||
| 4782cec115 | |||
| f15de22ce2 | |||
| 50cc159cc6 | |||
| 4ed3da6097 | |||
| fe60915147 | |||
| 267cf937be | |||
| 971369e968 | |||
| 60f3e1a0a4 | |||
| 66742754aa |
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: 问题反馈
|
||||
about: 如果你在使用过程中发现问题,请使用该模板进行反馈。
|
||||
labels: Bug
|
||||
---
|
||||
|
||||
### Ⅰ. 使用环境
|
||||
1. 操作系统:(Linux? macOS? Windows?)
|
||||
2. aliyunpan版本号:(v0.3.6? v0.3.7?...)
|
||||
|
||||
### Ⅱ. 问题描述
|
||||
|
||||
|
||||
### Ⅲ. 期望的结果
|
||||
|
||||
|
||||
### Ⅳ. 如何复现问题
|
||||
<!--
|
||||
如果该问题是必现的,请帮忙整理复现步骤。
|
||||
如果不是必现的,请尽可能说明问题出现之前你的操作步骤,越详细越好。
|
||||
-->
|
||||
1. xxx
|
||||
2. xxx
|
||||
3. xxx
|
||||
|
||||
### Ⅴ. 请提供相关的错误日志
|
||||
<!-- 最好能提供debug日志。如何开启debug日志请参考README文档。 -->
|
||||
```log
|
||||
(日志请贴在这里。如果日志很多,可以以附件的形式上传)
|
||||
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
name: 提新需求
|
||||
about: 如果你想提出一个新需求,新功能建议,请使用此模板。
|
||||
labels: Feature Request
|
||||
---
|
||||
|
||||
### Ⅰ. 你当前使用环境
|
||||
1. 操作系统:(Linux? macOS? Windows?)
|
||||
2. aliyunpan版本号:(v0.3.6? v0.3.7?...)
|
||||
|
||||
### Ⅱ. 请描述你想要的新功能
|
||||
<!-- 请简单描述你希望的新功能,例如:"upload上传命令添加一个xxx参数,用于实现xxx功能。" -->
|
||||
|
||||
### Ⅲ. 请描述新功能的使用场景
|
||||
<!-- 可以讲讲你对新功能的看法,为什么需要增加这个功能,这个新功能在哪些场景下会使用。 -->
|
||||
@@ -40,5 +40,8 @@ tests.sh
|
||||
main_test.go
|
||||
license_header.sh
|
||||
account.txt
|
||||
main_test.go
|
||||
git_push.sh
|
||||
git_push.sh
|
||||
git_pull.sh
|
||||
docker/sync/data
|
||||
clear_docker.sh
|
||||
upload.sh
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,539 +1,351 @@
|
||||
# 关于
|
||||
阿里云盘CLI。仿 Linux shell 文件处理命令的阿里云盘命令行客户端。
|
||||
阿里云盘CLI。仿 Linux shell 文件处理命令的阿里云盘命令行客户端,支持JavaScript插件,支持同步备份功能,支持相册批量下载。
|
||||
|
||||
# 相关说明
|
||||
1. 本项目还处于开发阶段,未经过充分的测试,如有bug或者好的建议欢迎提交issue
|
||||
2. 由于阿里云盘还在内测中,后面功能和接口随时会被修改,相对应的,本工具相关功能也会被影响
|
||||
3. 目前阶段优先处理Bug,功能增强的开发会相对往后安排
|
||||
# 特色
|
||||
1. 多平台支持, 支持 Windows, macOS, linux(x86/x64/arm), android, iOS 等
|
||||
2. 阿里云盘多用户支持
|
||||
3. 支持备份盘,资源库无缝切换
|
||||
4. [下载](docs/manual.md#下载文件目录)网盘内文件, 支持多个文件或目录下载, 支持断点续传和单文件并行下载。支持软链接(符号链接)文件。
|
||||
5. [上传](docs/manual.md#上传文件目录)本地文件, 支持多个文件或目录上传,支持排除指定文件夹/文件(正则表达式)功能。支持软链接(符号链接)文件。
|
||||
6. [同步备份功能](docs/manual.md#同步备份功能)支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份保持本地文件和网盘文件同步。常用于嵌入式或者NAS等设备,支持docker镜像部署。
|
||||
7. 命令和文件路径输入支持Tab键自动补全,路径支持通配符匹配模式
|
||||
8. 支持[JavaScript插件](docs/plugin_manual.md#简介),你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求
|
||||
9. 支持共享相册的相关操作,支持批量下载相册所有普通照片、实况照片文件到本地
|
||||
10. 支持多用户联合下载功能,对下载速度有极致追求的用户可以尝试使用该选项。详情请查看文档[多用户联合下载](docs/manual.md#多用户联合下载)
|
||||
|
||||
# 阿里云盘VIP会员推荐码
|
||||
如果大家有打算开通阿里云盘VIP会员,可以使用阿里云盘APP扫描下面的优惠推荐码进行开通。
|
||||
注意:您需要开通【三方应用权益包】,这样使用本程序下载才能加速,否则下载无法提速。
|
||||

|
||||
|
||||
# 目录
|
||||
- [关于](#关于)
|
||||
- [特色](#特色)
|
||||
- [下载/运行 说明](#下载运行说明)
|
||||
* [Windows](#windows)
|
||||
* [Linux / macOS](#linux--macos)
|
||||
- [命令列表及说明](#命令列表及说明)
|
||||
* [注意](#注意)
|
||||
* [检测程序更新](#检测程序更新)
|
||||
* [查看帮助](#查看帮助)
|
||||
* [登录阿里云盘帐号](#登录阿里云盘帐号)
|
||||
* [列出帐号列表](#列出帐号列表)
|
||||
* [获取当前帐号](#获取当前帐号)
|
||||
* [切换阿里云盘帐号](#切换阿里云盘帐号)
|
||||
* [退出阿里云盘帐号](#退出阿里云盘帐号)
|
||||
* [切换网盘(文件/相册)](#切换网盘)
|
||||
* [获取网盘配额](#获取网盘配额)
|
||||
* [切换工作目录](#切换工作目录)
|
||||
* [输出工作目录](#输出工作目录)
|
||||
* [列出目录](#列出目录)
|
||||
* [下载文件/目录](#下载文件目录)
|
||||
* [上传文件/目录](#上传文件目录)
|
||||
* [备份本地文件/目录](#备份本地文件目录)
|
||||
* [手动秒传文件](#手动秒传文件)
|
||||
* [创建目录](#创建目录)
|
||||
* [删除文件/目录](#删除文件目录)
|
||||
* [移动文件/目录](#移动文件目录)
|
||||
* [重命名文件/目录](#重命名文件目录)
|
||||
* [导出文件](#导出文件)
|
||||
* [导入文件](#导入文件)
|
||||
* [分享文件/目录](#分享文件目录)
|
||||
+ [设置分享文件/目录](#设置分享文件目录)
|
||||
+ [列出已分享文件/目录](#列出已分享文件目录)
|
||||
+ [取消分享文件/目录](#取消分享文件目录)
|
||||
+ [分享秒传链接](#分享秒传链接)
|
||||
* [显示和修改程序配置项](#显示和修改程序配置项)
|
||||
- [常见问题Q&A](#常见问题Q&A)
|
||||
* [1. 如何获取RefreshToken](#1-如何获取RefreshToken)
|
||||
* [2. 如何开启Debug调试日志](#1-如何开启Debug调试日志)
|
||||
- [目录](#目录)
|
||||
- [如何安装](#如何安装)
|
||||
- [直接下载安装](#直接下载安装)
|
||||
- [apt安装](#apt安装)
|
||||
- [yum安装](#yum安装)
|
||||
- [brew安装](#brew安装)
|
||||
- [winget安装](#winget安装)
|
||||
- [docker安装](#docker安装)
|
||||
- [sync同步盘](#sync同步盘)
|
||||
- [如何使用](#如何使用)
|
||||
- [基本使用](#基本使用)
|
||||
- [修改配置目录](#修改配置目录)
|
||||
- [启动程序](#启动程序)
|
||||
- [查看帮助](#查看帮助)
|
||||
- [登录](#登录)
|
||||
- [切换网盘](#切换网盘)
|
||||
- [查看文件列表](#查看文件列表)
|
||||
- [下载文件](#下载文件)
|
||||
- [上传文件](#上传文件)
|
||||
- [同步备份文件](#同步备份文件)
|
||||
- [更多命令](#更多命令)
|
||||
- [常见问题](#常见问题)
|
||||
- [如何开启Debug调试日志](#如何开启debug调试日志)
|
||||
- [第一步](#第一步)
|
||||
- [第二步](#第二步)
|
||||
- [如何登出和下线客户端](#如何登出和下线客户端)
|
||||
- [交流反馈](#交流反馈)
|
||||
- [鸣谢](#鸣谢)
|
||||
|
||||
# 如何安装
|
||||
## 直接下载安装
|
||||
可以直接在本仓库 [发布页](https://github.com/tickstep/aliyunpan/releases) 下载安装包,解压后使用。
|
||||
|
||||
要特别注意安装包的标签,不同的标签对应不同架构的系统,相关版本文件的标签说明如下:
|
||||
1. arm / armv5 / armv7 : 适用32位ARM系统
|
||||
2. arm64 : 适用64位ARM系统
|
||||
3. 386 / x86 : 适用32系统,包括Intel和AMD的CPU系统
|
||||
4. amd64 / x64 : 适用64位系统,包括Intel和AMD的CPU系统
|
||||
5. mips : 适用MIPS指令集的CPU,例如中标麒麟的系统。MIPS要注意大小端字序的不同对应的版本也不同。
|
||||
6. macOS amd64适用Intel CPU的机器,macOS arm64目前主要是适用苹果M系列芯片的机器
|
||||
7. iOS arm64适用iPhone手机,并且必须是越狱的手机才能正常运行
|
||||
8. loong64 : 适用于LoongArch64架构的机器,例如国产龙芯CPU
|
||||
|
||||
# 特色
|
||||
1. 多平台支持, 支持 Windows, macOS, linux(x86/x64/arm)等
|
||||
2. 阿里云盘多用户支持
|
||||
3. 支持文件网盘,相册网盘无缝切换
|
||||
4. 支持导入/导出功能,快速备份(导出)和恢复(导入)网盘的文件
|
||||
5. [下载](#下载文件目录)网盘内文件, 支持多个文件或目录下载, 支持断点续传和单文件并行下载
|
||||
6. [上传](#上传文件目录)本地文件, 支持多个文件或目录上传,支持排除指定文件夹/文件(正则表达式)功能
|
||||
7. [备份本地文件](#备份本地文件目录)支持备份本地文件夹到网盘中,保持本地文件和网盘文件同步。常用于嵌入式或者NAS等设备。
|
||||
|
||||
# 下载/运行说明
|
||||
|
||||
可以直接在[发布页](https://github.com/tickstep/aliyunpan/releases)下载使用.
|
||||
|
||||
也可以使用这个天翼云盘分享链接(阿里云盘还不支持zip压缩包分享)进行下载:[下载链接](https://cloud.189.cn/t/z2auuiFfYb2i)(访问码:5o9b)
|
||||
|
||||
如果程序运行时输出乱码, 请检查下终端的编码方式是否为 `UTF-8`.
|
||||
|
||||
使用本程序之前, 非常建议先学习一些 linux 基础命令知识.
|
||||
|
||||
如果没有带任何参数运行程序, 程序将会进入仿Linux shell系统用户界面的cli交互模式, 可直接运行相关命令.
|
||||
|
||||
cli交互模式下, 光标所在行的前缀应为 `aliyunpan >`, 如果登录了帐号则格式为 `aliyunpan:<工作目录> <用户昵称>$ `
|
||||
|
||||
程序会提供相关命令的使用说明.
|
||||
|
||||
## Windows
|
||||
|
||||
程序应在 命令提示符 (Command Prompt) 或 PowerShell 中运行.
|
||||
|
||||
也可直接双击程序运行, 具体使用方法请参见 [命令列表及说明](#命令列表及说明).
|
||||
|
||||
## Linux / macOS
|
||||
|
||||
程序应在 终端 (Terminal) 运行.
|
||||
|
||||
具体使用方法请参见 [命令列表及说明](#命令列表及说明) .
|
||||
|
||||
|
||||
# 命令列表及说明
|
||||
|
||||
## 注意
|
||||
|
||||
命令的前缀 `aliyunpan` 为指向程序运行的全路径名 (ARGv 的第一个参数)
|
||||
|
||||
直接运行程序时, 未带任何其他参数, 则程序进入cli交互模式, 进入cli模式运行以下命令时要把命令的前缀 `aliyunpan` 去掉! 即不需要输入`aliyunpan`。
|
||||
|
||||
cli交互模式支持按tab键自动补全命令.
|
||||
|
||||
## 检测程序更新
|
||||
```
|
||||
aliyunpan update
|
||||
参考例子:
|
||||
```shell
|
||||
wget https://github.com/tickstep/aliyunpan/releases/download/v0.4.0/aliyunpan-v0.4.0-linux-amd64.zip
|
||||
unzip aliyunpan-v0.4.0-linux-amd64.zip
|
||||
cd aliyunpan-v0.4.0-linux-amd64
|
||||
./aliyunpan
|
||||
```
|
||||
|
||||
## 查看帮助
|
||||
```
|
||||
aliyunpan help
|
||||
```
|
||||
### 例子
|
||||
```
|
||||
列出程序支持的命令
|
||||
aliyunpan help
|
||||
|
||||
查看login命令的帮助手册
|
||||
aliyunpan help login
|
||||
## apt安装
|
||||
适用于apt包管理器的系统,例如Ubuntu,国产deepin深度操作系统等。目前只支持amd64和arm64架构的机器。
|
||||
```shell
|
||||
sudo curl -fsSL http://file.tickstep.com/apt/pgp | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/tickstep-packages-archive-keyring.gpg > /dev/null && echo "deb [signed-by=/etc/apt/trusted.gpg.d/tickstep-packages-archive-keyring.gpg arch=amd64,arm64] http://file.tickstep.com/apt aliyunpan main" | sudo tee /etc/apt/sources.list.d/tickstep-aliyunpan.list > /dev/null && sudo apt-get update && sudo apt-get install -y aliyunpan
|
||||
|
||||
```
|
||||
|
||||
## 登录阿里云盘帐号
|
||||
## yum安装
|
||||
适用于yum包管理器的系统,例如CentOS、RockyLinux等。目前只支持amd64和arm64架构的机器。
|
||||
```shell
|
||||
sudo curl -fsSL http://file.tickstep.com/rpm/aliyunpan/aliyunpan.repo | sudo tee /etc/yum.repos.d/tickstep-aliyunpan.repo > /dev/null && sudo yum install aliyunpan -y
|
||||
|
||||
```
|
||||
|
||||
## brew安装
|
||||
适用于brew包管理器的系统,主要是苹果macOS系统。目前只支持amd64和arm64架构(Apple Silicon)的机器。
|
||||
```shell
|
||||
brew install aliyunpan
|
||||
|
||||
```
|
||||
由于brew默认安装在系统目录下面,这样配置文件也默认存放在系统目录里了,建议设置系统变量进行配置文件的单独存储,例如
|
||||
```shell
|
||||
export ALIYUNPAN_CONFIG_DIR=/Users/tickstep/Applications/adrive/config
|
||||
```
|
||||
|
||||
## winget安装
|
||||
适用于Windows系统的winget包管理器。目前只支持x86和x64架构的机器。
|
||||
|
||||
更新源(可选)
|
||||
```powershell
|
||||
winget source update
|
||||
|
||||
```
|
||||
安装
|
||||
```powershell
|
||||
winget install tickstep.aliyunpan --source winget --silent
|
||||
|
||||
```
|
||||
|
||||
## docker安装
|
||||
### sync同步盘
|
||||
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地两种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
备份功能支持以下模式:
|
||||
1. 备份本地文件,即上传本地文件到网盘,始终保持本地文件有一个完整的备份在网盘
|
||||
2. 备份云盘文件,即下载网盘文件到本地,始终保持网盘的文件有一个完整的备份在本地
|
||||
|
||||
备份功能支持指定备份策略:
|
||||
1. exclusive,排他备份文件(一比一镜像备份),目标目录多余的文件会被删除。保证备份的源目录,和目标目录文件一比一备份。源目录文件如果文件被删除,则对应的目标目录的文件也会被删除。
|
||||
2. increment,增量备份文件,目标目录多余的文件不会被删除。只会把源目录修改的文件,新增的文件备份到目标目录。如果源目录有文件删除,或者目标目录有其他文件新增是不会被删除。
|
||||
|
||||
同步的基本逻辑如下所示,一次循环包括:扫描-对比-执行,一共三个环节。
|
||||

|
||||
|
||||
```
|
||||
docker run -d --name=aliyunpan-sync --restart=always -v "<your aliyunpan_config.json>:/home/app/config/aliyunpan_config.json" -v "<your local dir>:/home/app/data" -e ALIYUNPAN_PAN_DIR="<your drive pan dir>" -e ALIYUNPAN_SYNC_MODE="upload" -e ALIYUNPAN_SYNC_POLICY="increment" -e ALIYUNPAN_SYNC_DRIVE="backup" -e ALIYUNPAN_SYNC_LOG="true" tickstep/aliyunpan-sync:v0.4.0
|
||||
|
||||
<your aliyunpan_config.json>: 用户已经登录成功并保存好的aliyunpan_config.json凭据文件
|
||||
<your local dir>:本地目标目录,绝对路径,例如:/tickstep/Documents/设计文档
|
||||
ALIYUNPAN_PAN_DIR:云盘目标目录,绝对路径
|
||||
ALIYUNPAN_SYNC_MODE:备份模式,支持: upload(备份本地文件到云盘),download(备份云盘文件到本地)
|
||||
ALIYUNPAN_SYNC_POLICY:备份策略,支持:exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)
|
||||
ALIYUNPAN_SYNC_DRIVE: 网盘,支持:backup(备份盘), resource(资源盘)
|
||||
ALIYUNPAN_SYNC_LOG: 同步日志,true-开启同步日志显示,false-关闭同步日志
|
||||
```
|
||||
docker-compose启动方式请查看:[docker-compose.yml](https://github.com/tickstep/aliyunpan/blob/main/docs/manual.md#Docker%E8%BF%90%E8%A1%8C)
|
||||
更详情文档请参考dockerhub网址:[tickstep/aliyunpan-sync](https://hub.docker.com/r/tickstep/aliyunpan-sync)
|
||||
|
||||
# 如何使用
|
||||
完整和详细的命令说明请查看手册:[命令手册](docs/manual.md)
|
||||
|
||||
1. Windows
|
||||
程序应在 命令提示符 (Command Prompt) 或 PowerShell 中运行.
|
||||
也可直接双击程序运行, 具体使用方法请参见 [命令列表及说明](docs/manual.md#命令列表及说明)
|
||||
|
||||
2. Linux / macOS
|
||||
程序应在 终端 (Terminal) 运行.
|
||||
具体使用方法请参见 [命令列表及说明](docs/manual.md#命令列表及说明)
|
||||
|
||||
如果程序运行时输出乱码, 请检查下终端的编码方式是否为 `UTF-8`.
|
||||
|
||||
如果没有带任何参数运行程序, 程序将会进入仿Linux shell系统用户界面的CLI交互模式, 可直接运行相关命令.
|
||||
在交互模式下, 光标所在行的前缀应为 `aliyunpan >`, 如果登录了帐号则格式为 `aliyunpan:<工作目录> <用户昵称>$ `
|
||||
|
||||
程序内置了相关命令的使用说明,你可以通过运行`命令 -h`的方式获取命令的使用说明,例如:`upload -h`获取上传命令的使用说明。
|
||||
|
||||
## 基本使用
|
||||
本程序支持阿里云盘大多数命令操作,这里只介绍基本的使用,更多更详细的命令请查看手册:[命令手册](docs/manual.md)。
|
||||
|
||||
### 修改配置目录
|
||||
你可以指定程序配置文件的存储路径,如果没有指定,程序会使用默认的目录。
|
||||
方法为设置环境变量`ALIYUNPAN_CONFIG_DIR`并指定一个存在的目录,例如linux下面可以这样指定
|
||||
```shell
|
||||
export ALIYUNPAN_CONFIG_DIR=/home/tickstep/tools/aliyunpan/config
|
||||
```
|
||||
|
||||
### 启动程序
|
||||
直接启动进入交互命令行
|
||||
```shell
|
||||
[tickstep@MacPro ~]$ aliyunpan
|
||||
提示: 方向键上下可切换历史命令.
|
||||
提示: Ctrl + A / E 跳转命令 首 / 尾.
|
||||
提示: 输入 help 获取帮助.
|
||||
aliyunpan >
|
||||
```
|
||||
|
||||
### 查看帮助
|
||||
```shell
|
||||
aliyunpan > help
|
||||
|
||||
...
|
||||
阿里云盘:
|
||||
album, abm 相簿(Beta)
|
||||
cd 切换工作目录
|
||||
download, d 下载文件/目录
|
||||
ls, l, ll 列出目录
|
||||
mkdir 创建目录
|
||||
mv 移动文件/目录
|
||||
pwd 输出工作目录
|
||||
recycle 回收站
|
||||
rename 重命名文件
|
||||
rm 删除文件/目录
|
||||
share 分享文件/目录
|
||||
sync 同步备份功能
|
||||
upload, u 上传文件/目录
|
||||
...
|
||||
```
|
||||
|
||||
### 登录
|
||||
当前支持使用RefreshToken进行登录。RefreshToken请参考 [1. 如何获取RefreshToken](#1-如何获取RefreshToken) 获取
|
||||
```
|
||||
aliyunpan login
|
||||
```
|
||||
|
||||
### 例子
|
||||
```
|
||||
按照引导步骤登录
|
||||
aliyunpan login
|
||||
请输入RefreshToken, 回车键提交 > 626a27b6193f4c5ca6ef0.......
|
||||
|
||||
命令行指定RefreshToken登录
|
||||
aliyunpan login -RefreshToken=626a27b6193f4c5ca6ef0.......
|
||||
```
|
||||
|
||||
|
||||
## 列出帐号列表
|
||||
|
||||
```
|
||||
aliyunpan loglist
|
||||
```
|
||||
|
||||
列出所有已登录的帐号
|
||||
|
||||
## 获取当前帐号
|
||||
|
||||
```
|
||||
aliyunpan who
|
||||
```
|
||||
|
||||
## 切换阿里云盘帐号
|
||||
|
||||
切换已登录的帐号
|
||||
```
|
||||
aliyunpan su <uid>
|
||||
```
|
||||
```
|
||||
aliyunpan su
|
||||
|
||||
请输入要切换帐号的 # 值 >
|
||||
```
|
||||
|
||||
## 退出阿里云盘帐号
|
||||
|
||||
退出当前登录的帐号
|
||||
```
|
||||
aliyunpan logout
|
||||
```
|
||||
|
||||
程序会进一步确认退出帐号, 防止误操作.
|
||||
|
||||
## 切换网盘
|
||||
程序默认工作在文件网盘下,如需切换到相册网盘,可以使用本命令进行切换。
|
||||
```
|
||||
aliyunpan drive <driveId>
|
||||
```
|
||||
```
|
||||
aliyunpan drive
|
||||
|
||||
输入要切换的网盘 # 值 >
|
||||
```
|
||||
|
||||
## 获取网盘配额
|
||||
|
||||
```
|
||||
aliyunpan quota
|
||||
```
|
||||
获取网盘的总储存空间, 和已使用的储存空间
|
||||
|
||||
## 切换工作目录
|
||||
```
|
||||
aliyunpan cd <目录>
|
||||
```
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 切换 /我的文档 工作目录
|
||||
aliyunpan cd /我的文档
|
||||
|
||||
# 切换 上级目录
|
||||
aliyunpan cd ..
|
||||
|
||||
# 切换 根目录
|
||||
aliyunpan cd /
|
||||
|
||||
```
|
||||
|
||||
## 输出工作目录
|
||||
```
|
||||
aliyunpan pwd
|
||||
```
|
||||
|
||||
## 列出目录
|
||||
|
||||
列出当前工作目录的文件和目录或指定目录
|
||||
```
|
||||
aliyunpan ls
|
||||
```
|
||||
```
|
||||
aliyunpan ls <目录>
|
||||
```
|
||||
|
||||
### 可选参数
|
||||
```
|
||||
-driveId value 网盘ID
|
||||
```
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 列出 我的文档 内的文件和目录
|
||||
aliyunpan ls 我的文档
|
||||
|
||||
# 绝对路径
|
||||
aliyunpan ls /我的文档
|
||||
|
||||
# 详细列出 我的文档 内的文件和目录
|
||||
aliyunpan ll /我的文档
|
||||
```
|
||||
|
||||
## 下载文件/目录
|
||||
```
|
||||
aliyunpan download <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
|
||||
aliyunpan d <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
|
||||
```
|
||||
|
||||
### 可选参数
|
||||
```
|
||||
--ow overwrite, 覆盖已存在的文件
|
||||
--status 输出所有线程的工作状态
|
||||
--save 将下载的文件直接保存到当前工作目录
|
||||
--saveto value 将下载的文件直接保存到指定的目录
|
||||
-x 为文件加上执行权限, (windows系统无效)
|
||||
-p value 指定下载线程数 (default: 0)
|
||||
-l value 指定同时进行下载文件的数量 (default: 0)
|
||||
--retry value 下载失败最大重试次数 (default: 3)
|
||||
--nocheck 下载文件完成后不校验文件
|
||||
--exn value 指定排除的文件夹或者文件的名称,只支持正则表达式。支持排除多个名称,每一个名称就是一个exn参数
|
||||
```
|
||||
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 设置保存目录, 保存到 D:\Downloads
|
||||
# 注意区别反斜杠 "\" 和 斜杠 "/" !!!
|
||||
aliyunpan config set -savedir D:/Downloads
|
||||
|
||||
# 下载 /我的文档/1.mp4
|
||||
aliyunpan d /我的文档/1.mp4
|
||||
|
||||
# 下载 /我的文档 整个目录!!
|
||||
aliyunpan d /我的文档
|
||||
```
|
||||
|
||||
下载的文件默认保存到 **程序所在目录** 的 download/ 目录, 支持设置指定目录, 重名的文件会自动跳过!
|
||||
|
||||
通过 `aliyunpan config set -savedir <savedir>` 可以自定义保存的目录.
|
||||
|
||||
支持多个文件或目录下载.
|
||||
|
||||
自动跳过下载重名的文件!
|
||||
|
||||
## 上传文件/目录
|
||||
```
|
||||
aliyunpan upload <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>
|
||||
aliyunpan u <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>
|
||||
```
|
||||
|
||||
### 例子:
|
||||
```
|
||||
# 将本地的 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录
|
||||
# 注意区别反斜杠 "\" 和 斜杠 "/" !!!
|
||||
aliyunpan upload C:/Users/Administrator/Desktop/1.mp4 /视频
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Desktop\1.mp4 和 C:\Users\Administrator\Desktop\2.mp4 上传到网盘 /视频 目录
|
||||
aliyunpan upload C:/Users/Administrator/Desktop/1.mp4 C:/Users/Administrator/Desktop/2.mp4 /视频
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录
|
||||
aliyunpan upload C:/Users/Administrator/Desktop /视频
|
||||
|
||||
## 下面演示文件或者文件夹排除功能
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
|
||||
aliyunpan upload -exn "\.jpg$" C:/Users/Administrator/Video /视频
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
|
||||
aliyunpan upload -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视频
|
||||
|
||||
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式
|
||||
1)排除@eadir文件或者文件夹:-exn "^@eadir$"
|
||||
2)排除.jpg文件:-exn "\.jpg$"
|
||||
3)排除.号开头的文件:-exn "^\."
|
||||
4)排除~号开头的文件:-exn "^~"
|
||||
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
|
||||
```
|
||||
|
||||
## 备份本地文件/目录
|
||||
|
||||
本地备份功能一般用于NAS等系统,日常只进行增量备份操作,默认情况下本地删除不影响网盘文件。
|
||||
|
||||
比如在手机上备分照片目录,就可以使用这个功能定时备份,备份完成后本地文件可安全删除。
|
||||
|
||||
基本用法和`upload`命令一样,额外增加两个参数
|
||||
|
||||
>1. delete 用于同步删除操作(只通过本地数据库记录对比)。
|
||||
>2. sync 使网盘上的文件和本地文件同步(本地为主),会删除网盘中不存在于本地的文件或目录(速度比较慢)。
|
||||
>sync 和 delete 的区别在于sync是通过网盘文件列表和本地文件进行判断。
|
||||
>delete 只通过本地数据库进行判断,正常情况下使用 delete 就够用了。
|
||||
|
||||
和 `upload` 相比由于增加了本地数据库,可以快速判断文件是否有更新等,大大减少了 API 的调用,操作速度更快。
|
||||
|
||||
```
|
||||
aliyunpan backup <本地目录1> <目录2> <目录3> ... <目标目录>
|
||||
注:
|
||||
1. 默认不删除网盘文件,可用 delete 或 sync 参数进行同步删除。
|
||||
```
|
||||
|
||||
### 例子:
|
||||
```
|
||||
# 将本地的 C:\Users\Administrator\Desktop 备份到网盘 /test 目录
|
||||
# 注意区别反斜杠 "\" 和 斜杠 "/" !!!
|
||||
aliyunpan backup C:/Users/Administrator/Desktop /test
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Desktop 备份到网盘 /test 目录,但排除所有 @eadir 目录
|
||||
aliyunpan backup -exn "^@eadir$" C:/Users/Administrator/Desktop /test
|
||||
```
|
||||
|
||||
## 手动秒传上传文件
|
||||
通过秒传链接上传文件到网盘,秒传链接可以通过share命令获取
|
||||
```
|
||||
aliyunpan rapidupload <秒传链接1> <秒传链接2> <秒传链接3> ...
|
||||
```
|
||||
|
||||
### 例子:
|
||||
```
|
||||
# 如果秒传成功, 则保存到网盘路径 /file.dmg
|
||||
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|"
|
||||
|
||||
# 如果秒传成功, 则保存到网盘路径 /pan_folder/file.dmg
|
||||
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder"
|
||||
|
||||
```
|
||||
|
||||
## 创建目录
|
||||
```
|
||||
aliyunpan mkdir <目录>
|
||||
```
|
||||
|
||||
### 例子
|
||||
```
|
||||
aliyunpan mkdir test123
|
||||
```
|
||||
|
||||
## 删除文件/目录
|
||||
```
|
||||
aliyunpan rm <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
|
||||
```
|
||||
|
||||
注意: 删除多个文件和目录时, 请确保每一个文件和目录都存在, 否则删除操作会失败.
|
||||
|
||||
被删除的文件或目录可在网盘文件回收站找回.
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 删除 /我的文档/1.mp4
|
||||
aliyunpan rm /我的文档/1.mp4
|
||||
|
||||
# 删除 /我的文档/1.mp4 和 /我的文档/2.mp4
|
||||
aliyunpan rm /我的文档/1.mp4 /我的文档/2.mp4
|
||||
|
||||
# 删除 /我的文档 整个目录 !!
|
||||
aliyunpan rm /我的文档
|
||||
```
|
||||
|
||||
|
||||
## 移动文件/目录
|
||||
```
|
||||
aliyunpan mv <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>
|
||||
```
|
||||
|
||||
注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败.
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 将 /我的文档/1.mp4 移动到 根目录 /
|
||||
aliyunpan mv /我的文档/1.mp4 /
|
||||
```
|
||||
|
||||
## 重命名文件/目录
|
||||
```
|
||||
aliyunpan rename <旧文件/目录名> <新文件/目录名>
|
||||
```
|
||||
|
||||
注意: 重命名的文件/目录,如果指定的是绝对路径,则必须保证新旧的绝对路径在同一个文件夹内,否则重命名失败!
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 将 /我的文档/1.mp4 重命名为 /我的文档/2.mp4
|
||||
aliyunpan rename /我的文档/1.mp4 /我的文档/2.mp4
|
||||
```
|
||||
|
||||
## 导出文件
|
||||
导出文件主要是用于备份网盘的文件。通过该命令将网盘的文件元数据信息导出并保存到本地文件中,等到以后需要的时候再通过import命令导入到网盘中。
|
||||
```
|
||||
aliyunpan export <网盘文件/目录的路径1> <文件/目录2> <文件/目录3> ... <本地保存文件路径>
|
||||
```
|
||||
导出指定文件/目录下面的所有文件的元数据信息,并保存到指定的本地文件里面。导出的文件元信息可以使用 import 命令(秒传文件功能)导入到网盘中。
|
||||
|
||||
### 例子
|
||||
```
|
||||
导出 /我的资源 整个目录 元数据到文件 /Users/tickstep/Downloads/export_files.txt
|
||||
aliyunpan export /我的资源 /Users/tickstep/Downloads/export_files.txt
|
||||
|
||||
导出 网盘 整个目录 元数据到文件 /Users/tickstep/Downloads/export_files.txt
|
||||
aliyunpan export / /Users/tickstep/Downloads/export_files.txt
|
||||
```
|
||||
|
||||
## 导入文件
|
||||
```
|
||||
aliyunpan export <本地元数据文件路径>
|
||||
```
|
||||
导入文件中记录的元数据文件到网盘。保存到网盘的文件会使用文件元数据记录的路径位置,如果没有指定云盘目录(saveto)则默认导入到目录 aliyunpan 中。
|
||||
导入的文件可以使用 export 命令获得。
|
||||
|
||||
导入文件每一行是一个文件元数据,样例如下:
|
||||
```
|
||||
aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|
|
||||
```
|
||||
需要先登录,已经登录过的可以跳过此步。登录是在浏览器上进行,你需要进行两次登录。
|
||||
```shell
|
||||
aliyunpan > login
|
||||
请在浏览器打开以下链接进行登录,链接有效时间为5分钟。
|
||||
注意:你需要进行一次授权一次扫码的两次登录。
|
||||
https://openapi.alipan.com/oauth/authorize?client_id=cf9f70e8fc61430f8ec5ab5cadf31375&redirect_uri=https%3A%2F%2Fapi.tickstep.com%2Fauth%2Ftickstep%2Faliyunpan%2Ftoken%2Fopenapi%2F8206f0.....fb5db6b40336%2Fauth&scope=user:base,file:all:read,file:all:write
|
||||
阿里云盘登录成功: tickstep
|
||||
aliyunpan:/ tickstep$
|
||||
```
|
||||
|
||||
### 切换网盘
|
||||
支持切换备份盘、资源库,默认工作在备份盘下。
|
||||
```shell
|
||||
aliyunpan:/ tickstep(备份盘)$ drive
|
||||
# DRIVE ID 网盘名称
|
||||
1 11519221 备份盘
|
||||
2 1311893110 资源库
|
||||
|
||||
### 例子
|
||||
```
|
||||
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项
|
||||
aliyunpan import /Users/tickstep/Downloads/export_files.txt
|
||||
|
||||
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项并保存到目录 /my2020 中
|
||||
aliyunpan import -saveto=/my2020 /Users/tickstep/Downloads/export_files.txt
|
||||
|
||||
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项并保存到网盘根目录 / 中
|
||||
aliyunpan import -saveto=/ /Users/tickstep/Downloads/export_files.txt
|
||||
输入要切换的网盘 # 值 > 2
|
||||
切换到网盘:资源库
|
||||
aliyunpan:/ tickstep(资源库)$
|
||||
```
|
||||
|
||||
## 分享文件/目录
|
||||
```
|
||||
aliyunpan share
|
||||
### 查看文件列表
|
||||
```shell
|
||||
aliyunpan:/ tickstep$ ls
|
||||
|
||||
# 文件大小 修改日期 文件(目录)
|
||||
0 - 2021-11-03 13:32:22 临时/
|
||||
1 - 2021-07-10 07:44:34 好友的分享/
|
||||
2 - 2021-07-09 22:11:22 我的项目/
|
||||
3 - 2021-07-09 22:10:37 我的游戏/
|
||||
4 - 2021-07-09 22:10:10 我的文档/
|
||||
5 349.86KB 2021-06-06 11:46:02 使用统计.xls
|
||||
6 503.57KB 2021-06-06 11:46:02 IMG_0098.JPG
|
||||
7 72.20KB 2021-06-06 11:46:02 IMG_0103.PNG
|
||||
总: 3.20MB 文件总数: 3, 目录总数: 7
|
||||
----
|
||||
```
|
||||
|
||||
### 设置分享文件/目录
|
||||
阿里目前之支持少数文件类型的分享,不支持的文件分享会提示分享失败
|
||||
### 下载文件
|
||||
如果需要更改默认下载目录路径,可以通过 `aliyunpan config set -savedir <savedir>` 自定义下载文件保存的目录。
|
||||
```shell
|
||||
aliyunpan:/ tickstep$ download IMG_0106.JPG
|
||||
|
||||
[0] 当前文件下载最大并发量为: 5, 下载缓存为: 64.00KB
|
||||
[1] 加入下载队列: /IMG_0106.JPG
|
||||
[1] ----
|
||||
文件ID: 60bc44f855814e19692a4958b4a8823a1a06e5de
|
||||
文件名: IMG_0106.JPG
|
||||
文件类型: 文件
|
||||
文件路径: /IMG_0106.JPG
|
||||
|
||||
[1] 准备下载: /IMG_0106.JPG
|
||||
[1] 将会下载到路径: /root/Downloads/4d001d48564f43b..62874f04bbe6/IMG_0106.JPG
|
||||
[1] 下载开始
|
||||
[1] ↓ 704.00KB/1.48MB 0B/s(1.69MB/s) in 1.88s, left - ............
|
||||
[1] 下载完成, 保存位置: /root/Downloads/4d001d48564f43b..62874f04bbe6/IMG_0106.JPG
|
||||
[1] 检验文件有效性成功: /root/Downloads/4d001d48564f43b..62874f04bbe6/IMG_0106.JPG
|
||||
|
||||
下载结束, 时间: 4秒, 数据总量: 1.48MB
|
||||
```
|
||||
aliyunpan share set <文件/目录1> <文件/目录2> ...
|
||||
aliyunpan share s <文件/目录1> <文件/目录2> ...
|
||||
或者使用UI面板展示下载进度(使用-ui选项),更加直观和友好。
|
||||
```shell
|
||||
aliyunpan:/ tickstep$ download /photo -ui
|
||||
┌──────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│总速度: 341.59 KB/s | 文件: 0/7 | 失败: 0 | 状态: 下载中 | 已用时间: 9s | 剩余时间: 51s │
|
||||
│总进度: [===> ] 7.4% (896.00KB/11.83MB) │
|
||||
│──────────────────────────────────────────────────────────────────────────────────────────│
|
||||
│ 1. IMG_0005.JPG [============> ] 66.9% 258.42KB/s 3s │
|
||||
│ 2. IMG_0007.JPG [============> ] 67.3% 258.54KB/s 3s │
|
||||
│ 3. IMG_0009.JPG [ ] 0.0% 等待中 - │
|
||||
│──────────────────────────────────────────────────────────────────────────────────────────│
|
||||
│[10:39:24] [0] 当前文件下载最大并发量为: 2, 单文件下载分片线程数为: 1, 下载缓存为: 64.00KB │
|
||||
│[10:39:27] [2] 加入下载队列: /photo/p/IMG_0007.JPG │
|
||||
│[10:39:27] [2] ---- 文件ID: 67dfe476a7573e76a2034626bd013f36581296ed 文件名: IMG_0007.... │
|
||||
│[10:39:27] [3] 准备下载: /photo/p/IMG_0007.JPG │
|
||||
│[10:39:27] [2] 将会下载到路径: /Users/tickstep/Downloads/4d001d48564f43b3bc5662874f04bb... │
|
||||
│[10:39:28] [2] 下载开始 │
|
||||
└──────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 列出已分享文件/目录
|
||||
### 上传文件
|
||||
```shell
|
||||
aliyunpan:/ tickstep$ upload /Users/tickstep/Downloads/apt.zip /tmp
|
||||
|
||||
[0] 当前文件上传最大并发量为: 10, 上传分片大小为: 10.00MB
|
||||
[1] 加入上传队列: /Users/tickstep/Downloads/apt.zip
|
||||
[1] 2022-08-13 13:41:22 准备上传: /Users/tickstep/Downloads/apt.zip => /tmp/apt.zip
|
||||
[1] 2022-08-13 13:41:22 正在检测和创建云盘文件夹: /tmp
|
||||
[1] 2022-08-13 13:41:22 正在计算文件SHA1: /Users/tickstep/Downloads/apt.zip
|
||||
[1] 2022-08-13 13:41:22 检测秒传中, 请稍候...
|
||||
[1] 2022-08-13 13:41:22 秒传失败,开始正常上传文件
|
||||
[1] ↑ 21.00MB/21.00MB 702.53KB/s(702.70KB/s) in 15s ............
|
||||
[1] 2022-08-13 13:41:22 上传文件成功, 保存到网盘路径: /tmp/apt.zip
|
||||
[1] 2022-08-13 13:41:22 文件上传结果: 成功! 耗时 18秒
|
||||
|
||||
上传结束, 时间: 18秒, 数据总量: 21.00MB
|
||||
```
|
||||
aliyunpan share list
|
||||
aliyunpan share l
|
||||
或者使用UI面板展示上传进度(使用-ui选项),更加直观和友好。
|
||||
```shell
|
||||
aliyunpan:/ tickstep$ upload /Users/tickstep/Downloads /photo -ui
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│总速度: 3.62 MB/s | 文件: 9/14 | 失败: 0 | 状态: 上传中 | 已用时间: 12s | 剩余时间: 17s │
|
||||
│总进度: [==========================> ] 51.7% (68.56MB/132.73MB) │
|
||||
│─────────────────────────────────────────────────────────────────────────────────────────│
|
||||
│ 1. System Design Interview 2.zip [====> ] 26.2% 3.25MB/s 18s │
|
||||
│ 2. ips_20260228.txt [ ] 0.0% 0 B/s - │
|
||||
│ 3. neko-agent [ ] 0.0% 0 B/s - │
|
||||
│─────────────────────────────────────────────────────────────────────────────────────────│
|
||||
│[11:14:49] [9] 2026-03-02 11:14:26 正在计算文件SHA1: /Users/tickstep/Downloads/test/da... │
|
||||
│[11:14:49] [13] 2026-03-02 11:14:26 正在检测和创建云盘文件夹: /photo/p2/test/output │
|
||||
│[11:14:49] [9] 2026-03-02 11:14:26 检测秒传中, 请稍候... │
|
||||
│[11:14:49] [9] 2026-03-02 11:14:26 秒传成功, 保存到网盘路径: /photo/p2/test/data/ips_2... │
|
||||
│[11:14:49] [9] 2026-03-02 11:14:26 文件上传结果: 成功! 耗时 12秒 │
|
||||
│[11:14:49] 完成: /Users/tickstep/Downloads/test/data/ips_20260226.txt │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 取消分享文件/目录
|
||||
```
|
||||
aliyunpan share cancel <shareid_1> <shareid_2> ...
|
||||
aliyunpan share c <shareid_1> <shareid_2> ...
|
||||
```
|
||||
目前只支持通过分享id (shareid) 来取消分享.
|
||||
### 同步备份文件
|
||||
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地两种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
|
||||
例如:将本地目录 `/tickstep/Documents/设计文档` 中的文件备份上传到云盘目录 `/备份盘/我的文档`
|
||||
```shell
|
||||
aliyunpan:/ tickstep$ sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload" -drive "backup"
|
||||
|
||||
启动同步备份进程
|
||||
备份配置文件:(使用命令行配置)
|
||||
链接类型:默认链接
|
||||
下载并发:2
|
||||
上传并发:2
|
||||
下载分片大小:1.00MB
|
||||
上传分片大小:10.00MB
|
||||
|
||||
### 分享秒传链接
|
||||
秒传链接支持所有类型的文件分享,可以突破阿里的分享限制。得到的链接可以使用import或者rapidupload命令保存到自己网盘中。
|
||||
秒传链接只支持文件分享,不支持文件夹。如果指定文件夹会创建文件夹下所有文件的秒传链接。
|
||||
```
|
||||
aliyunpan share mc <文件/目录1> <文件/目录2> ...
|
||||
|
||||
例子
|
||||
# 创建文件 1.mp4 的秒传链接
|
||||
aliyunpan share mc 1.mp4
|
||||
|
||||
# 创建文件 1.mp4 的秒传链接,但链接隐藏相对路径
|
||||
aliyunpan share mc -hp 1.mp4
|
||||
|
||||
# 创建文件夹 share_folder 下面所有文件的秒传链接
|
||||
aliyunpan share mc share_folder/
|
||||
启动同步任务
|
||||
任务: 设计文档(de3d6b69a607497b73624bcca0845f19)
|
||||
同步模式: 备份本地文件(只上传)
|
||||
本地目录: /tickstep/Documents/设计文档
|
||||
云盘目录: /备份盘/我的文档
|
||||
```
|
||||
|
||||
## 更多命令
|
||||
更多更详细的命令请查看手册:[命令手册](docs/manual.md)。
|
||||
|
||||
## 显示和修改程序配置项
|
||||
```
|
||||
# 显示配置
|
||||
aliyunpan config
|
||||
|
||||
# 设置配置
|
||||
aliyunpan config set
|
||||
```
|
||||
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 显示所有可以设置的值
|
||||
aliyunpan config -h
|
||||
aliyunpan config set -h
|
||||
|
||||
# 设置下载文件的储存目录
|
||||
aliyunpan config set -savedir D:/Downloads
|
||||
|
||||
# 设置下载最大并发量为 15
|
||||
aliyunpan config set -max_download_parallel 15
|
||||
|
||||
# 组合设置
|
||||
aliyunpan config set -max_download_parallel 15 -savedir D:/Downloads
|
||||
```
|
||||
|
||||
# 常见问题Q&A
|
||||
## 1 如何获取RefreshToken
|
||||
需要通过浏览器获取refresh_token。这里以Chrome浏览器为例,其他浏览器类似。
|
||||
打开 [阿里云盘网页](https://www.aliyundrive.com/drive) 并进行登录,然后F12按键打开浏览器调试菜单,按照下面步骤进行
|
||||

|
||||
|
||||
## 2 如何开启Debug调试日志
|
||||
# 常见问题
|
||||
## 如何开启Debug调试日志
|
||||
当需要定位问题,或者提交issue的时候抓取log,则需要开启debug日志。步骤如下:
|
||||
|
||||
### 第一步
|
||||
@@ -545,19 +357,29 @@ export ALIYUNPAN_VERBOSE=1
|
||||
|
||||
Windows
|
||||
不同版本会有些许不一样,请自行查询具体方法
|
||||
设置示意图如下:
|
||||
设置示意图如下:
|
||||

|
||||
|
||||
### 第二步
|
||||
打开aliyunpan命令行程序,任何云盘命令都有类似如下日志输出
|
||||
打开aliyunpan命令行程序,任何云盘命令都有类似如下日志输出
|
||||

|
||||
|
||||
## 如何登出和下线客户端
|
||||
阿里云盘单账户最多只允许同时登录 10 台设备
|
||||
当出现这个提示:你账号已超出最大登录设备数量,请先下线一台设备,然后重启本应用,才可以继续使用
|
||||
说明你的账号登录客户端已经超过数量,你需要先登出其他客户端才能继续使用,如下所示
|
||||
|
||||
APP手机端
|
||||

|
||||
Web网页端
|
||||

|
||||
|
||||
# 交流反馈
|
||||
|
||||
提交issue: [issues页面](https://github.com/tickstep/aliyunpan/issues)
|
||||
联系邮箱: tickstep@outlook.com
|
||||
|
||||
# 鸣谢
|
||||
本项目大量借鉴了以下相关项目的功能&成果
|
||||
> [tickstep/cloudpan189-go](https://github.com/tickstep/cloudpan189-go)
|
||||
> [tickstep/aliyunpan-api](https://github.com/tickstep/aliyunpan-api)
|
||||
> [tickstep/cloudpan189-go](https://github.com/tickstep/cloudpan189-go)
|
||||
> [kazutoiris/ali_ecc](https://github.com/kazutoiris/ali_ecc)
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity version="0.4.0.0" processorArchitecture="*" name="com.tickstep.aliyunpan" type="win32"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
||||
<dpiAware>true</dpiAware>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 371 KiB |
|
After Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 301 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 308 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 428 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 92 KiB |
@@ -0,0 +1,146 @@
|
||||
// ==========================================================================================
|
||||
// aliyunpan JS插件回调处理函数
|
||||
// 支持 JavaScript ECMAScript 5.1 语言规范
|
||||
//
|
||||
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
|
||||
// ==========================================================================================
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:下载文件前的回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 文件下载前参数
|
||||
// {
|
||||
// "downloadActionId": "20976308-a026-450d-92b8-16a969fed0c6",
|
||||
// "driveId": "19519221",
|
||||
// "driveFileName": "token.bat",
|
||||
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
|
||||
// "driveFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "driveFileSize": 125330,
|
||||
// "driveFileType": "file",
|
||||
// "driveFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "localFilePath": "aliyunpan\\Downloads\\token.bat"
|
||||
// }
|
||||
// downloadActionId - 下载动作ID。同一次下载动作(download命令)下载的文件,这个ID都是同一个值。注意这个ID不是永久的,每执行一次download命令会生成一个对应ID。
|
||||
// driveId - 网盘ID
|
||||
// driveFileName - 网盘文件名
|
||||
// driveFilePath - 网盘文件绝对完整路径
|
||||
// driveFileSize - 网盘文件大小,单位B
|
||||
// driveFileSha1 - 网盘文件SHA1
|
||||
// driveFileType - 网盘文件类型,file-文件,folder-文件夹
|
||||
// driveFileUpdatedAt - 网盘文件修改时间
|
||||
// localFilePath - 下载文件到本地保存的路径,这个是相对路径,相对指定下载的目标文件夹
|
||||
//
|
||||
// 返回值说明
|
||||
// {
|
||||
// "downloadApproved": "yes",
|
||||
// "localFilePath": "newfolder\\token.bat"
|
||||
// }
|
||||
// downloadApproved - 该文件是否下载,yes-确认下载,no-取消下载
|
||||
// localFilePath - 文件保存的本地路径,这个是相对路径,如果为空""代表保持原本的目标路径。
|
||||
// 这个改动要小心,会导致重名文件下载只会下载一个
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function downloadFilePrepareCallback(context, params) {
|
||||
console.log(params)
|
||||
|
||||
var result = {
|
||||
"downloadApproved": "yes",
|
||||
"localFilePath": ""
|
||||
};
|
||||
|
||||
// 所有的.dmg.exe文件,下载保存的文件后缀名去掉.exe,网盘文件不改动
|
||||
if (params["driveFileName"].lastIndexOf(".dmg.exe") > 0) {
|
||||
result["localFilePath"] = params["localFilePath"].substr(0, (params["localFilePath"].length - ".exe".length));
|
||||
}
|
||||
|
||||
// 禁止.txt文件下载
|
||||
if (params["driveFileName"].lastIndexOf(".txt") > 0) {
|
||||
result["downloadApproved"] = "no";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:下载文件结束的回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 文件下载结束参数
|
||||
// {
|
||||
// "downloadActionId": "20976308-a026-450d-92b8-16a969fed0c6",
|
||||
// "driveId": "19519221",
|
||||
// "driveFileId": "65ebb9396d66dfcd9f25414bbdd81594ecb9fcd6",
|
||||
// "driveFileName": "token.bat",
|
||||
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
|
||||
// "driveFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "driveFileSize": 125330,
|
||||
// "driveFileType": "file",
|
||||
// "driveFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "downloadResult": "success",
|
||||
// "localFilePath": "aliyunpan\\Downloads\\token.bat"
|
||||
// }
|
||||
// downloadActionId - 下载动作ID。同一次下载动作(download命令)下载的文件,这个ID都是同一个值。注意这个ID不是永久的,每执行一次download命令会生成一个对应ID。
|
||||
// driveId - 网盘ID
|
||||
// driveFileId - 网盘文件ID
|
||||
// driveFileName - 网盘文件名
|
||||
// driveFilePath - 网盘文件绝对完整路径
|
||||
// driveFileSize - 网盘文件大小,单位B
|
||||
// driveFileSha1 - 网盘文件SHA1
|
||||
// driveFileType - 网盘文件类型,file-文件,folder-文件夹
|
||||
// driveFileUpdatedAt - 网盘文件修改时间
|
||||
// downloadResult - 下载结果,success-成功,fail-失败
|
||||
// localFilePath - 下载文件到本地保存的路径,这个是相对路径,相对指定下载的目标文件夹
|
||||
//
|
||||
// 返回值说明
|
||||
// (没有返回值)
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function downloadFileFinishCallback(context, params) {
|
||||
console.log(params)
|
||||
|
||||
// // 云盘文件成功下载到本地后,删除云盘的文件
|
||||
// if (params["downloadResult"] == "success") {
|
||||
// if (params["driveFileType"] == "file") {
|
||||
// // 文件下载成功,删除该云盘文件
|
||||
// var userId = context["userId"]
|
||||
// var driveId = params["driveId"]
|
||||
// var driveFileId = params["driveFileId"]
|
||||
// if (PluginUtil.PanFS.deleteFile(userId, driveId, driveFileId)) {
|
||||
// console.log("插件删除云盘文件成功:" + params["driveFilePath"])
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// ==========================================================================================
|
||||
// aliyunpan JS插件回调处理函数
|
||||
// 支持 JavaScript ECMAScript 5.1 语言规范
|
||||
//
|
||||
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
|
||||
// ==========================================================================================
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:删除文件前的回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 删除文件前的调用参数
|
||||
// {
|
||||
// "count": 10,
|
||||
// "items": [{}, {}, ...],
|
||||
// }
|
||||
// count - 文件总数
|
||||
// items - 文件列表,文件详情请查看下面 RemoveFilePrepareItem 定义
|
||||
//
|
||||
// RemoveFilePrepareItem - 文件详情参数
|
||||
// {
|
||||
// "driveId": "19519221",
|
||||
// "driveFileId": "65f6c5161ee4a1b9a02b41399282f281c6e6df21",
|
||||
// "driveFileName": "token.bat",
|
||||
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
|
||||
// "driveFileSize": 125330,
|
||||
// "driveFileType": "file",
|
||||
// "driveFileUpdatedAt": "2025-03-02 10:39:14",
|
||||
// "driveFileCreatedAt": "2025-03-02 10:39:14"
|
||||
// }
|
||||
// driveId - 网盘ID
|
||||
// driveFileId - 网盘文件的ID
|
||||
// driveFileName - 网盘文件名
|
||||
// driveFilePath - 网盘文件绝对完整路径
|
||||
// driveFileSize - 网盘文件大小,单位B
|
||||
// driveFileType - 网盘文件类型,file-文件,folder-文件夹
|
||||
// driveFileUpdatedAt - 网盘文件修改时间
|
||||
// driveFileCreatedAt - 网盘文件创建时间
|
||||
//
|
||||
// 返回值说明
|
||||
// {
|
||||
// "result": [{}, {}, ...],
|
||||
// }
|
||||
// result - 结果列表,每个文件的确认结果,文件详细定义请查看 RemoveFilePrepareResultItem 定义
|
||||
//
|
||||
// RemoveFilePrepareResultItem - 每项文件确认结果
|
||||
// {
|
||||
// "driveId": "19519221",
|
||||
// "driveFileId": "65f6c5161ee4a1b9a02b41399282f281c6e6df21",
|
||||
// "removeApproved": "yes"
|
||||
// }
|
||||
// driveId - 网盘ID
|
||||
// driveFileId - 网盘文件的ID
|
||||
// removeApproved - 该文件是否能删除,yes-确认删除,no-不能删除
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function removeFilePrepareCallback(context, params) {
|
||||
console.log(params)
|
||||
var result = {"result":[]};
|
||||
for(var i = 0; i < params.items.length; i++) {
|
||||
var fileItem = params.items[i]
|
||||
console.log(fileItem);
|
||||
var fileItemApprovedResult = {
|
||||
"driveId": fileItem["driveId"],
|
||||
"driveFileId": fileItem["driveFileId"],
|
||||
"removeApproved": "yes"
|
||||
}
|
||||
|
||||
// 可以通过名称过滤
|
||||
if (fileItem["driveFileType"] == "file") { // 文件
|
||||
if (fileItem["driveFileName"] == "password.key") {
|
||||
fileItemApprovedResult["removeApproved"] = "no"; // 禁止删除password.key文件
|
||||
}
|
||||
} else if (params["driveFileType"] == "folder") { // 文件夹
|
||||
// 对文件夹进行处理
|
||||
}
|
||||
|
||||
// 可以通过时间过滤,例如:保留最近10天的文件
|
||||
// ...
|
||||
|
||||
// 可以通过数量过滤,例如:保留最新5个文件
|
||||
// ...
|
||||
|
||||
// 放入结果列表
|
||||
result["result"].push(fileItemApprovedResult)
|
||||
}
|
||||
// 返回
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
// ==========================================================================================
|
||||
// aliyunpan JS插件回调处理函数
|
||||
// 支持 JavaScript ECMAScript 5.1 语言规范
|
||||
//
|
||||
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
|
||||
// ==========================================================================================
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:同步备份-扫描本地文件前的回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 扫描本地文件前参数
|
||||
// {
|
||||
// "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
|
||||
// "localFileName": "token.bat",
|
||||
// "localFileSize": 125330,
|
||||
// "localFileType": "file",
|
||||
// "localFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "driveId": "19519221"
|
||||
// }
|
||||
// localFilePath - 本地文件绝对完整路径
|
||||
// localFileName - 本地文件名
|
||||
// localFileSize - 本地文件大小,单位B
|
||||
// localFileType - 本地文件类型,file-文件,folder-文件夹
|
||||
// localFileUpdatedAt - 文件修改时间
|
||||
// driveId - 备份的目标网盘ID
|
||||
//
|
||||
// 返回值说明
|
||||
// {
|
||||
// "syncScanLocalApproved": "yes"
|
||||
// }
|
||||
// syncScanLocalApproved - 该文件是否确认扫描,yes-允许扫描,no-禁止扫描。
|
||||
// 禁止扫描的文件不会执行后续的动作,例如上传,下载。
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function syncScanLocalFilePrepareCallback(context, params) {
|
||||
console.log(params);
|
||||
var result = {
|
||||
"syncScanLocalApproved": "yes"
|
||||
};
|
||||
|
||||
// 禁止.开头文件上传
|
||||
if (params["localFileName"].indexOf(".") == 0) {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止~$开头文件上传(office暂存临时文件)
|
||||
if (params["localFileName"].indexOf("~$") == 0) {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止.txt文件上传(正则表达式方式)
|
||||
if (params["localFileName"].search(/.txt$/i) >= 0) {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止password.key文件上传
|
||||
if (params["localFileName"] == "password.key") {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止@eadir文件上传
|
||||
if (params["localFileName"] == "@eadir") {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:同步备份-扫描云盘文件前的回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 扫描云盘文件前参数
|
||||
// {
|
||||
// "driveId": "19519221",
|
||||
// "driveFileName": "token.bat",
|
||||
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
|
||||
// "driveFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "driveFileSize": 125330,
|
||||
// "driveFileType": "file",
|
||||
// "driveFileUpdatedAt": "2022-04-14 07:05:12"
|
||||
// }
|
||||
// driveId - 网盘ID
|
||||
// driveFileName - 网盘文件名
|
||||
// driveFilePath - 网盘文件绝对完整路径
|
||||
// driveFileSize - 网盘文件大小,单位B
|
||||
// driveFileSha1 - 网盘文件SHA1
|
||||
// driveFileType - 网盘文件类型,file-文件,folder-文件夹
|
||||
// driveFileUpdatedAt - 网盘文件修改时间
|
||||
//
|
||||
// 返回值说明
|
||||
// {
|
||||
// "syncScanPanApproved": "yes"
|
||||
// }
|
||||
// syncScanPanApproved - 该文件是否确认扫描,yes-允许扫描,no-禁止扫描。
|
||||
// 禁止扫描的文件不会执行后续的动作,例如上传,下载。
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function syncScanPanFilePrepareCallback(context, params) {
|
||||
console.log(params);
|
||||
|
||||
var result = {
|
||||
"syncScanPanApproved": "yes"
|
||||
};
|
||||
|
||||
// 禁止.开头文件下载
|
||||
if (params["driveFileName"].indexOf(".") == 0) {
|
||||
result["syncScanPanApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止~$开头文件下载(office暂存临时文件)
|
||||
if (params["driveFileName"].indexOf("~$") == 0) {
|
||||
result["syncScanPanApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止.txt文件下载(正则表达式方式)
|
||||
// if (params["driveFileName"].search(/.txt$/i) >= 0) {
|
||||
// result["syncScanPanApproved"] = "no";
|
||||
// }
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:同步备份-同步文件后的回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 同步文件后的参数
|
||||
// {
|
||||
// "action": "upload",
|
||||
// "actionResult": "success",
|
||||
// "driveId": "19519221",
|
||||
// "driveFileId": "65ebb9396d66dfcd9f25414bbdd81594ecb9fcd6",
|
||||
// "fileName": "1.txt",
|
||||
// "filePath": "D:/goprojects/dev/upload/1.txt",
|
||||
// "fileSha1": "294C8F24C56042710813E95C55A0B018299BA9A7",
|
||||
// "fileSize": "28077",
|
||||
// "fileType": "file",
|
||||
// "fileUpdatedAt": "2022-03-04 15:19:47"
|
||||
// }
|
||||
// action - 同步动作,download-下载文件,upload-上传文件
|
||||
// actionResult - 同步结果,success-成功,fail-失败
|
||||
// driveId - 网盘ID
|
||||
// driveFileId - 网盘文件ID,只有download模式才会有这个字段
|
||||
// fileName - 文件名称
|
||||
// filePath - 文件完整路径
|
||||
// fileSha1 - 文件SHA1
|
||||
// fileSize - 文件大小,单位B
|
||||
// fileType - 文件类型,file-文件,folder-文件夹
|
||||
// fileUpdatedAt - 文件修改时间
|
||||
//
|
||||
// 返回值说明
|
||||
// (无)
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function syncFileFinishCallback(context, params) {
|
||||
console.log(params)
|
||||
// 云盘文件成功下载到本地后,删除云盘的文件
|
||||
// if (params["action"] == "download") {
|
||||
// if (params["actionResult"] == "success" && params["fileType"] == "file") {
|
||||
// // 文件下载成功,删除该云盘文件
|
||||
// var userId = context["userId"]
|
||||
// var driveId = params["driveId"]
|
||||
// var driveFileId = params["driveFileId"]
|
||||
// if (PluginUtil.PanFS.deleteFile(userId, driveId, driveFileId)) {
|
||||
// console.println("插件删除云盘文件成功:" + params["filePath"])
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:同步备份-同步任务同步全部文件完成时的回调函数(目前只有"只运行一次备份"的任务模式才有该回调)
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 任务完成的回调参数
|
||||
// {
|
||||
// "name": "备份相册",
|
||||
// "id": "2e959a34-8435-4773-9a3e-687f2692ddc1",
|
||||
// "userId": "4d001d48564f43b3bc5662874114bbe6",
|
||||
// "driveName": "backup",
|
||||
// "driveId": "19519221",
|
||||
// "localFolderPath": "/Volumes/DataDisk3T/Games/Switch/图库备份",
|
||||
// "panFolderPath": "/备份盘/Switch游戏/Album",
|
||||
// "mode": "upload",
|
||||
// "policy": "upload",
|
||||
// }
|
||||
// name - 任务名称
|
||||
// id - 任务ID
|
||||
// userId - 登录的用户ID
|
||||
// driveName - 网盘名称,backup-备份盘,resource-资源盘
|
||||
// driveId - 目标网盘ID
|
||||
// localFolderPath - 本地目录
|
||||
// panFolderPath - 云盘目录
|
||||
// mode - 备份模式,upload-备份本地文件到云盘,download-备份云盘文件到本地
|
||||
// policy - 备份策略,increment-增量备份,exclusive-排他备份
|
||||
//
|
||||
// 返回值说明
|
||||
// 无
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function syncAllFileFinishCallback(context, params) {
|
||||
console.log(params)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// ==========================================================================================
|
||||
// aliyunpan JS插件回调处理函数
|
||||
// 支持 JavaScript ECMAScript 5.1 语言规范
|
||||
//
|
||||
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
|
||||
// ==========================================================================================
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:用户Token刷新完成后回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - Token刷新参数
|
||||
// {
|
||||
// "result": "success",
|
||||
// "message": "ok",
|
||||
// "oldToken": "aa31fcc229c54d5ab6d8bfb17aff3711",
|
||||
// "newToken": "bb31fcc229c54d5ab6d8bfb17aff3722",
|
||||
// "updatedAt": "2022-04-14 07:05:12"
|
||||
// }
|
||||
// result - Token刷新的结果,success-成功,fail-失败
|
||||
// message - 消息说明,如果失败这里会有原因说明
|
||||
// oldToken - 刷新前的Token
|
||||
// newToken - 刷新后的Token,只有result为success的才有该值
|
||||
// updatedAt - Token刷新的时间
|
||||
//
|
||||
// 返回值说明
|
||||
// (没有返回值)
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function userTokenRefreshFinishCallback(context, params) {
|
||||
console.log(params)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// ==========================================================================================
|
||||
// aliyunpan JS插件回调处理函数
|
||||
// 支持 JavaScript ECMAScript 5.1 语言规范
|
||||
//
|
||||
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
|
||||
// ==========================================================================================
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:上传文件前的回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 文件上传前参数
|
||||
// {
|
||||
// "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
|
||||
// "localFileName": "token.bat",
|
||||
// "localFileSize": 125330,
|
||||
// "localFileType": "file",
|
||||
// "localFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "driveId": "19519221",
|
||||
// "driveFilePath": "aliyunpan/Downloads/token.bat"
|
||||
// }
|
||||
// localFilePath - 本地文件绝对完整路径
|
||||
// localFileName - 本地文件名
|
||||
// localFileSize - 本地文件大小,单位B
|
||||
// localFileType - 本地文件类型,file-文件,folder-文件夹
|
||||
// localFileUpdatedAt - 文件修改时间
|
||||
// driveId - 准备上传的目标网盘ID
|
||||
// driveFilePath - 准备上传的目标网盘保存的路径,这个是相对路径,相对指定上传的目标文件夹
|
||||
//
|
||||
// 返回值说明
|
||||
// {
|
||||
// "uploadApproved": "yes",
|
||||
// "driveFilePath": "newfolder/token.bat"
|
||||
// }
|
||||
// uploadApproved - 该文件是否确认上传,yes-允许上传,no-禁止上传
|
||||
// driveFilePath - 文件保存的网盘路径,这个是相对路径,如果为空""代表保持原本的目标路径。
|
||||
// 这个改动要小心,会导致重名文件只会上传一个
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function uploadFilePrepareCallback(context, params) {
|
||||
var result = {
|
||||
"uploadApproved": "yes",
|
||||
"driveFilePath": ""
|
||||
};
|
||||
|
||||
// 所有的.dmg文件,网盘保存的文件增加后缀名为.exe,本地文件不改动
|
||||
if (params["localFilePath"].lastIndexOf(".dmg") > 0) {
|
||||
result["driveFilePath"] = params["driveFilePath"] + ".exe";
|
||||
}
|
||||
|
||||
// 禁止.txt文件上传
|
||||
if (params["localFilePath"].lastIndexOf(".txt") > 0) {
|
||||
result["uploadApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止password.key文件上传
|
||||
if (params["localFileName"] == "password.key") {
|
||||
result["uploadApproved"] = "no";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:上传文件结束的回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 文件上传结束参数
|
||||
// {
|
||||
// "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
|
||||
// "localFileName": "token.bat",
|
||||
// "localFileSize": 125330,
|
||||
// "localFileType": "file",
|
||||
// "localFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "localFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "uploadResult": "success",
|
||||
// "driveId": "19519221",
|
||||
// "driveFilePath": "/tmp/test/aliyunpan/Downloads/token.bat"
|
||||
// }
|
||||
// localFilePath - 本地文件绝对完整路径
|
||||
// localFileName - 本地文件名
|
||||
// localFileSize - 本地文件大小,单位B
|
||||
// localFileType - 本地文件类型,file-文件,folder-文件夹
|
||||
// localFileUpdatedAt - 文件修改时间
|
||||
// localFileSha1 - 本地文件的SHA1。这个值不一定会有
|
||||
// uploadResult - 上传结果,success-成功,fail-失败
|
||||
// driveId - 目标网盘ID
|
||||
// driveFilePath - 文件网盘保存的绝对路径
|
||||
//
|
||||
// 返回值说明
|
||||
// (没有返回值)
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function uploadFileFinishCallback(context, params) {
|
||||
console.log(params)
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
|
||||
REM 配置环境变量
|
||||
REM set ALIYUNPAN_CONFIG_DIR=d:\path\to\your\aliyunpan\config
|
||||
|
||||
REM 指定refresh token用于登录
|
||||
aliyunpan login -RefreshToken=9078907....adg9087
|
||||
|
||||
REM 上传下载链接类型:1-默认 2-阿里ECS环境
|
||||
aliyunpan config set -transfer_url_type 1
|
||||
|
||||
REM 指定webdav启动参数并进行启动。你可以按照自己的需求更改以下参数,例如用户名和密码
|
||||
aliyunpan webdav start -ip "0.0.0.0" -port 23077 -webdav_user "admin" -webdav_password "admin" -pan_dir_path "/" -bs 1024
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 请更改成你自己电脑上aliyunpan执行文件所在的目录
|
||||
#cd /path/to/aliyunpan/folder
|
||||
|
||||
# 配置环境变量
|
||||
#export ALIYUNPAN_CONFIG_DIR=/path/to/your/aliyunpan/config
|
||||
|
||||
chmod +x ./aliyunpan
|
||||
|
||||
# 指定refresh token用于登录
|
||||
./aliyunpan login -RefreshToken=9078907....adg9087
|
||||
|
||||
# 上传下载链接类型:1-默认 2-阿里ECS环境
|
||||
./aliyunpan config set -transfer_url_type 1
|
||||
|
||||
# 指定webdav启动参数并进行启动。你可以按照自己的需求更改以下参数,例如用户名、密码和网盘目录
|
||||
./aliyunpan webdav start -ip "0.0.0.0" -port 23077 -webdav_user "admin" -webdav_password "admin" -pan_dir_path "/" -bs 1024
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# linux service 配置文件模版
|
||||
# centos系统默认存放路径为:/lib/systemd/system/aliyunpansync.service
|
||||
# 启动服务:systemctl start aliyunpansync
|
||||
# 停止服务:systemctl stop aliyunpansync
|
||||
|
||||
[Unit]
|
||||
Description=aliyunpansync
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
ExecStart=/path/to/start.sh
|
||||
ExecStop=/path/to/stop.sh
|
||||
PrivateTmp=true
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 更改路径为你电脑sync.sh文件所在文件夹路径
|
||||
cd /path/to/your/sync.sh/folder
|
||||
nohup ./sync.sh >/dev/null 2>&1 &
|
||||
@@ -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
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"configVer": "1.0",
|
||||
"syncTaskList": [
|
||||
{
|
||||
"name": "设计文档备份",
|
||||
"localFolderPath": "D:\\tickstep\\Documents\\设计文档",
|
||||
"panFolderPath": "/sync_drive/我的文档",
|
||||
"mode": "upload",
|
||||
"priority": "local"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
#!/bin/sh
|
||||
|
||||
# how to use
|
||||
# for macOS & linux, run this command in shell
|
||||
# ./build.sh v0.1.0
|
||||
|
||||
name="aliyunpan"
|
||||
version=$1
|
||||
|
||||
if [ "$1" = "" ]; then
|
||||
version=v1.0.0
|
||||
fi
|
||||
|
||||
output="out"
|
||||
|
||||
build_dir=`dirname $0`
|
||||
if [ ! -d ${build_dir}/${output} ];then
|
||||
mkdir -p ${build_dir}/${output}
|
||||
fi
|
||||
|
||||
default_golang() {
|
||||
export GOROOT=/usr/local/go
|
||||
go=$GOROOT/bin/go
|
||||
}
|
||||
|
||||
Build() {
|
||||
default_golang
|
||||
goarm=$4
|
||||
if [ "$4" = "" ]; then
|
||||
goarm=7
|
||||
fi
|
||||
|
||||
echo "Building $1..."
|
||||
export GOOS=$2 GOARCH=$3 GO386=sse2 CGO_ENABLED=0 GOARM=$4
|
||||
if [ $2 = "windows" ]; then
|
||||
goversioninfo -o=resource_windows_386.syso
|
||||
goversioninfo -64 -o=resource_windows_amd64.syso
|
||||
$go build -ldflags "-X main.Version=$version -s -w" -o "$output/$1/$name.exe"
|
||||
RicePack $1 $name.exe
|
||||
else
|
||||
$go build -ldflags "-X main.Version=$version -s -w" -o "$output/$1/$name"
|
||||
RicePack $1 $name
|
||||
fi
|
||||
|
||||
Pack $1 $2
|
||||
}
|
||||
|
||||
AndroidBuild() {
|
||||
default_golang
|
||||
echo "Building $1..."
|
||||
export GOOS=$2 GOARCH=$3 GOARM=$4 CGO_ENABLED=1
|
||||
$go build -ldflags "-X main.Version=$version -s -w -linkmode=external -extldflags=-pie" -o "$output/$1/$name"
|
||||
|
||||
RicePack $1 $name
|
||||
Pack $1 $2
|
||||
}
|
||||
|
||||
IOSBuild() {
|
||||
default_golang
|
||||
echo "Building $1..."
|
||||
mkdir -p "$output/$1"
|
||||
cd "$output/$1"
|
||||
export CC=/usr/local/go/misc/ios/clangwrap.sh GOOS=ios GOARCH=arm64 GOARM=7 CGO_ENABLED=1
|
||||
$go build -ldflags "-X main.Version=$version -s -w" -o $name github.com/tickstep/aliyunpan
|
||||
jtool2 --sign --inplace --ent ../../entitlements.xml $name
|
||||
cd ../..
|
||||
RicePack $1 $name
|
||||
Pack $1 "ios"
|
||||
}
|
||||
|
||||
# zip 打包
|
||||
Pack() {
|
||||
if [ $2 != "windows" ]; then
|
||||
chmod +x "$output/$1/$name"
|
||||
fi
|
||||
|
||||
cp README.md "$output/$1"
|
||||
cp docs/manual.md "$output/$1"
|
||||
cp docs/plugin_manual.md "$output/$1"
|
||||
cp -rf assets/scripts/* "$output/$1"
|
||||
cp -rf assets/plugin "$output/$1"
|
||||
cp -rf assets/sync_drive "$output/$1"
|
||||
|
||||
cd $output
|
||||
zip -q -r "$1.zip" "$1"
|
||||
|
||||
# 删除
|
||||
rm -rf "$1"
|
||||
|
||||
cd ..
|
||||
}
|
||||
|
||||
# rice 打包静态资源
|
||||
RicePack() {
|
||||
return # 已取消web功能
|
||||
}
|
||||
|
||||
############### Android ###############
|
||||
export ANDROID_NDK_ROOT=/Users/tickstep/Applications/android_ndk/android-ndk-r23-darwin
|
||||
CC=$ANDROID_NDK_ROOT/bin/arm-linux-androideabi/bin/clang AndroidBuild $name-$version"-android-api16-armv7" android arm 7
|
||||
CC=$ANDROID_NDK_ROOT/bin/aarch64-linux-android/bin/clang AndroidBuild $name-$version"-android-api21-arm64" android arm64 7
|
||||
CC=$ANDROID_NDK_ROOT/bin/i686-linux-android/bin/clang AndroidBuild $name-$version"-android-api16-386" android 386 7
|
||||
CC=$ANDROID_NDK_ROOT/bin/x86_64-linux-android/bin/clang AndroidBuild $name-$version"-android-api21-amd64" android amd64 7
|
||||
|
||||
############### iOS ###############
|
||||
IOSBuild $name-$version"-ios-arm64"
|
||||
|
||||
############### macOS ###############
|
||||
Build $name-$version"-darwin-macos-amd64" darwin amd64
|
||||
# Build $name-$version"-darwin-macos-386" darwin 386
|
||||
Build $name-$version"-darwin-macos-arm64" darwin arm64
|
||||
|
||||
############### Windows ###############
|
||||
Build $name-$version"-windows-x86" windows 386
|
||||
Build $name-$version"-windows-x64" windows amd64
|
||||
Build $name-$version"-windows-arm64" windows arm64
|
||||
|
||||
############### Linux ###############
|
||||
# x64/x86
|
||||
Build $name-$version"-linux-386" linux 386
|
||||
Build $name-$version"-linux-amd64" linux amd64
|
||||
|
||||
# 龙芯 LoongArch
|
||||
Build $name-$version"-linux-loong64" linux loong64
|
||||
|
||||
# arm
|
||||
Build $name-$version"-linux-armv5" linux arm 5
|
||||
Build $name-$version"-linux-armv7" linux arm 7
|
||||
Build $name-$version"-linux-arm64" linux arm64
|
||||
|
||||
# mips
|
||||
GOMIPS=softfloat Build $name-$version"-linux-mips64" linux mips64
|
||||
GOMIPS=softfloat Build $name-$version"-linux-mips64le" linux mips64le
|
||||
GOMIPS=hardfloat Build $name-$version"-linux-mips64hf" linux mips64
|
||||
GOMIPS=hardfloat Build $name-$version"-linux-mips64lehf" linux mips64le
|
||||
GOMIPS=softfloat Build $name-$version"-linux-mips" linux mips
|
||||
GOMIPS=softfloat Build $name-$version"-linux-mipsle" linux mipsle
|
||||
GOMIPS=hardfloat Build $name-$version"-linux-mipshf" linux mips
|
||||
GOMIPS=hardfloat Build $name-$version"-linux-mipslehf" linux mipsle
|
||||
|
||||
# freebsd
|
||||
Build $name-$version"-freebsd-386" freebsd 386
|
||||
Build $name-$version"-freebsd-amd64" freebsd amd64
|
||||
# Build $name-$version"-freebsd-arm" freebsd arm
|
||||
|
||||
# Others
|
||||
# Build $name-$version"-linux-ppc64" linux ppc64
|
||||
# Build $name-$version"-linux-ppc64le" linux ppc64le
|
||||
# Build $name-$version"-linux-s390x" linux s390x
|
||||
# Build $name-$version"-solaris-amd64" solaris amd64
|
||||
# Build $name-$version"-netbsd-386" netbsd 386
|
||||
# Build $name-$version"-netbsd-amd64" netbsd amd64
|
||||
# Build $name-$version"-netbsd-arm" netbsd arm
|
||||
# Build $name-$version"-openbsd-386" openbsd 386
|
||||
# Build $name-$version"-openbsd-amd64" openbsd amd64
|
||||
# Build $name-$version"-openbsd-arm" openbsd arm
|
||||
# Build $name-$version"-plan9-386" plan9 386
|
||||
# Build $name-$version"-plan9-amd64" plan9 amd64
|
||||
# Build $name-$version"-plan9-arm" plan9 arm
|
||||
# Build $name-$version"-nacl-386" nacl 386
|
||||
# Build $name-$version"-nacl-amd64p32" nacl amd64p32
|
||||
# Build $name-$version"-nacl-arm" nacl arm
|
||||
# Build $name-$version"-dragonflybsd-amd64" dragonfly amd64
|
||||
@@ -0,0 +1,17 @@
|
||||
package cmder
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
appInstance *cli.App
|
||||
)
|
||||
|
||||
func SetApp(app *cli.App) {
|
||||
appInstance = app
|
||||
}
|
||||
|
||||
func App() *cli.App {
|
||||
return appInstance
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package args
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
CharEscape = '\\'
|
||||
CharSingleQuote = '\''
|
||||
CharDoubleQuote = '"'
|
||||
CharBackQuote = '`'
|
||||
)
|
||||
|
||||
// IsQuote 是否为引号
|
||||
func IsQuote(r rune) bool {
|
||||
return r == CharSingleQuote || r == CharDoubleQuote || r == CharBackQuote
|
||||
}
|
||||
|
||||
// Parse 解析line, 忽略括号
|
||||
func Parse(line string) (lineArgs []string) { // 在函数中定义的返回值变量,会自动赋为 zero-value,即相当于 var lineArgs string[]
|
||||
var (
|
||||
rl = []rune(line + " ")
|
||||
buf = strings.Builder{}
|
||||
quoteChar rune
|
||||
nextChar rune
|
||||
escaped bool
|
||||
in bool
|
||||
)
|
||||
|
||||
var (
|
||||
isSpace bool
|
||||
)
|
||||
|
||||
for k, r := range rl {
|
||||
isSpace = unicode.IsSpace(r)
|
||||
if !isSpace && !in {
|
||||
in = true
|
||||
}
|
||||
|
||||
switch {
|
||||
case escaped: // 已转义, 跳过
|
||||
escaped = false
|
||||
//pass
|
||||
case r == CharEscape: // 转义模式
|
||||
if k+1+1 < len(rl) { // 不是最后一个字符, 多+1是因为最后一个空格
|
||||
nextChar = rl[k+1]
|
||||
// 仅支持转义这些字符, 否则原样输出反斜杠
|
||||
if unicode.IsSpace(nextChar) || IsQuote(nextChar) || nextChar == CharEscape {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
// pass
|
||||
case IsQuote(r):
|
||||
if quoteChar == 0 { //未引
|
||||
quoteChar = r
|
||||
continue
|
||||
}
|
||||
|
||||
if quoteChar == r { //取消引
|
||||
quoteChar = 0
|
||||
continue
|
||||
}
|
||||
case isSpace:
|
||||
if !in { // 忽略多余的空格
|
||||
continue
|
||||
}
|
||||
if quoteChar == 0 { // 未在引号内
|
||||
lineArgs = append(lineArgs, buf.String())
|
||||
buf.Reset()
|
||||
in = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteRune(r)
|
||||
}
|
||||
|
||||
// Go 允许在定义函数时,命名返回值,当然这些变量可以在函数中使用。
|
||||
// 在 return 语句中,无需显示的返回这些值,Go 会自动将其返回。当然 return 语句还是必须要写的,否则编译器会报错。
|
||||
// 相当于 return lineArgs
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// +build !windows
|
||||
|
||||
package cmdliner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ClearScreen 清空屏幕
|
||||
func (pl *CmdLiner) ClearScreen() {
|
||||
ClearScreen()
|
||||
}
|
||||
|
||||
// ClearScreen 清空屏幕
|
||||
func ClearScreen() {
|
||||
fmt.Print("\x1b[H\x1b[2J")
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package cmdliner
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
std_output_handle = uint32(-11 & 0xFFFFFFFF)
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
|
||||
procGetStdHandle = kernel32.NewProc("GetStdHandle")
|
||||
procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
|
||||
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
|
||||
procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
|
||||
)
|
||||
|
||||
type (
|
||||
coord struct {
|
||||
x, y int16
|
||||
}
|
||||
smallRect struct {
|
||||
left, top, right, bottom int16
|
||||
}
|
||||
consoleScreenBufferInfo struct {
|
||||
dwSize coord
|
||||
dwCursorPosition coord
|
||||
wAttributes int16
|
||||
srWindow smallRect
|
||||
dwMaximumWindowSize coord
|
||||
}
|
||||
)
|
||||
|
||||
// ClearScreen 清空屏幕
|
||||
func (pl *CmdLiner) ClearScreen() {
|
||||
ClearScreen()
|
||||
}
|
||||
|
||||
// ClearScreen 清空屏幕
|
||||
func ClearScreen() {
|
||||
out, _, _ := procGetStdHandle.Call(uintptr(std_output_handle))
|
||||
hOut := syscall.Handle(out)
|
||||
|
||||
var sbi consoleScreenBufferInfo
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(hOut), uintptr(unsafe.Pointer(&sbi)))
|
||||
|
||||
var numWritten uint32
|
||||
procFillConsoleOutputCharacter.Call(uintptr(hOut), uintptr(' '),
|
||||
uintptr(sbi.dwSize.x)*uintptr(sbi.dwSize.y),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&numWritten)))
|
||||
procSetConsoleCursorPosition.Call(uintptr(hOut), 0)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package cmdliner
|
||||
|
||||
import (
|
||||
"github.com/peterh/liner"
|
||||
)
|
||||
|
||||
// CmdLiner 封装 *liner.State, 提供更简便的操作
|
||||
type CmdLiner struct {
|
||||
State *liner.State
|
||||
History *LineHistory
|
||||
|
||||
tmode liner.ModeApplier
|
||||
lmode liner.ModeApplier
|
||||
|
||||
paused bool
|
||||
}
|
||||
|
||||
// NewLiner 返回 *CmdLiner, 默认设置允许 Ctrl+C 结束
|
||||
func NewLiner() *CmdLiner {
|
||||
pl := &CmdLiner{}
|
||||
pl.tmode, _ = liner.TerminalMode()
|
||||
|
||||
line := liner.NewLiner()
|
||||
pl.lmode, _ = liner.TerminalMode()
|
||||
|
||||
line.SetMultiLineMode(true)
|
||||
line.SetCtrlCAborts(true)
|
||||
|
||||
pl.State = line
|
||||
|
||||
return pl
|
||||
}
|
||||
|
||||
// Pause 暂停服务
|
||||
func (pl *CmdLiner) Pause() error {
|
||||
if pl.paused {
|
||||
panic("CmdLiner already paused")
|
||||
}
|
||||
|
||||
pl.paused = true
|
||||
pl.DoWriteHistory()
|
||||
|
||||
return pl.tmode.ApplyMode()
|
||||
}
|
||||
|
||||
// Resume 恢复服务
|
||||
func (pl *CmdLiner) Resume() error {
|
||||
if !pl.paused {
|
||||
panic("CmdLiner is not paused")
|
||||
}
|
||||
|
||||
pl.paused = false
|
||||
|
||||
return pl.lmode.ApplyMode()
|
||||
}
|
||||
|
||||
// Close 关闭服务
|
||||
func (pl *CmdLiner) Close() (err error) {
|
||||
err = pl.State.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pl.History != nil && pl.History.historyFile != nil {
|
||||
return pl.History.historyFile.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package cmdliner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// LineHistory 命令行历史
|
||||
type LineHistory struct {
|
||||
historyFilePath string
|
||||
historyFile *os.File
|
||||
}
|
||||
|
||||
// NewLineHistory 设置历史
|
||||
func NewLineHistory(filePath string) (lh *LineHistory, err error) {
|
||||
lh = &LineHistory{
|
||||
historyFilePath: filePath,
|
||||
}
|
||||
|
||||
lh.historyFile, err = os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return lh, nil
|
||||
}
|
||||
|
||||
// DoWriteHistory 执行写入历史
|
||||
func (pl *CmdLiner) DoWriteHistory() (err error) {
|
||||
if pl.History == nil {
|
||||
return fmt.Errorf("history not set")
|
||||
}
|
||||
|
||||
pl.History.historyFile, err = os.Create(pl.History.historyFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入历史错误, %s", err)
|
||||
}
|
||||
|
||||
_, err = pl.State.WriteHistory(pl.History.historyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入历史错误: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadHistory 读取历史
|
||||
func (pl *CmdLiner) ReadHistory() (err error) {
|
||||
if pl.History == nil {
|
||||
return fmt.Errorf("history not set")
|
||||
}
|
||||
|
||||
_, err = pl.State.ReadHistory(pl.History.historyFile)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package cmdtable
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"io"
|
||||
)
|
||||
|
||||
type CmdTable struct {
|
||||
*tablewriter.Table
|
||||
}
|
||||
|
||||
// NewTable 预设了一些配置
|
||||
func NewTable(wt io.Writer) CmdTable {
|
||||
tb := tablewriter.NewWriter(wt)
|
||||
tb.SetAutoWrapText(false)
|
||||
tb.SetBorder(false)
|
||||
tb.SetHeaderLine(false)
|
||||
tb.SetColumnSeparator("")
|
||||
return CmdTable{tb}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package cmdutil
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// ListAddresses 列出本地可用的 IP 地址
|
||||
func ListAddresses() (addresses []string) {
|
||||
iFaces, _ := net.Interfaces()
|
||||
addresses = make([]string, 0, len(iFaces))
|
||||
for k := range iFaces {
|
||||
iFaceAddrs, _ := iFaces[k].Addrs()
|
||||
for l := range iFaceAddrs {
|
||||
switch v := iFaceAddrs[l].(type) {
|
||||
case *net.IPNet:
|
||||
addresses = append(addresses, v.IP.String())
|
||||
case *net.IPAddr:
|
||||
addresses = append(addresses, v.IP.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ParseHost 解析地址中的host
|
||||
func ParseHost(address string) string {
|
||||
h, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return address
|
||||
}
|
||||
return h
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package cmdutil
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"flag"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TrimPathPrefix 去除目录的前缀
|
||||
func TrimPathPrefix(path, prefixPath string) string {
|
||||
if prefixPath == "/" {
|
||||
return path
|
||||
}
|
||||
return strings.TrimPrefix(path, prefixPath)
|
||||
}
|
||||
|
||||
// ContainsString 检测字符串是否在字符串数组里
|
||||
func ContainsString(ss []string, s string) bool {
|
||||
for k := range ss {
|
||||
if ss[k] == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetURLCookieString 返回cookie字串
|
||||
func GetURLCookieString(urlString string, jar *cookiejar.Jar) string {
|
||||
u, _ := url.Parse(urlString)
|
||||
cookies := jar.Cookies(u)
|
||||
cookieString := ""
|
||||
for _, v := range cookies {
|
||||
cookieString += v.String() + "; "
|
||||
}
|
||||
cookieString = strings.TrimRight(cookieString, "; ")
|
||||
return cookieString
|
||||
}
|
||||
|
||||
// DecompressGZIP 对 io.Reader 数据, 进行 gzip 解压
|
||||
func DecompressGZIP(r io.Reader) ([]byte, error) {
|
||||
gzipReader, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gzipReader.Close()
|
||||
return ioutil.ReadAll(gzipReader)
|
||||
}
|
||||
|
||||
// FlagProvided 检测命令行是否提供名为 name 的 flag, 支持多个name(names)
|
||||
func FlagProvided(names ...string) bool {
|
||||
if len(names) == 0 {
|
||||
return false
|
||||
}
|
||||
var targetFlag *flag.Flag
|
||||
for _, name := range names {
|
||||
targetFlag = flag.Lookup(name)
|
||||
if targetFlag == nil {
|
||||
return false
|
||||
}
|
||||
if targetFlag.DefValue == targetFlag.Value.String() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Trigger 用于触发事件
|
||||
func Trigger(f func()) {
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
go f()
|
||||
}
|
||||
|
||||
// TriggerOnSync 用于触发事件, 同步触发
|
||||
func TriggerOnSync(f func()) {
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
f()
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package escaper
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
// RuneFunc 判断指定rune
|
||||
RuneFunc func(r rune) bool
|
||||
)
|
||||
|
||||
// EscapeByRuneFunc 通过runeFunc转义, runeFunc返回真, 则转义
|
||||
func EscapeByRuneFunc(s string, runeFunc RuneFunc) string {
|
||||
if runeFunc == nil {
|
||||
return s
|
||||
}
|
||||
|
||||
var (
|
||||
builder = &strings.Builder{}
|
||||
rs = []rune(s)
|
||||
)
|
||||
|
||||
for k := range rs {
|
||||
if !runeFunc(rs[k]) {
|
||||
builder.WriteRune(rs[k])
|
||||
continue
|
||||
}
|
||||
|
||||
if k >= 1 && rs[k-1] == '\\' {
|
||||
builder.WriteRune(rs[k])
|
||||
continue
|
||||
}
|
||||
builder.WriteString(`\`)
|
||||
builder.WriteRune(rs[k])
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// Escape 转义指定的escapeRunes, 在escapeRunes的前面加上一个反斜杠
|
||||
func Escape(s string, escapeRunes []rune) string {
|
||||
return EscapeByRuneFunc(s, func(r rune) bool {
|
||||
for k := range escapeRunes {
|
||||
if escapeRunes[k] == r {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
// EscapeStrings 转义字符串数组
|
||||
func EscapeStrings(ss []string, escapeRunes []rune) {
|
||||
for k := range ss {
|
||||
ss[k] = Escape(ss[k], escapeRunes)
|
||||
}
|
||||
}
|
||||
|
||||
// EscapeStringsByRuneFunc 转义字符串数组, 通过runeFunc
|
||||
func EscapeStringsByRuneFunc(ss []string, runeFunc RuneFunc) {
|
||||
for k := range ss {
|
||||
ss[k] = EscapeByRuneFunc(ss[k], runeFunc)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package cmdutil
|
||||
|
||||
import (
|
||||
"github.com/kardianos/osext"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func IsPipeInput() bool {
|
||||
fileInfo, err := os.Stdin.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (fileInfo.Mode() & os.ModeNamedPipe) == os.ModeNamedPipe
|
||||
}
|
||||
|
||||
// IsIPhoneOS 是否为苹果移动设备
|
||||
func IsIPhoneOS() bool {
|
||||
if runtime.GOOS == "darwin" && (runtime.GOARCH == "arm" || runtime.GOARCH == "arm64") {
|
||||
_, err := os.Stat("Info.plist")
|
||||
return err == nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ChWorkDir 切换回工作目录
|
||||
func ChWorkDir() {
|
||||
if !IsIPhoneOS() {
|
||||
return
|
||||
}
|
||||
|
||||
dir, err := filepath.Abs("")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
subPath := filepath.Dir(os.Args[0])
|
||||
os.Chdir(strings.TrimSuffix(dir, subPath))
|
||||
}
|
||||
|
||||
// Executable 获取程序所在的真实目录或真实相对路径
|
||||
func Executable() string {
|
||||
executablePath, err := osext.Executable()
|
||||
if err != nil {
|
||||
logger.Verbosef("DEBUG: osext.Executable: %s\n", err)
|
||||
executablePath, err = filepath.Abs(filepath.Dir(os.Args[0]))
|
||||
if err != nil {
|
||||
logger.Verbosef("DEBUG: filepath.Abs: %s\n", err)
|
||||
executablePath = filepath.Dir(os.Args[0])
|
||||
}
|
||||
}
|
||||
|
||||
if IsIPhoneOS() {
|
||||
executablePath = filepath.Join(strings.TrimSuffix(executablePath, os.Args[0]), filepath.Base(os.Args[0]))
|
||||
}
|
||||
|
||||
// 读取链接
|
||||
linkedExecutablePath, err := filepath.EvalSymlinks(executablePath)
|
||||
if err != nil {
|
||||
logger.Verbosef("DEBUG: filepath.EvalSymlinks: %s\n", err)
|
||||
return executablePath
|
||||
}
|
||||
return linkedExecutablePath
|
||||
}
|
||||
|
||||
// ExecutablePath 获取程序所在目录
|
||||
func ExecutablePath() string {
|
||||
return filepath.Dir(Executable())
|
||||
}
|
||||
|
||||
// ExecutablePathJoin 返回程序所在目录的子目录
|
||||
func ExecutablePathJoin(subPath string) string {
|
||||
return filepath.Join(ExecutablePath(), subPath)
|
||||
}
|
||||
|
||||
// WalkDir 获取指定目录及所有子目录下的所有文件,可以匹配后缀过滤。
|
||||
// 支持 Linux/macOS 软链接
|
||||
func WalkDir(dirPth, suffix string) (files []string, err error) {
|
||||
files = make([]string, 0, 32)
|
||||
suffix = strings.ToUpper(suffix) //忽略后缀匹配的大小写
|
||||
|
||||
var walkFunc filepath.WalkFunc
|
||||
walkFunc = func(filename string, fi os.FileInfo, err error) error { //遍历目录
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.IsDir() { // 忽略目录
|
||||
return nil
|
||||
}
|
||||
if fi.Mode()&os.ModeSymlink != 0 { // 读取 symbol link
|
||||
err = filepath.Walk(filename+string(os.PathSeparator), walkFunc)
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.HasSuffix(strings.ToUpper(fi.Name()), suffix) {
|
||||
files = append(files, path.Clean(filename))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err = filepath.Walk(dirPth, walkFunc)
|
||||
return files, err
|
||||
}
|
||||
|
||||
// ConvertToUnixPathSeparator 将 windows 目录分隔符转换为 Unix 的
|
||||
func ConvertToUnixPathSeparator(p string) string {
|
||||
return strings.Replace(p, "\\", "/", -1)
|
||||
}
|
||||
|
||||
// ConvertToWindowsPathSeparator 将路径中的所有分隔符转换成windows格式
|
||||
func ConvertToWindowsPathSeparator(p string) string {
|
||||
return strings.Replace(p, "/", "\\", -1)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package jsonhelper
|
||||
|
||||
import (
|
||||
"github.com/json-iterator/go"
|
||||
"io"
|
||||
)
|
||||
|
||||
// UnmarshalData 将 r 中的 json 格式的数据, 解析到 data
|
||||
func UnmarshalData(r io.Reader, data interface{}) error {
|
||||
d := jsoniter.NewDecoder(r)
|
||||
return d.Decode(data)
|
||||
}
|
||||
|
||||
// MarshalData 将 data, 生成 json 格式的数据, 写入 w 中
|
||||
func MarshalData(w io.Writer, data interface{}) error {
|
||||
e := jsoniter.NewEncoder(w)
|
||||
return e.Encode(data)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
# alpine:3.21
|
||||
|
||||
# 参数
|
||||
ARG DOCKER_IMAGE_HASH
|
||||
|
||||
#FROM alpine@sha256:$DOCKER_IMAGE_HASH
|
||||
#FROM alpine
|
||||
FROM alpine:$DOCKER_IMAGE_HASH
|
||||
|
||||
LABEL author="tickstep"
|
||||
LABEL email="tickstep@outlook.com"
|
||||
LABEL version="1.0"
|
||||
LABEL description="sync & backup service for aliyun cloud drive"
|
||||
|
||||
# 时区
|
||||
ENV TZ=Asia/Shanghai
|
||||
# 手动下载tzdata安装包,注意要下载对应架构的: https://dl-cdn.alpinelinux.org/alpine/v3.15/community/
|
||||
RUN apk add -U tzdata
|
||||
# 更新Alpine的软件源为国内镜像站点,提高下载速度。安装tzdata包
|
||||
# RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
# RUN apk add --no-cache tzdata
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
# 创建运行目录
|
||||
RUN mkdir -p /home/app
|
||||
VOLUME /home/app
|
||||
WORKDIR /home/app
|
||||
RUN cd /home/app
|
||||
|
||||
# 创建配置文件目录
|
||||
RUN mkdir -p /home/app/config
|
||||
|
||||
# 创建数据文件夹
|
||||
RUN mkdir -p /home/app/data
|
||||
RUN chmod 777 /home/app/data
|
||||
|
||||
# 复制文件
|
||||
COPY ./docker/sync/app.sh /home/app/app.sh
|
||||
RUN chmod +x /home/app/app.sh
|
||||
COPY ./docker/sync/health_check.sh /home/app/health_check.sh
|
||||
RUN chmod +x /home/app/health_check.sh
|
||||
|
||||
COPY ./out/binary_files/aliyunpan /home/app
|
||||
RUN mkdir -p /home/app/config/plugin
|
||||
COPY ./out/binary_files/plugin /home/app/config/plugin
|
||||
RUN mkdir -p /home/app/config/sync_drive
|
||||
COPY ./out/binary_files/sync_drive /home/app/config/sync_drive
|
||||
#RUN chmod +x /home/app/aliyunpan
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --start-period=5s --interval=10s --timeout=5s --retries=3 CMD /bin/sh /home/app/health_check.sh
|
||||
|
||||
# 环境变量
|
||||
ENV ALIYUNPAN_DOCKER=1
|
||||
ENV ALIYUNPAN_CONFIG_DIR=/home/app/config
|
||||
|
||||
ENV ALIYUNPAN_DOWNLOAD_PARALLEL=2
|
||||
ENV ALIYUNPAN_UPLOAD_PARALLEL=2
|
||||
ENV ALIYUNPAN_DOWNLOAD_BLOCK_SIZE=1024
|
||||
ENV ALIYUNPAN_UPLOAD_BLOCK_SIZE=10240
|
||||
ENV ALIYUNPAN_LOCAL_DIR=/home/app/data
|
||||
ENV ALIYUNPAN_PAN_DIR=/sync_drive
|
||||
ENV ALIYUNPAN_SYNC_MODE=upload
|
||||
ENV ALIYUNPAN_SYNC_POLICY=increment
|
||||
ENV ALIYUNPAN_SYNC_DRIVE=backup
|
||||
ENV ALIYUNPAN_SYNC_CYCLE=infinity
|
||||
ENV ALIYUNPAN_SYNC_PRIORITY=time
|
||||
ENV ALIYUNPAN_SYNC_LOG=false
|
||||
ENV ALIYUNPAN_LOCAL_DELAY_TIME=3
|
||||
ENV ALIYUNPAN_SCAN_INTERVAL_TIME=1
|
||||
ENV ALIYUNPAN_DEVICE_ID=""
|
||||
ENV ALIYUNPAN_IP_TYPE="ipv4"
|
||||
|
||||
# 运行
|
||||
ENTRYPOINT ./app.sh
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/bin/sh
|
||||
cd /home/app
|
||||
chmod +x ./aliyunpan
|
||||
|
||||
# sync config file
|
||||
readonly sync_drive_config_file="$ALIYUNPAN_CONFIG_DIR/sync_drive/sync_drive_config.json"
|
||||
if test -s $sync_drive_config_file
|
||||
then
|
||||
echo "using existed sync_drive_config.json file"
|
||||
else
|
||||
echo "generate sync_drive_config.json file"
|
||||
tee $sync_drive_config_file << EOF
|
||||
{
|
||||
"configVer": "1.0",
|
||||
"syncTaskList": [
|
||||
{
|
||||
"name": "阿里云盘备份",
|
||||
"id": "5b2d7c10-e927-4e72-8f9d-5abb3bb04815",
|
||||
"localFolderPath": "$ALIYUNPAN_LOCAL_DIR",
|
||||
"panFolderPath": "$ALIYUNPAN_PAN_DIR",
|
||||
"mode": "$ALIYUNPAN_SYNC_MODE",
|
||||
"policy": "$ALIYUNPAN_SYNC_POLICY",
|
||||
"driveName": "$ALIYUNPAN_SYNC_DRIVE",
|
||||
"lastSyncTime": "2022-06-12 19:28:20"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
sleep 2s
|
||||
|
||||
# device-id
|
||||
#if [[ -z $ALIYUNPAN_DEVICE_ID ]];
|
||||
#then
|
||||
# echo "the program use random device id"
|
||||
#else
|
||||
# echo "set device id"
|
||||
# ./aliyunpan config set -device_id ${ALIYUNPAN_DEVICE_ID}
|
||||
#fi
|
||||
|
||||
# show docker IPs
|
||||
./aliyunpan tool getip
|
||||
|
||||
# check login already or not
|
||||
./aliyunpan who
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
echo "cache token is valid, not need to re-login"
|
||||
else
|
||||
echo "token is invalid, please use the valid aliyunpan_config.json file and retry"
|
||||
# ./aliyunpan login -RefreshToken=${ALIYUNPAN_REFRESH_TOKEN}
|
||||
fi
|
||||
|
||||
if [ "$ALIYUNPAN_SYNC_LOG" = "true" ]
|
||||
then
|
||||
./aliyunpan config set -file_record_config 1
|
||||
else
|
||||
./aliyunpan config set -file_record_config 2
|
||||
fi
|
||||
|
||||
# IP type
|
||||
./aliyunpan config set -ip_type ${ALIYUNPAN_IP_TYPE}
|
||||
|
||||
# run
|
||||
./aliyunpan sync start -dp ${ALIYUNPAN_DOWNLOAD_PARALLEL} -up ${ALIYUNPAN_UPLOAD_PARALLEL} -dbs ${ALIYUNPAN_DOWNLOAD_BLOCK_SIZE} -ubs ${ALIYUNPAN_UPLOAD_BLOCK_SIZE} -log ${ALIYUNPAN_SYNC_LOG} -ldt ${ALIYUNPAN_LOCAL_DELAY_TIME} -cycle ${ALIYUNPAN_SYNC_CYCLE} -sit ${ALIYUNPAN_SCAN_INTERVAL_TIME}
|
||||
@@ -0,0 +1,43 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
sync:
|
||||
image: tickstep/aliyunpan-sync:v0.1.6
|
||||
container_name: aliyunpan-sync
|
||||
restart: always
|
||||
volumes:
|
||||
# (必须)映射的本地目录
|
||||
- ./data:/home/app/data:rw
|
||||
# (可选)可以指定JS插件sync_handler.js用于过滤文件,详见插件说明
|
||||
#- ./plugin/js/sync_handler.js:/home/app/config/plugin/js/sync_handler.js
|
||||
# (推荐)挂载sync_drive同步数据库到本地,这样即使容器销毁,同步数据库还可以用于以后使用
|
||||
#- ./sync_drive:/home/app/config/sync_drive
|
||||
# (必须)映射token凭据文件
|
||||
- /your/file/path/for/aliyunpan_config.json:/home/app/config/aliyunpan_config.json
|
||||
environment:
|
||||
# 时区,东8区
|
||||
- TZ=Asia/Shanghai
|
||||
# 下载文件并发数
|
||||
- ALIYUNPAN_DOWNLOAD_PARALLEL=2
|
||||
# 上传文件并发数
|
||||
- ALIYUNPAN_UPLOAD_PARALLEL=2
|
||||
# 下载数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
|
||||
- ALIYUNPAN_DOWNLOAD_BLOCK_SIZE=1024
|
||||
# 上传数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
|
||||
- ALIYUNPAN_UPLOAD_BLOCK_SIZE=10240
|
||||
# 指定网盘文件夹作为备份目标目录,不要指定根目录
|
||||
- ALIYUNPAN_PAN_DIR=/my_sync_dir
|
||||
# 备份模式:upload(备份本地文件到云盘), download(备份云盘文件到本地)
|
||||
- ALIYUNPAN_SYNC_MODE=upload
|
||||
# 备份策略: exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)
|
||||
- ALIYUNPAN_SYNC_POLICY=increment
|
||||
# 备份周期, 支持两种: infinity(永久循环备份),onetime(只运行一次备份)
|
||||
- ALIYUNPAN_SYNC_CYCLE=infinity
|
||||
# 网盘:backup(备份盘), resource(资源盘)
|
||||
- ALIYUNPAN_SYNC_DRIVE=backup
|
||||
# 是否显示文件备份过程日志,true-显示,false-不显示
|
||||
- ALIYUNPAN_SYNC_LOG=true
|
||||
# 本地文件修改检测延迟间隔,单位秒。如果本地文件会被频繁修改,例如录制视频文件,配置好该时间可以避免上传未录制好的文件
|
||||
- ALIYUNPAN_LOCAL_DELAY_TIME=3
|
||||
# 扫描文件间隔时间,单位:分钟
|
||||
- ALIYUNPAN_SCAN_INTERVAL_TIME=1
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
# 检查aliyunpan进程是否存在
|
||||
ps | awk '{print $4}' | grep aliyunpan
|
||||
if [ $? == 0 ]
|
||||
then
|
||||
echo $?
|
||||
exit 0
|
||||
else
|
||||
echo $?
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# 关于 Windows EXE ICO 和应用信息编译
|
||||
为了编译出来的windows的exe文件带有ico和应用程序信息,需要使用 github.com/josephspurrier/goversioninfo/cmd/goversioninfo 工具
|
||||
|
||||
工具安装,运行下面的命令即可生成工具。也可以直接用 bin/ 文件夹下面的编译好的
|
||||
```
|
||||
go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo
|
||||
go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo
|
||||
```
|
||||
|
||||
versioninfo.json - 里面有exe程序信息以及ico的配置
|
||||
使用 goversioninfo 工具运行以下命令
|
||||
```
|
||||
goversioninfo -o=resource_windows_386.syso
|
||||
goversioninfo -64 -o=resource_windows_amd64.syso
|
||||
```
|
||||
即可编译出.syso资源库,再使用 go build 编译之后,exe文件就会拥有应用程序信息和ico图标
|
||||
@@ -0,0 +1,793 @@
|
||||
# 目录
|
||||
- [命令列表及说明](#命令列表及说明)
|
||||
* [注意](#注意)
|
||||
* [修改配置文件存储路径](#修改配置文件存储路径)
|
||||
* [检测程序更新](#检测程序更新)
|
||||
* [查看帮助](#查看帮助)
|
||||
* [登录阿里云盘帐号](#登录阿里云盘帐号)
|
||||
* [列出帐号列表](#列出帐号列表)
|
||||
* [获取当前帐号](#获取当前帐号)
|
||||
* [切换阿里云盘帐号](#切换阿里云盘帐号)
|
||||
* [退出阿里云盘帐号](#退出阿里云盘帐号)
|
||||
* [切换网盘(备份盘/资源库)](#切换网盘)
|
||||
* [获取网盘配额](#获取网盘配额)
|
||||
* [切换工作目录](#切换工作目录)
|
||||
* [输出工作目录](#输出工作目录)
|
||||
* [列出目录](#列出目录)
|
||||
* [下载文件/目录](#下载文件目录)
|
||||
* [多用户联合下载](#多用户联合下载)
|
||||
* [上传文件/目录](#上传文件目录)
|
||||
* [创建目录](#创建目录)
|
||||
* [删除文件/目录](#删除文件目录)
|
||||
* [移动文件/目录](#移动文件目录)
|
||||
* [重命名文件/目录](#重命名文件目录)
|
||||
* [分享文件/目录](#分享文件目录)
|
||||
+ [设置分享文件/目录](#设置分享文件目录)
|
||||
+ [创建快传链接](#创建快传链接)
|
||||
+ [列出已分享文件/目录](#列出已分享文件目录)
|
||||
+ [取消分享文件/目录](#取消分享文件目录)
|
||||
* [共享相册](#共享相册)
|
||||
+ [展示共享相簿列表](#展示共享相簿列表)
|
||||
+ [展示指定相簿中的文件](#展示指定相簿中的文件)
|
||||
+ [下载相簿中的所有文件](#下载相簿中的所有文件)
|
||||
* [同步备份功能](#同步备份功能)
|
||||
+ [常用命令说明](#常用命令说明)
|
||||
+ [备份配置文件说明](#备份配置文件说明)
|
||||
+ [命令行启动](#命令行启动)
|
||||
+ [Linux后台启动](#Linux后台启动)
|
||||
+ [Windows后台启动](#Windows后台启动)
|
||||
+ [Docker运行](#Docker运行)
|
||||
* [JavaScript插件](#JavaScript插件)
|
||||
* [显示和修改程序配置项](#显示和修改程序配置项)
|
||||
- [常见问题Q&A](#常见问题QA)
|
||||
* [1. 如何开启Debug调试日志](#1-如何开启Debug调试日志)
|
||||
|
||||
# 命令列表及说明
|
||||
## 注意
|
||||
|
||||
命令的前缀 `aliyunpan` 为指向程序运行的全路径名 (ARGv 的第一个参数)
|
||||
|
||||
直接运行程序时, 未带任何其他参数, 则程序进入cli交互模式, 进入cli模式运行以下命令时要把命令的前缀 `aliyunpan` 去掉! 即不需要输入`aliyunpan`。
|
||||
|
||||
cli交互模式支持按tab键自动补全命令.
|
||||
|
||||
## 修改配置文件存储路径
|
||||
设置环境变量ALIYUNPAN_CONFIG_DIR并指定一个存在目录即可,注意目录需要是绝对路径
|
||||
```
|
||||
例如linux下面可以这样指定
|
||||
|
||||
export ALIYUNPAN_CONFIG_DIR=/home/tickstep/tools/aliyunpan/config
|
||||
```
|
||||
|
||||
## 检测程序更新
|
||||
```
|
||||
aliyunpan update
|
||||
```
|
||||
|
||||
## 查看帮助
|
||||
```
|
||||
aliyunpan help
|
||||
```
|
||||
### 例子
|
||||
```
|
||||
列出程序支持的命令
|
||||
aliyunpan help
|
||||
|
||||
查看login命令的帮助手册
|
||||
aliyunpan help login
|
||||
```
|
||||
|
||||
## 登录阿里云盘帐号
|
||||
|
||||
### 登录
|
||||
当前支持在浏览器进行登录,请输入以下命令按照步骤进行即可。
|
||||
```
|
||||
aliyunpan login
|
||||
```
|
||||
|
||||
### 例子
|
||||
```
|
||||
aliyunpan > login
|
||||
请在浏览器打开以下链接进行登录,链接有效时间为5分钟。
|
||||
https://openapi.alipan.com/oauth/authorize?client_id=cf9f70e8fc61430f8ec5ab5cadf31375&redirect_uri=https%3A%2F%2Fapi.tickstep.com%2Fauth%2Ftickstep%2Faliyunpan%2Ftoken%2Fopenapi%2F8206f0.....fb5db6b40336%2Fauth&scope=user:base,file:all:read,file:all:write
|
||||
阿里云盘登录成功: tickstep
|
||||
aliyunpan:/ tickstep$
|
||||
```
|
||||
目前登录全部采用网页登录,由于 aliyunpan 融合了阿里官方Open接口+网页Web端接口,所以你需要进行两次登录。输入 login 命令
|
||||

|
||||
|
||||
复制该链接到浏览器打开,会自动跳转到阿里官方授权登录页面,点击允许完成第一次登录
|
||||

|
||||
|
||||
接着页面会自动跳转到网页接口登录页面,使用阿里APP扫码完成第二次登录
|
||||

|
||||
|
||||
然后切换回 aliyunpan 程序,按下 Enter 按键完成登录即可
|
||||

|
||||
|
||||
## 列出帐号列表
|
||||
|
||||
```
|
||||
aliyunpan loglist
|
||||
```
|
||||
|
||||
列出所有已登录的帐号
|
||||
|
||||
## 获取当前帐号
|
||||
|
||||
```
|
||||
aliyunpan who
|
||||
```
|
||||
|
||||
## 切换阿里云盘帐号
|
||||
|
||||
切换已登录的帐号
|
||||
```
|
||||
aliyunpan su <uid>
|
||||
```
|
||||
```
|
||||
aliyunpan su
|
||||
|
||||
请输入要切换帐号的 # 值 >
|
||||
```
|
||||
|
||||
## 退出阿里云盘帐号
|
||||
|
||||
退出当前登录的帐号
|
||||
```
|
||||
aliyunpan logout
|
||||
```
|
||||
|
||||
程序会进一步确认退出帐号, 防止误操作.
|
||||
|
||||
## 切换网盘
|
||||
程序默认工作在文件网盘下,如需切换到资源库网盘,可以使用本命令进行切换。
|
||||
```
|
||||
aliyunpan drive <driveId>
|
||||
```
|
||||
```
|
||||
aliyunpan drive
|
||||
|
||||
输入要切换的网盘 # 值 >
|
||||
```
|
||||
|
||||
## 获取网盘配额
|
||||
|
||||
```
|
||||
aliyunpan quota
|
||||
```
|
||||
获取网盘的总储存空间, 和已使用的储存空间
|
||||
|
||||
## 切换工作目录
|
||||
```
|
||||
aliyunpan cd <目录>
|
||||
```
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 切换 /我的文档 工作目录
|
||||
aliyunpan cd /我的文档
|
||||
|
||||
# 切换 上级目录
|
||||
aliyunpan cd ..
|
||||
|
||||
# 切换 根目录
|
||||
aliyunpan cd /
|
||||
|
||||
```
|
||||
|
||||
## 输出工作目录
|
||||
```
|
||||
aliyunpan pwd
|
||||
```
|
||||
|
||||
## 列出目录
|
||||
|
||||
列出当前工作目录的文件和目录或指定目录
|
||||
```
|
||||
aliyunpan ls
|
||||
```
|
||||
```
|
||||
aliyunpan ls <目录>
|
||||
```
|
||||
|
||||
### 可选参数
|
||||
```
|
||||
-driveId value 网盘ID
|
||||
```
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 列出 我的文档 内的文件和目录
|
||||
aliyunpan ls 我的文档
|
||||
|
||||
# 绝对路径
|
||||
aliyunpan ls /我的文档
|
||||
|
||||
# 详细列出 我的文档 内的文件和目录
|
||||
aliyunpan ll /我的文档
|
||||
```
|
||||
|
||||
## 下载文件/目录
|
||||
```
|
||||
aliyunpan download <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
|
||||
aliyunpan d <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
|
||||
```
|
||||
|
||||
### 可选参数
|
||||
```
|
||||
--ow overwrite, 覆盖已存在的文件
|
||||
--status 输出所有线程的工作状态
|
||||
--save 将下载的文件直接保存到当前工作目录
|
||||
--saveto value 将下载的文件直接保存到指定的目录
|
||||
-x 为文件加上执行权限, (windows系统无效)
|
||||
-p value parallel,指定同时进行下载文件的数量(取值范围:1 ~ 3) (default: 1)
|
||||
--sp value slice parallel,指定单个文件下载的最大线程(分片)数(取值范围:1 ~ 3) (default: 0)
|
||||
--retry value 下载失败最大重试次数 (default: 3)
|
||||
--nocheck 下载文件完成后不校验文件
|
||||
--np no progress 不展示下载进度条
|
||||
--driveId value 网盘ID
|
||||
--exn value exclude name,指定排除的文件夹或者文件的名称,被排除的文件不会进行下载,只支持正则表达式。支持同时排除多个名称,每一个名称就是一个exn参数
|
||||
--md (BETA) Multi-User Download,使用多用户联合下载,可以对单一文件叠加所有登录用户的下载速度
|
||||
--ui (BETA) 使用UI面板显示下载详情和进度,更加直观和友好
|
||||
```
|
||||
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 设置保存目录, 保存到 D:\Downloads
|
||||
# 注意区别反斜杠 "\" 和 斜杠 "/" !!!
|
||||
aliyunpan config set -savedir D:/Downloads
|
||||
|
||||
# 下载 /我的文档/1.mp4
|
||||
aliyunpan d /我的文档/1.mp4
|
||||
|
||||
# 下载 /我的文档 整个目录!!
|
||||
aliyunpan d /我的文档
|
||||
|
||||
# 下载 /photo 整个目录,并使用UI面板展示下载详情
|
||||
aliyunpan d /photo -ui
|
||||
```
|
||||
|
||||
下载的文件默认保存到 **程序所在目录** 的 download/ 目录, 支持设置指定目录, 重名的文件会自动跳过!
|
||||
|
||||
通过 `aliyunpan config set -savedir <savedir>` 可以自定义保存的目录。
|
||||
支持多个文件或目录下载,支持自动跳过下载重名的文件!
|
||||
|
||||
### Linux后台下载
|
||||
需要结合nohup进行启动。
|
||||
|
||||
新建 download.sh 脚本,内容如下
|
||||
```
|
||||
#!/bin/bash
|
||||
|
||||
# 是否开启调试日志
|
||||
export ALIYUNPAN_VERBOSE=0
|
||||
# (可选)配置目录的绝对路径(请更改成你自己的目录)
|
||||
export ALIYUNPAN_CONFIG_DIR=/Users/tickstep/Applications/adrive/config
|
||||
# aliyunpan程序所在的绝对路径(请更改成你自己的目录)
|
||||
export ALIYUNPAN_BIN=/Users/tickstep/Applications/adrive/aliyunpan
|
||||
|
||||
# 本地目录(请更改成你自己的目录)
|
||||
LOCAL_DIR="/tickstep/Documents/我的文档"
|
||||
# 网盘目录(请更改成你自己的目录)
|
||||
PAN_DIR="/我的文档"
|
||||
|
||||
# 执行下载
|
||||
"$ALIYUNPAN_BIN" download --saveto "$LOCAL_DIR" "$PAN_DIR"
|
||||
```
|
||||
|
||||
增加脚本执行权限
|
||||
```
|
||||
$ chmod +x download.sh
|
||||
```
|
||||
|
||||
然后启动该脚本进行后台运行
|
||||
```
|
||||
$ nohup ./download.sh > aliyunpan.log 2>&1 &
|
||||
```
|
||||
|
||||
## 多用户联合下载
|
||||
前提:程序必须登录多个帐号,并且登录授权都有效。
|
||||
```
|
||||
# 使用多用户联合下载 /我的资源/1.mp4 文件。必须保证所有登录的用户在相同的网盘(备份盘/资源盘)下,相同的路径下,有相同的文件
|
||||
aliyunpan download /我的资源/1.mp4 -md
|
||||
```
|
||||
什么是多用户联合下载?
|
||||
由于阿里云盘的限制,一个用户下载的时候只能有3个线程并发数,未开通三方权益包的账号一个下载线程速度大概是500KB/s,3个并发即为1.5MB/s。
|
||||
多用户联合下载,即是多个用户同时下载同一个文件,每个用户并发3个线程,N个用户即能开启 Nx3 个线程,对应的下载速度叠加为 Nx1.5MB/s。
|
||||
对下载速度有极致追求的用户可以尝试使用该选项。
|
||||
如果你的账号都开通了三方权益包,则一个用户下载速度为50MB/s,两个用户联合下载可以轻松突破100MB/s。
|
||||

|
||||
|
||||
## 上传文件/目录
|
||||
```
|
||||
aliyunpan upload <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>
|
||||
aliyunpan u <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>
|
||||
```
|
||||
|
||||
### 例子:
|
||||
```
|
||||
# 将本地的 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录
|
||||
# 注意区别反斜杠 "\" 和 斜杠 "/" !!!
|
||||
aliyunpan upload C:/Users/Administrator/Desktop/1.mp4 /视频
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Desktop\1.mp4 和 C:\Users\Administrator\Desktop\2.mp4 上传到网盘 /视频 目录
|
||||
aliyunpan upload C:/Users/Administrator/Desktop/1.mp4 C:/Users/Administrator/Desktop/2.mp4 /视频
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录
|
||||
aliyunpan upload C:/Users/Administrator/Desktop /视频
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录,并使用UI面板展示下载详情
|
||||
aliyunpan upload C:/Users/Administrator/Desktop /视频 -ui
|
||||
|
||||
## 下面演示文件或者文件夹排除功能
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
|
||||
aliyunpan upload -exn "\.jpg$" C:/Users/Administrator/Video /视频
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
|
||||
aliyunpan upload -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视频
|
||||
|
||||
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式
|
||||
1)排除@eadir文件或者文件夹:-exn "^@eadir$"
|
||||
2)排除.jpg文件:-exn "\.jpg$"
|
||||
3)排除.号开头的文件:-exn "^\."
|
||||
4)排除~号开头的文件:-exn "^~"
|
||||
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
|
||||
```
|
||||
### Linux后台上传
|
||||
需要结合nohup进行启动。
|
||||
|
||||
新建 upload.sh 脚本,内容如下
|
||||
```
|
||||
#!/bin/bash
|
||||
|
||||
# 是否开启调试日志 (1=开启, 0=关闭)
|
||||
export ALIYUNPAN_VERBOSE=0
|
||||
|
||||
# (可选)配置目录的绝对路径
|
||||
export ALIYUNPAN_CONFIG_DIR="/Users/tickstep/Applications/adrive/config"
|
||||
|
||||
# aliyunpan程序所在的绝对路径
|
||||
export ALIYUNPAN_BIN="/Users/tickstep/Applications/adrive/aliyunpan"
|
||||
|
||||
# 本地目录
|
||||
LOCAL_DIR="/tickstep/Documents/我的文档"
|
||||
|
||||
# 网盘目录
|
||||
PAN_DIR="/我的文档"
|
||||
|
||||
# 检查本地目录是否存在
|
||||
if [ ! -d "$LOCAL_DIR" ]; then
|
||||
echo "[ERROR] 本地目录不存在: $LOCAL_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 执行上传(已修复引号、添加安全引用和覆盖策略)
|
||||
"$ALIYUNPAN_BIN" upload \
|
||||
-exn "^\." \
|
||||
-exn "^@eadir$" \
|
||||
-ow \
|
||||
"$LOCAL_DIR" \
|
||||
"$PAN_DIR"
|
||||
|
||||
echo "[INFO] 上传任务执行完毕"
|
||||
```
|
||||
|
||||
增加脚本执行权限
|
||||
```
|
||||
$ chmod +x upload.sh
|
||||
```
|
||||
|
||||
然后启动该脚本进行后台运行
|
||||
```
|
||||
$ nohup ./upload.sh > aliyunpan.log 2>&1 &
|
||||
```
|
||||
|
||||
## 创建目录
|
||||
```
|
||||
aliyunpan mkdir <目录>
|
||||
```
|
||||
|
||||
### 例子
|
||||
```
|
||||
aliyunpan mkdir test123
|
||||
```
|
||||
|
||||
## 删除文件/目录
|
||||
```
|
||||
aliyunpan rm <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
|
||||
```
|
||||
|
||||
注意: 删除多个文件和目录时, 请确保每一个文件和目录都存在, 否则删除操作会失败.
|
||||
|
||||
被删除的文件或目录可在网盘文件回收站找回.
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 删除 /我的文档/1.mp4
|
||||
aliyunpan rm /我的文档/1.mp4
|
||||
|
||||
# 删除 /我的文档/1.mp4 和 /我的文档/2.mp4
|
||||
aliyunpan rm /我的文档/1.mp4 /我的文档/2.mp4
|
||||
|
||||
# 删除 /我的文档 整个目录 !!
|
||||
aliyunpan rm /我的文档
|
||||
```
|
||||
|
||||
|
||||
## 移动文件/目录
|
||||
```
|
||||
aliyunpan mv <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>
|
||||
```
|
||||
|
||||
注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败.
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 将 /我的文档/1.mp4 移动到 根目录 /
|
||||
aliyunpan mv /我的文档/1.mp4 /
|
||||
```
|
||||
|
||||
## 重命名文件/目录
|
||||
```
|
||||
aliyunpan rename <旧文件/目录名> <新文件/目录名>
|
||||
```
|
||||
|
||||
注意: 重命名的文件/目录,如果指定的是绝对路径,则必须保证新旧的绝对路径在同一个文件夹内,否则重命名失败!
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 将 /我的文档/1.mp4 重命名为 /我的文档/2.mp4
|
||||
aliyunpan rename /我的文档/1.mp4 /我的文档/2.mp4
|
||||
```
|
||||
|
||||
## 分享文件/目录
|
||||
```
|
||||
aliyunpan share
|
||||
```
|
||||
|
||||
### 设置分享文件/目录
|
||||
阿里目前只支持少数文件类型的分享,不支持的文件分享会提示分享失败
|
||||
```
|
||||
aliyunpan share set -mode 1 <文件/目录1> <文件/目录2> ...
|
||||
```
|
||||
|
||||
### 创建快传链接
|
||||
阿里的快传支持大部分文件的共享,例如zip压缩包,按照如下方式可以创建快传链接
|
||||
```
|
||||
aliyunpan share set -mode 3 <文件/目录1> <文件/目录2> ...
|
||||
```
|
||||
|
||||
### 列出已分享文件/目录
|
||||
```
|
||||
aliyunpan share list
|
||||
```
|
||||
|
||||
### 取消分享文件/目录
|
||||
```
|
||||
aliyunpan share cancel <shareid_1> <shareid_2> ...
|
||||
```
|
||||
目前只支持通过分享id (shareid) 来取消分享.
|
||||
|
||||
## 共享相册
|
||||
```
|
||||
aliyunpan album
|
||||
```
|
||||
|
||||
### 展示共享相簿列表
|
||||
```
|
||||
aliyunpan album list
|
||||
```
|
||||
|
||||
### 展示指定相簿中的文件
|
||||
展示相簿中文件,同名的相簿只会展示第一个符合条件的
|
||||
```
|
||||
例如:展示相簿中文件"我的相簿2025"
|
||||
aliyunpan album list-file "我的相簿2025"
|
||||
```
|
||||
|
||||
### 下载相簿中的所有文件
|
||||
下载相簿中的所有文件到本地,支持普通照片、实况照片。
|
||||
```
|
||||
下载相簿 "我的相簿2025" 里面的所有文件
|
||||
aliyunpan album download-file 我的相簿2025
|
||||
```
|
||||
|
||||
## 同步备份功能
|
||||
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地两种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
指定本地目录和对应的一个网盘目录,以备份文件。网盘目录必须和本地目录独占使用,不要用作其他用途,不然备份可能会有问题。
|
||||
|
||||
备份功能支持以下模式:
|
||||
1. 备份本地文件,即上传本地文件到网盘,始终保持本地文件有一个完整的备份在网盘
|
||||
2. 备份云盘文件,即下载网盘文件到本地,始终保持网盘的文件有一个完整的备份在本地
|
||||
|
||||
备份功能支持指定备份策略:
|
||||
1. exclusive,排他备份文件,目标目录多余的文件会被删除。保证备份的源目录,和目标目录文件一比一备份。源目录文件如果文件被删除,则对应的目标目录的文件也会被删除。
|
||||
2. increment,增量备份文件,目标目录多余的文件不会被删除。只会把源目录修改的文件,新增的文件备份到目标目录。如果源目录有文件删除,或者目标目录有其他文件新增是不会被删除。
|
||||
|
||||
备份功能一般用于NAS等系统,进行文件备份。比如备份照片,就可以使用这个功能定期备份照片到云盘。
|
||||
|
||||
同步的基本逻辑如下所示,一次循环包括:扫描-对比-执行,一共三个环节。
|
||||

|
||||
|
||||
### 常用命令说明
|
||||
```
|
||||
查看同步备份功能说明
|
||||
aliyunpan sync
|
||||
|
||||
查看如何配置和启动同步备份功能
|
||||
aliyunpan sync start -h
|
||||
|
||||
使用命令行配置启动同步备份服务,将本地目录 D:\tickstep\Documents\设计文档 中的文件备份上传到云盘目录 /sync_drive/我的文档
|
||||
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "upload"
|
||||
|
||||
使用命令行配置启动同步备份服务,将云盘目录 /sync_drive/我的文档 中的文件备份下载到本地目录 D:\tickstep\Documents\设计文档
|
||||
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "download"
|
||||
|
||||
使用命令行配置启动同步备份服务,将本地目录 D:\tickstep\Documents\设计文档 中的文件备份到云盘目录 /sync_drive/我的文档
|
||||
同时配置下载并发为2,上传并发为1,下载分片大小为256KB,上传分片大小为1MB
|
||||
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "upload" -dp 2 -up 1 -dbs 256 -ubs 1024
|
||||
|
||||
使用配置文件启动同步备份服务,使用配置文件可以支持同时启动多个备份任务。配置文件必须存在,否则启动失败。
|
||||
aliyunpan sync start
|
||||
|
||||
使用配置文件启动同步备份服务,并配置下载并发为2,上传并发为1,下载分片大小为256KB,上传分片大小为1MB
|
||||
aliyunpan sync start -dp 2 -up 1 -dbs 256 -ubs 1024
|
||||
```
|
||||
|
||||
### 备份配置文件说明
|
||||
如果你只有一个文件夹进行备份建议直接使用命令行配置启动即可。如果需要同时启动多个备份任务,则可以使用备份配置文件启动同步备份任务。
|
||||
配置文件如下所示,如果你有通过环境变量```ALIYUNPAN_CONFIG_DIR```设置配置目录,则需要将```sync_drive```文件夹拷贝到配置的目录中才可以生效。
|
||||
```
|
||||
配置文件需要保存在:(配置目录)/sync_drive/sync_drive_config.json,样例如下:
|
||||
|
||||
{
|
||||
"configVer": "1.0",
|
||||
"syncTaskList": [
|
||||
{
|
||||
"name": "设计文档备份",
|
||||
"localFolderPath": "D:/tickstep/Documents/设计文档",
|
||||
"panFolderPath": "/备份盘/我的文档",
|
||||
"mode": "upload",
|
||||
"driveName": "backup"
|
||||
},
|
||||
{
|
||||
"name": "手机图片备份",
|
||||
"localFolderPath": "D:/tickstep/Photos/手机图片",
|
||||
"panFolderPath": "/备份盘/手机图片",
|
||||
"mode": "upload",
|
||||
"driveName": "resource"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
相关字段说明如下:
|
||||
name - 任务名称
|
||||
localFolderPath - 本地目录
|
||||
panFolderPath - 网盘目录
|
||||
mode - 模式,支持: upload(备份本地文件到云盘),download(备份云盘文件到本地)
|
||||
driveName - 网盘,支持:backup(备份盘), resource(资源盘)
|
||||
```
|
||||
|
||||
### 命令行启动
|
||||
需要先进行登录。然后使用以下命令运行即可,该命令是阻塞的不会退出。
|
||||
|
||||
```
|
||||
使用命令行配置启动同步备份服务,将本地目录 /tickstep/Documents/设计文档 中的文件备份上传到云盘目录 /备份盘/我的文档
|
||||
./aliyunpan sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload" -drive "backup"
|
||||
|
||||
参数说明
|
||||
ldir:本地目录
|
||||
pdir:云盘目录
|
||||
mode:备份模式,支持:upload(备份本地文件到云盘),download(备份云盘文件到本地)
|
||||
drive - 网盘,支持:backup(备份盘), resource(资源盘)
|
||||
|
||||
--------------------------------------------------------------
|
||||
正常会有以下的输出:
|
||||
|
||||
启动同步备份进程
|
||||
备份配置文件:(使用命令行配置)
|
||||
链接类型:默认链接
|
||||
下载并发:2
|
||||
上传并发:2
|
||||
下载分片大小:1.00MB
|
||||
上传分片大小:10.00MB
|
||||
|
||||
启动同步任务
|
||||
任务: 设计文档(de3d6b69a607497b73624bcca0845f19)
|
||||
同步模式: 备份本地文件(只上传)
|
||||
本地目录: /tickstep/Documents/设计文档
|
||||
云盘目录: /备份盘/我的文档
|
||||
```
|
||||
|
||||
### Linux后台启动
|
||||
建议结合nohup进行启动。
|
||||
|
||||
sync.sh脚本,内容如下
|
||||
```
|
||||
#!/bin/bash
|
||||
|
||||
# 是否开启调试日志
|
||||
export ALIYUNPAN_VERBOSE=0
|
||||
# (可选)配置目录的绝对路径(请更改成你自己的目录)
|
||||
export ALIYUNPAN_CONFIG_DIR=/Users/tickstep/Applications/adrive/config
|
||||
# aliyunpan程序所在的绝对路径(请更改成你自己的目录)
|
||||
export ALIYUNPAN_BIN=/Users/tickstep/Applications/adrive/aliyunpan
|
||||
|
||||
# 本地目录(请更改成你自己的目录)
|
||||
LOCAL_DIR="/tickstep/Documents/设计文档"
|
||||
# 网盘目录(请更改成你自己的目录)
|
||||
PAN_DIR="/备份盘/我的文档"
|
||||
|
||||
# 指定配置参数并进行启动
|
||||
# 支持的模式:upload(备份本地文件到云盘),download(备份云盘文件到本地)
|
||||
"$ALIYUNPAN_BIN" sync start -ldir "$LOCAL_DIR" -pdir "$PAN_DIR" -mode "upload" -drive "backup"
|
||||
```
|
||||
|
||||
增加脚本执行权限
|
||||
```
|
||||
$ chmod +x sync.sh
|
||||
```
|
||||
|
||||
然后启动该脚本进行后台运行
|
||||
```
|
||||
$ nohup ./sync.sh >/dev/null 2>&1 &
|
||||
```
|
||||
|
||||
### Windows后台启动
|
||||
需要结合 [WinSW](https://github.com/winsw/winsw) 进行后台启动,请前往官网自行下载: https://github.com/winsw/winsw
|
||||
|
||||
步骤如下:
|
||||
1. 配置好备份任务:(配置目录)\sync_drive\sync_drive_config.json
|
||||
2. 下载winsw.exe并更名为alisync.exe
|
||||
3. 新增一个alisync.xml文件,内容如下:
|
||||
```xml
|
||||
<service>
|
||||
<id>alisync</id>
|
||||
<name>alisync</name>
|
||||
<description>aliyunpan-sync 后台备份服务。</description>
|
||||
<env name="ALIYUNPAN_CONFIG_DIR" value="(更改成你PC上aliyunpan.exe工具所在目录)"/>
|
||||
<executable>aliyunpan.exe</executable>
|
||||
<arguments>sync start</arguments>
|
||||
<log mode="roll"></log>
|
||||
</service>
|
||||
```
|
||||
4. 将alisync.exe和alisync.xml存放在你PC上aliyunpan.exe工具所在目录,例如:
|
||||

|
||||
5. CMD命令行启动程序
|
||||
```
|
||||
# 安装服务(只需要第一次安装,后面不用再安装)
|
||||
D:\Program Files\aliyunpan>alisync install
|
||||
|
||||
# 启动服务
|
||||
D:\Program Files\aliyunpan>alisync start
|
||||
2022-06-21 13:39:05,795 INFO - Starting service 'alisync (alisync)'...
|
||||
2022-06-21 13:39:06,361 INFO - Service 'alisync (alisync)' started successfully.
|
||||
|
||||
# 查看服务状态
|
||||
D:\Program Files\aliyunpan>alisync status
|
||||
Started
|
||||
|
||||
# 停止服务
|
||||
D:\Program Files\aliyunpan>alisync stop
|
||||
2022-06-21 13:42:32,201 INFO - Stopping service 'alisync (alisync)'...
|
||||
2022-06-21 13:42:32,211 INFO - Service 'alisync (alisync)' stopped successfully.
|
||||
```
|
||||
效果截图如下
|
||||

|
||||
|
||||
### Docker运行
|
||||
详情文档请参考dockerhub网址:[tickstep/aliyunpan-sync](https://hub.docker.com/r/tickstep/aliyunpan-sync)
|
||||
|
||||
1. 直接运行
|
||||
```
|
||||
docker run -d --name=aliyunpan-sync --restart=always -v "<your aliyunpan_config.json>:/home/app/config/aliyunpan_config.json" -v "<your local dir>:/home/app/data" -e ALIYUNPAN_PAN_DIR="<your drive pan dir>" -e ALIYUNPAN_SYNC_MODE="upload" -e ALIYUNPAN_SYNC_POLICY="increment" -e ALIYUNPAN_SYNC_DRIVE="backup" -e ALIYUNPAN_SYNC_LOG="true" tickstep/aliyunpan-sync:<tag>
|
||||
|
||||
<your aliyunpan_config.json>: 用户已经登录成功并保存好的aliyunpan_config.json凭据文件
|
||||
<your local dir>:本地目标目录,绝对路径,例如:/tickstep/Documents/设计文档
|
||||
ALIYUNPAN_PAN_DIR:云盘目标目录,绝对路径
|
||||
ALIYUNPAN_SYNC_MODE:备份模式,支持: upload(备份本地文件到云盘),download(备份云盘文件到本地)
|
||||
ALIYUNPAN_SYNC_POLICY:备份策略,支持:exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)
|
||||
ALIYUNPAN_SYNC_DRIVE: 网盘,支持:backup(备份盘), resource(资源盘)
|
||||
ALIYUNPAN_SYNC_LOG: 同步日志,true-开启同步日志显示,false-关闭同步日志
|
||||
```
|
||||
|
||||
2. docker-compose运行
|
||||
docker-compose.yml 文件如下所示,为了方便说明增加了相关的注释,部署的时候可以去掉注释。
|
||||
|
||||
```
|
||||
version: '3'
|
||||
services:
|
||||
sync:
|
||||
image: tickstep/aliyunpan-sync:<tag>
|
||||
container_name: aliyunpan-sync
|
||||
restart: always
|
||||
volumes:
|
||||
# (必须)映射的本地目录
|
||||
- ./data:/home/app/data:rw
|
||||
# (可选)可以指定JS插件sync_handler.js用于过滤文件,详见插件说明
|
||||
#- ./plugin/js/sync_handler.js:/home/app/config/plugin/js/sync_handler.js
|
||||
# (推荐)挂载sync_drive同步数据库到本地,这样即使容器销毁,同步数据库还可以用于以后使用
|
||||
#- ./sync_drive:/home/app/config/sync_drive
|
||||
# (必须)映射token凭据文件
|
||||
- /your/file/path/for/aliyunpan_config.json:/home/app/config/aliyunpan_config.json
|
||||
environment:
|
||||
# 时区,东8区
|
||||
- TZ=Asia/Shanghai
|
||||
# 下载文件并发数
|
||||
- ALIYUNPAN_DOWNLOAD_PARALLEL=2
|
||||
# 上传文件并发数
|
||||
- ALIYUNPAN_UPLOAD_PARALLEL=2
|
||||
# 下载数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
|
||||
- ALIYUNPAN_DOWNLOAD_BLOCK_SIZE=1024
|
||||
# 上传数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
|
||||
- ALIYUNPAN_UPLOAD_BLOCK_SIZE=10240
|
||||
# 指定网盘文件夹作为备份目标目录,不要指定根目录
|
||||
- ALIYUNPAN_PAN_DIR=/my_sync_dir
|
||||
# 备份模式:upload(备份本地文件到云盘), download(备份云盘文件到本地)
|
||||
- ALIYUNPAN_SYNC_MODE=upload
|
||||
# 备份策略: exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)
|
||||
- ALIYUNPAN_SYNC_POLICY=increment
|
||||
# 备份周期, 支持两种: infinity(永久循环备份),onetime(只运行一次备份)
|
||||
- ALIYUNPAN_SYNC_CYCLE=infinity
|
||||
# 网盘:backup(备份盘), resource(资源盘)
|
||||
- ALIYUNPAN_SYNC_DRIVE=backup
|
||||
# 是否显示文件备份过程日志,true-显示,false-不显示
|
||||
- ALIYUNPAN_SYNC_LOG=true
|
||||
# 本地文件修改检测延迟间隔,单位秒。如果本地文件会被频繁修改,例如录制视频文件,配置好该时间可以避免上传未录制好的文件
|
||||
- ALIYUNPAN_LOCAL_DELAY_TIME=3
|
||||
```
|
||||
|
||||
3. sync_handler.js插件说明
|
||||
可以使用JS插件过滤备份的文件。更多细节请查看文档:[JavaScript插件手册](https://github.com/tickstep/aliyunpan/blob/main/docs/plugin_manual.md#如何使用)
|
||||
|
||||
## JavaScript插件
|
||||
本程序支持javascript插件,更多细节请查看文档:[JavaScript插件手册](https://github.com/tickstep/aliyunpan/blob/main/docs/plugin_manual.md)
|
||||
|
||||
## 显示和修改程序配置项
|
||||
```
|
||||
# 显示配置
|
||||
aliyunpan config
|
||||
|
||||
# 设置配置
|
||||
aliyunpan config set
|
||||
```
|
||||
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 显示所有可以设置的值
|
||||
aliyunpan config -h
|
||||
aliyunpan config set -h
|
||||
|
||||
# 设置下载文件的储存目录
|
||||
aliyunpan config set -savedir D:/Downloads
|
||||
|
||||
# 设置下载最大并发量为 15
|
||||
aliyunpan config set -max_download_parallel 15
|
||||
|
||||
# 组合设置
|
||||
aliyunpan config set -max_download_parallel 15 -savedir D:/Downloads
|
||||
```
|
||||
|
||||
# 常见问题Q&A
|
||||
## 1 如何开启Debug调试日志
|
||||
当需要定位问题,或者提交issue的时候抓取log,则需要开启debug日志。步骤如下:
|
||||
|
||||
### 第一步
|
||||
Linux&MacOS
|
||||
命令行运行
|
||||
```
|
||||
export ALIYUNPAN_VERBOSE=1
|
||||
```
|
||||
|
||||
Windows
|
||||
不同版本会有些许不一样,请自行查询具体方法
|
||||
设置示意图如下:
|
||||

|
||||
|
||||
### 第二步
|
||||
打开aliyunpan命令行程序,任何云盘命令都有类似如下日志输出
|
||||

|
||||
@@ -0,0 +1,553 @@
|
||||
# 目录
|
||||
- [简介](#简介)
|
||||
- [如何使用](#如何使用)
|
||||
- [JS中内置的函数](#JS中内置的函数)
|
||||
+ [console.log()](#consolelog)
|
||||
+ [console.println()](#consoleprintln)
|
||||
+ [PluginUtil.Http.get()](#PluginUtilHttpget)
|
||||
+ [PluginUtil.Http.post()](#PluginUtilHttppost)
|
||||
+ [PluginUtil.LocalFS.deleteFile()](#PluginUtilLocalFSdeleteFile)
|
||||
+ [PluginUtil.PanFS.deleteFile()](#PluginUtilPanFSdeleteFile)
|
||||
+ [PluginUtil.Email.sendTextMail()](#PluginUtilEmailsendTextMail)
|
||||
+ [PluginUtil.Email.sendHtmlMail()](#PluginUtilEmailsendHtmlMail)
|
||||
+ [PluginUtil.KV.putString()](#PluginUtilKVputString)
|
||||
+ [PluginUtil.KV.getString()](#PluginUtilKVgetString)
|
||||
+ [PluginUtil.HashTool.md5Hex()](#PluginUtilHashToolmd5Hex)
|
||||
- [常见场景样例](#常见场景样例)
|
||||
+ [1.禁止特定文件上传](#1禁止特定文件上传)
|
||||
+ [2.上传文件后删除本地文件](#2上传文件后删除本地文件)
|
||||
+ [3.下载文件并截断过长的文件名](#3下载文件并截断过长的文件名)
|
||||
+ [4.上传文件去掉文件名包含的部分字符](#4上传文件去掉文件名包含的部分字符)
|
||||
+ [5.上传文件时过滤指定目录或者文件路径](#5上传文件时过滤指定目录或者文件路径)
|
||||
+ [6.下载云盘文件到本地后删除云盘对应的文件](#6下载云盘文件到本地后删除云盘对应的文件)
|
||||
+ [7.Token刷新失败发送外部通知](#7Token刷新失败发送外部通知)
|
||||
+ [8.每次只下载指定数量的文件](#8每次只下载指定数量的文件)
|
||||
|
||||
# 简介
|
||||
本程序支持javascript插件。通过JS插件,你可以按照自己的需要定制上传、下载、同步、删除过程中关键步骤的行为,最大程度满足自己的个性化需求。
|
||||
例如:
|
||||
1. 排除某个特定敏感文件的上传
|
||||
2. 上传文件进行改名,但是本地文件不做更改
|
||||
3. 上传完成文件,删除本地文件
|
||||
4. 上传的文件路径进行更改,但是本地的文件保持不变
|
||||
5. 上传文件成功后,通过HTTP通知其他服务
|
||||
6. 排除某些网盘文件的下载
|
||||
7. 下载的文件进行改名,但是网盘的文件保持不变
|
||||
8. 下载的文件路径进行更改,但是网盘的文件保持不变
|
||||
9. 下载文件完成后,通过HTTP通知其他服务
|
||||
10. 同步备份功能,支持过滤本地文件,或者过滤云盘文件。定制上传或者下载需要同步的文件
|
||||
|
||||
# 如何使用
|
||||
JS插件的样本文件默认存放在程序所在的```plugin/js```文件夹下,分别为:
|
||||
1. 下载插件(download_handler.js.sample)
|
||||
2. 上传插件(upload_handler.js.sample)
|
||||
3. 删除插件(remove_handler.js.sample)
|
||||
4. 同步备份插件(sync_handler.js.sample)
|
||||
5. 用户Token插件(token_handler.js.sample)
|
||||
|
||||
建议拷贝一份并将后缀名更改为.js,例如:upload_handler.js,不然插件不会生效。
|
||||
你必须具备一定的JS语言基础,然后按照里面的样例根据自己所需进行改动即可。如果你不会JS那也没关系,你可以提issue需求,然后我们开发成员或者网友会给你提供JS脚本代码。
|
||||
注意:如果你有通过环境变量```ALIYUNPAN_CONFIG_DIR```设置配置目录,则需要将plugin文件夹拷贝到配置的目录中才可以生效。
|
||||
|
||||
# JS中内置的函数
|
||||
目前开放了如下函数,你可以在你的js脚本中直接调用,以用于增强JS脚本的扩展性、可玩性以及可适用性。
|
||||
|
||||
## console.log()
|
||||
打印日志,这个日志需要开启debug日志才会在控制台窗口显示
|
||||
```js
|
||||
console.log("hello world");
|
||||
```
|
||||
## console.println()
|
||||
打印日志,这个日志会直接在控制台窗口显示,无需开启debug日志
|
||||
```js
|
||||
console.println("hello world");
|
||||
```
|
||||
|
||||
## PluginUtil.Http.get()
|
||||
发起HTTP的get请求
|
||||
```js
|
||||
var header = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
};
|
||||
try {
|
||||
var r = PluginUtil.Http.get(header, "https://625f528c53a42eaa07f37e13.mockapi.io/files/1");
|
||||
console.log(r);
|
||||
} catch (e) {
|
||||
if (e !== "Error") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## PluginUtil.Http.post()
|
||||
发起HTTP的post请求
|
||||
```js
|
||||
var header = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
};
|
||||
try {
|
||||
var reqDataStr = JSON.stringify({
|
||||
"id": "1",
|
||||
"localFilePath": "/usr/local/src/borders_burundi_producer.gram.aab",
|
||||
"localFileSize": 1111,
|
||||
"uploadApproved": false
|
||||
});
|
||||
var r = PluginUtil.Http.post(header, "https://625f528c53a42eaa07f37e13.mockapi.io/files", reqDataStr);
|
||||
console.log(r);
|
||||
} catch (e) {
|
||||
if (e !== "Error") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## PluginUtil.LocalFS.deleteFile()
|
||||
删除本地指定文件,不支持文件夹
|
||||
```
|
||||
PluginUtil.LocalFS.deleteFile(localFilePath);
|
||||
其中:
|
||||
localFilePath - 本地文件的绝对完整路径
|
||||
```
|
||||
样例
|
||||
```js
|
||||
PluginUtil.LocalFS.deleteFile("/Users/tickstep/Downloads/IMG_0884.HEIC");
|
||||
```
|
||||
|
||||
## PluginUtil.PanFS.deleteFile()
|
||||
删除云盘指定文件,支持文件、文件夹
|
||||
```
|
||||
PluginUtil.PanFS.deleteFile(userId, driveId, panFileId);
|
||||
其中:
|
||||
userId - 登录的用户ID
|
||||
driveId - 网盘ID
|
||||
panFileId - 网盘文件ID
|
||||
```
|
||||
样例
|
||||
```js
|
||||
var userId = context["userId"]
|
||||
var driveId = params["driveId"]
|
||||
var driveFileId = params["driveFileId"]
|
||||
if (PluginUtil.PanFS.deleteFile(userId, driveId, driveFileId)) {
|
||||
console.println("插件删除云盘文件成功")
|
||||
}
|
||||
```
|
||||
|
||||
## PluginUtil.Email.sendTextMail()
|
||||
发送文本邮件,定义如下
|
||||
```
|
||||
PluginUtil.Email.sendTextMail(mailServer, userName, password, to, subject, body)
|
||||
|
||||
其中:
|
||||
mailServer - smtp服务器+端口
|
||||
userName - 发件人邮箱地址
|
||||
password - 发件人邮箱密码
|
||||
to - 收件人邮箱地址
|
||||
subject - 邮件标题
|
||||
body - 邮件内容,纯文本
|
||||
```
|
||||
样例
|
||||
```js
|
||||
PluginUtil.Email.sendTextMail("smtp.qq.com:465", "123xxx@qq.com", "pwdxxxxxx", "12545xxx@qq.com", "文件上传通知", "该文件已经上传完毕");
|
||||
```
|
||||
|
||||
## PluginUtil.Email.sendHtmlMail()
|
||||
发送HTML富文本邮件,定义如下
|
||||
```
|
||||
PluginUtil.Email.sendHtmlMail(mailServer, userName, password, to, subject, body)
|
||||
|
||||
其中:
|
||||
mailServer - smtp服务器+端口
|
||||
userName - 发件人邮箱地址
|
||||
password - 发件人邮箱密码
|
||||
to - 收件人邮箱地址
|
||||
subject - 邮件标题
|
||||
body - 邮件内容,HTML富文本
|
||||
```
|
||||
样例
|
||||
```js
|
||||
var html = "<html>"
|
||||
+"<body>"
|
||||
+"<h1>文件上传通知</h1>"
|
||||
+"<h2>该文件已经上传完毕</h2>"
|
||||
+"</body>"
|
||||
+"</html>";
|
||||
PluginUtil.Email.sendHtmlMail("smtp.qq.com:465", "123xxx@qq.com", "pwdxxxxxx", "12545xxx@qq.com", "文件上传通知", html);
|
||||
```
|
||||
|
||||
## PluginUtil.KV.putString()
|
||||
存储键值对,定义如下
|
||||
```
|
||||
PluginUtil.KV.putString(key, value)
|
||||
|
||||
其中:
|
||||
key - 键,字符串
|
||||
value - 值,字符串
|
||||
```
|
||||
样例
|
||||
```js
|
||||
PluginUtil.KV.putString("mykey", "1670419352");
|
||||
```
|
||||
|
||||
## PluginUtil.KV.getString()
|
||||
获取存储的键值,指定对应的键,返回存储的值,如果没有则返回空字符串,定义如下
|
||||
```
|
||||
PluginUtil.KV.getString(key)
|
||||
|
||||
其中:
|
||||
key - 键,字符串
|
||||
```
|
||||
样例
|
||||
```js
|
||||
var value = PluginUtil.KV.getString("mykey");
|
||||
```
|
||||
|
||||
## PluginUtil.HashTool.md5Hex()
|
||||
计算指定字符串text的MD5值,返回hex格式的字符串MD5,定义如下
|
||||
```
|
||||
PluginUtil.HashTool.md5Hex(text)
|
||||
|
||||
其中:
|
||||
text - 字符串
|
||||
```
|
||||
样例
|
||||
```js
|
||||
var md5 = PluginUtil.HashTool.md5Hex("123456");
|
||||
```
|
||||
|
||||
# 常见场景样例
|
||||
这里收集了一些常见的需求样例,可以作为插件定制的样例模板。
|
||||
|
||||
## 1.禁止特定文件上传
|
||||
使用JavaScript上传插件中的`uploadFilePrepareCallback`函数。如下所示:
|
||||
```js
|
||||
function uploadFilePrepareCallback(context, params) {
|
||||
console.log(params)
|
||||
|
||||
var result = {
|
||||
"uploadApproved": "yes",
|
||||
"driveFilePath": ""
|
||||
};
|
||||
if (params["localFileType"] != "file") {
|
||||
// do nothing
|
||||
return result;
|
||||
}
|
||||
|
||||
// 禁止点号.开头的文件上传
|
||||
if (params["localFileName"].indexOf(".") == 0) {
|
||||
result["uploadApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止.txt文件上传
|
||||
if (params["localFileName"].search(/.txt$/i) >= 0) {
|
||||
result["uploadApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止password.key文件上传
|
||||
if (params["localFileName"] == "password.key") {
|
||||
result["uploadApproved"] = "no";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
## 2.上传文件后删除本地文件
|
||||
使用JavaScript上传插件中的`uploadFileFinishCallback`函数。如下所示:
|
||||
```js
|
||||
function uploadFileFinishCallback(context, params) {
|
||||
console.log(params);
|
||||
if (params["localFileType"] != "file") {
|
||||
// do nothing
|
||||
return;
|
||||
}
|
||||
if (params["uploadResult"] == "success") {
|
||||
PluginUtil.LocalFS.deleteFile(params["localFilePath"]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3.下载文件并截断过长的文件名
|
||||
有些文件的路径或者名称太长,下载的时候可能会由于路径名称过程导致无法下载,这时候可以使用JavaScript下载插件中的`downloadFilePrepareCallback`函数定制下载保存的文件名。
|
||||
如下所示:
|
||||
```js
|
||||
function downloadFilePrepareCallback(context, params) {
|
||||
console.log(params)
|
||||
|
||||
var result = {
|
||||
"downloadApproved": "yes",
|
||||
"localFilePath": ""
|
||||
};
|
||||
if (params["driveFileType"] != "file") {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 下面的代码都是分隔路径,方便后面修改路径时使用
|
||||
var filePath = params["localFilePath"];
|
||||
filePath = filePath.replace(/\\/g, "/");
|
||||
|
||||
// 目录完整路径
|
||||
var dirPath = "";
|
||||
// 文件名,不包括后缀名
|
||||
var fileName = "";
|
||||
// 文件后缀名
|
||||
var fileExt = "";
|
||||
|
||||
var idx = filePath.lastIndexOf('/');
|
||||
if (idx > 0) {
|
||||
dirPath = filePath.substring(0,idx);
|
||||
fileName = filePath.substring(idx+1,filePath.length);
|
||||
} else {
|
||||
fileName = filePath;
|
||||
}
|
||||
idx = fileName.lastIndexOf(".")
|
||||
if (idx > 0) {
|
||||
fileExt = fileName.substring(idx,fileName.length);
|
||||
fileName = fileName.substring(0, fileName.length-fileExt.length)
|
||||
}
|
||||
|
||||
// 开始按照需要截断太长的文件路径,例如下面的这个例子:
|
||||
//
|
||||
// dirPath + "/" ==> 这个的意思是保留前面的文件夹路径,如果不需要就去掉
|
||||
// fileName.substr(0,10) ==> 文件名只取前面10个字符,其他的不要了
|
||||
// + fileExt ==> 把文件的后缀名补上
|
||||
var saveFilePath = dirPath + "/" + fileName.substr(0,10) + fileExt;
|
||||
|
||||
// 返回
|
||||
result["localFilePath"] = saveFilePath;
|
||||
return result;
|
||||
}
|
||||
```
|
||||
效果如下:
|
||||
1)假如网盘源路径是:`/亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi`
|
||||
2)下载后保存本地的路径是:`D:\test\亚马逊书籍合集\亚马逊 kindle ebook 大合集5289册\经济金融 (2)\解密Instagra.mobi`
|
||||
```
|
||||
[1] ----
|
||||
文件ID: 630f0623e67c0a1d2e554b1994a29108d089f0d5
|
||||
文件名: 解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi
|
||||
文件类型: 文件
|
||||
文件路径: /亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi
|
||||
|
||||
插件修改文件下载保存路径为: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi
|
||||
[1] 准备下载: /亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi
|
||||
[1] 将会下载到路径: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi
|
||||
[1] 下载开始
|
||||
|
||||
[1] 下载完成, 保存位置: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi
|
||||
[1] 检验文件有效性成功: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi
|
||||
```
|
||||
## 4.上传文件去掉文件名包含的部分字符
|
||||
例如本地文件夹名称为```[周杰伦]范特西[mp3]```,上传到网盘希望能更改成名称```[周杰伦]范特西```但保持本地文件夹名称不变,因为文件夹有很多不希望每个手动更改,可以这样实现:
|
||||
```js
|
||||
function uploadFilePrepareCallback(context, params) {
|
||||
var result = {
|
||||
"uploadApproved": "yes",
|
||||
"driveFilePath": ""
|
||||
};
|
||||
|
||||
// 去掉网盘保存路径中包含的[mp3]字段
|
||||
var filePath = params["driveFilePath"];
|
||||
filePath = filePath.replace(/\[mp3\]/g, "");
|
||||
result["driveFilePath"] = filePath;
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
## 5.上传文件时过滤指定目录或者文件路径
|
||||
upload命令本身支持exn参数排除上传文件或目录,但是只能指定文件名称,不能指定文件的路径。如果需要排除指定路径则可以通过插件脚本实现,样例如下:
|
||||
```js
|
||||
function uploadFilePrepareCallback(context, params) {
|
||||
//(自行配置)禁止上传的本地【目录列表】,使用绝对路径,可以配置多个路径用逗号分隔
|
||||
var forbiddenUploadFolders = ["/Users/tickstep/Downloads/up/target","/Users/tickstep/Downloads/up/.idea"]
|
||||
//(自行配置)禁止上传的本地【文件列表】,使用绝对路径,可以配置多个路径用逗号分隔
|
||||
var forbiddenUploadFiles = ["/Users/tickstep/Downloads/up/pom.xml"]
|
||||
|
||||
// -------------------- 以下代码不要修改 --------------------
|
||||
var result = {
|
||||
"uploadApproved": "yes",
|
||||
"driveFilePath": ""
|
||||
};
|
||||
|
||||
// 下面的代码都是分隔路径,方便后面使用
|
||||
var filePath = params["localFilePath"];
|
||||
filePath = filePath.replace(/\\/g, "/");
|
||||
// 目录完整路径
|
||||
var dirPath = "";
|
||||
// 文件名,不包括后缀名
|
||||
var fileName = "";
|
||||
// 文件后缀名
|
||||
var fileExt = "";
|
||||
var idx = filePath.lastIndexOf('/');
|
||||
if (idx > 0) {
|
||||
dirPath = filePath.substring(0,idx);
|
||||
fileName = filePath.substring(idx+1,filePath.length);
|
||||
} else {
|
||||
fileName = filePath;
|
||||
}
|
||||
idx = fileName.lastIndexOf(".")
|
||||
if (idx > 0) {
|
||||
fileExt = fileName.substring(idx,fileName.length);
|
||||
fileName = fileName.substring(0, fileName.length-fileExt.length)
|
||||
}
|
||||
|
||||
if (params["localFileType"] == "file") {
|
||||
for (var i = 0; i < forbiddenUploadFiles.length; i++) {
|
||||
if (forbiddenUploadFiles[i].replace(/\\/g, "/") == params["localFilePath"]) {
|
||||
result["uploadApproved"] = "no"; // 禁止文件上传
|
||||
break
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < forbiddenUploadFolders.length; i++) {
|
||||
if (forbiddenUploadFolders[i].replace(/\\/g, "/") == dirPath) {
|
||||
result["uploadApproved"] = "no"; // 禁止文件上传
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (params["localFileType"] == "folder") {
|
||||
for (var i = 0; i < forbiddenUploadFolders.length; i++) {
|
||||
if (forbiddenUploadFolders[i].replace(/\\/g, "/") == params["localFilePath"]) {
|
||||
result["uploadApproved"] = "no"; // 禁止文件上传
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
## 6.下载云盘文件到本地后删除云盘对应的文件
|
||||
```js
|
||||
function downloadFileFinishCallback(context, params) {
|
||||
console.log(params)
|
||||
// 云盘文件成功下载到本地后,删除云盘的文件
|
||||
if (params["downloadResult"] == "success") {
|
||||
if (params["driveFileType"] == "file") {
|
||||
// 文件下载成功,删除该云盘文件
|
||||
var userId = context["userId"]
|
||||
var driveId = params["driveId"]
|
||||
var driveFileId = params["driveFileId"]
|
||||
if (PluginUtil.PanFS.deleteFile(userId, driveId, driveFileId)) {
|
||||
console.println("插件删除云盘文件成功:" + params["driveFilePath"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7.Token刷新失败发送外部通知
|
||||
Token刷新失败发送外部通知,例如Server酱
|
||||
```js
|
||||
function userTokenRefreshFinishCallback(context, params) {
|
||||
var header = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
};
|
||||
try {
|
||||
if (params["result"] === "fail") {
|
||||
// 避免频繁发送
|
||||
var ONE_MINUTE = 60 * 1000;
|
||||
var lastSendEmailTime = PluginUtil.KV.getString("email_last_send_time");
|
||||
if (lastSendEmailTime != "") {
|
||||
if ((Date.now() - lastSendEmailTime) < (10 * ONE_MINUTE)) {
|
||||
console.log("距离上次发送邮件小于10分钟,先不发送了");
|
||||
return
|
||||
}
|
||||
}
|
||||
PluginUtil.KV.putString("email_last_send_time", Date.now());
|
||||
|
||||
var reqData = {
|
||||
"text": "Token刷新失败",
|
||||
"desp": params["message"]
|
||||
};
|
||||
var r = PluginUtil.Http.post(header, "https://sctapi.ftqq.com/xxxxxkeyxxxxxx.send", JSON.stringify(reqData));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e !== "Error") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
或者发送邮件通知
|
||||
```js
|
||||
function userTokenRefreshFinishCallback(context, params) {
|
||||
try {
|
||||
if (params["result"] === "fail") {
|
||||
// 避免频繁发送
|
||||
var ONE_MINUTE = 60 * 1000;
|
||||
var lastSendEmailTime = PluginUtil.KV.getString("email_last_send_time");
|
||||
if (lastSendEmailTime != "") {
|
||||
if ((Date.now() - lastSendEmailTime) < (10 * ONE_MINUTE)) {
|
||||
console.log("距离上次发送邮件小于10分钟,先不发送了");
|
||||
return
|
||||
}
|
||||
}
|
||||
PluginUtil.KV.putString("email_last_send_time", Date.now());
|
||||
|
||||
// 发送通知邮件,请确保你的发送邮箱具备smtp发送权限,没有的需要找邮箱提供商配置开启
|
||||
console.log("发送通知邮件");
|
||||
PluginUtil.Email.sendTextMail("smtp.qq.com:465", "123xxx@qq.com", "pwdxxxxxx", "12545xxx@qq.com", "Token刷新失败了", "Token过期了,骚年快去手动恢复吧");
|
||||
}
|
||||
} catch (e) {
|
||||
if (e !== "Error") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
## 8.每次只下载指定数量的文件
|
||||
每次运行download下载命令,只下载指定数量的文件,只要数量够了其他文件就跳过不再下载,其他文件等下一次运行download命令的时候再下载。
|
||||
```js
|
||||
function downloadFilePrepareCallback(context, params) {
|
||||
var result = {
|
||||
"downloadApproved": "yes",
|
||||
"localFilePath": ""
|
||||
};
|
||||
|
||||
// 这次下载限制的最大文件总数量
|
||||
const maxCountOfDownloadAction = 3;
|
||||
|
||||
// 只处理文件
|
||||
if (params["driveFileType"] != "file") {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取这次下载动作,已经下载的文件数量
|
||||
var keyOfThisDownloadAction = "download:" + params["downloadActionId"];
|
||||
var valueOfThisDownloadAction = PluginUtil.KV.getString(keyOfThisDownloadAction);
|
||||
if (valueOfThisDownloadAction == "") {
|
||||
valueOfThisDownloadAction = "0";
|
||||
}
|
||||
var countOfThisDownloadAction = parseInt(valueOfThisDownloadAction);
|
||||
if (countOfThisDownloadAction >= maxCountOfDownloadAction) {
|
||||
// 下载数量已达到最大,其他文件不再下载
|
||||
result["downloadApproved"] = "no";
|
||||
return result;
|
||||
}
|
||||
|
||||
// 该文件需要下载,记录相关信息
|
||||
var keyOfThisDownloadFile = "file:" + PluginUtil.HashTool.md5Hex(params["driveFilePath"]);
|
||||
var valueOfThisDownloadFile = PluginUtil.KV.getString(keyOfThisDownloadFile);
|
||||
if (valueOfThisDownloadFile == "finish") {
|
||||
// 已经在下载的文件不做处理
|
||||
return result;
|
||||
}
|
||||
PluginUtil.KV.putString(keyOfThisDownloadFile, "downloading");
|
||||
|
||||
// 更新下载总数量
|
||||
countOfThisDownloadAction += 1;
|
||||
PluginUtil.KV.putString(keyOfThisDownloadAction, String(countOfThisDownloadAction));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function downloadFileFinishCallback(context, params) {
|
||||
var keyOfThisDownloadFile = "file:" + PluginUtil.HashTool.md5Hex(params["driveFilePath"]);
|
||||
console.log(keyOfThisDownloadFile)
|
||||
PluginUtil.KV.putString(keyOfThisDownloadFile, "finish");
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>application-identifier</key>
|
||||
<string>com.tickstep.aliyunpan</string>
|
||||
<key>get-task-allow</key>
|
||||
<true/>
|
||||
<key>platform-application</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>com.tickstep.aliyunpan</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,42 @@
|
||||
module github.com/tickstep/aliyunpan
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/GeertJohan/go.incremental v1.0.0
|
||||
github.com/adrg/xdg v0.4.0
|
||||
github.com/deckarep/golang-set v1.8.0
|
||||
github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1
|
||||
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1
|
||||
github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb
|
||||
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce
|
||||
github.com/peterh/liner v1.2.1
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/tickstep/aliyunpan-api v0.2.9
|
||||
github.com/tickstep/bolt v1.3.4
|
||||
github.com/tickstep/library-go v0.1.3
|
||||
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f
|
||||
golang.org/x/sys v0.15.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/boltdb/bolt v1.3.1 // indirect
|
||||
github.com/btcsuite/btcd v0.22.1 // indirect
|
||||
github.com/cpuguy83/go-md2man v1.0.10 // indirect
|
||||
github.com/denisbrodbeck/machineid v1.0.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/russross/blackfriday v1.5.2 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
)
|
||||
|
||||
//replace github.com/boltdb/bolt => github.com/tickstep/bolt v1.3.4
|
||||
//replace github.com/tickstep/bolt => /Users/tickstep/Documents/Workspace/go/projects/bolt
|
||||
//replace github.com/tickstep/library-go => /Users/tickstep/Documents/Workspace/go/projects/library-go
|
||||
//replace github.com/tickstep/aliyunpan-api => /Users/tickstep/Documents/Workspace/go/projects/aliyunpan-api
|
||||
@@ -0,0 +1,146 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
|
||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
|
||||
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.1 h1:CnwP9LM/M9xuRrGSCGeMVs9iv09uMqwsVX7EeIpgV2c=
|
||||
github.com/btcsuite/btcd v0.22.1/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o=
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
|
||||
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4=
|
||||
github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
|
||||
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E=
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
|
||||
github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1 h1:9/4Hyp+A98nqssD7qceXwoLeAn75MZr25IN76dxlfbg=
|
||||
github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1/go.mod h1:TQJQ+ZNyFVvUtUEtCZxBhfWiH7RJqR3EivNmvD6Waik=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e h1:OGunVjqY7y4U4laftpEHv+mvZBlr7UGimJXKEGQtg48=
|
||||
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro=
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb h1:x0yCvYsspui5SAxSRvLd2zFg7PfFijzKdCo7QAtN92I=
|
||||
github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb/go.mod h1:ym0w0flrmBtGvApLDgFLa0sfGJkWxDQqnm0/0ok5w3Y=
|
||||
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce h1:RLmZmfx/K62HKpbwPqtW3tg+V2GgugN/XNNx+uiMH/Y=
|
||||
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/peterh/liner v1.2.1 h1:O4BlKaq/LWu6VRWmol4ByWfzx6MfXc5Op5HETyIy5yg=
|
||||
github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tickstep/aliyunpan-api v0.2.9 h1:0n7BNjmKEi/JYqNLhotBnCQJmJE3OAqiDX28q0Gknxs=
|
||||
github.com/tickstep/aliyunpan-api v0.2.9/go.mod h1:UvQgQYMIgA5k1QsfKIVnEJ2h/HqDXIkAb16oookNc8o=
|
||||
github.com/tickstep/bolt v1.3.4 h1:UN1txRsauOfR8a5mUWm5So0ng233PsIMVR5/+3WowCE=
|
||||
github.com/tickstep/bolt v1.3.4/go.mod h1:Y24RwCywbOsKAyHiVcJ4K2QQfED+q67x7PEOH9OG7Q4=
|
||||
github.com/tickstep/library-go v0.1.3 h1:OUj6nkimTsqhHXh5s+PbmHq5hR0m739Y1tu9QpfA2ng=
|
||||
github.com/tickstep/library-go v0.1.3/go.mod h1:uAHeNOIpoywCzlaeLrWmmRSupn03m9kJVZKOEmuarmA=
|
||||
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f h1:xKDKjIsL76VUyHcA0G4Qe1cIAUB/nrq6Pt8D411bd1g=
|
||||
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f/go.mod h1:qXyCeJubPqsgeiLd3kvHOGHHSrQcNdjZ2ScXIcVZK/I=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,481 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/file/downloader"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
"github.com/tickstep/aliyunpan/internal/taskframework"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CmdAlbum() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "album",
|
||||
Aliases: []string{"abm"},
|
||||
Usage: "共享相册",
|
||||
UsageText: cmder.App().Name + " album",
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
},
|
||||
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "展示共享相簿列表",
|
||||
UsageText: cmder.App().Name + " album list",
|
||||
Description: `
|
||||
示例:
|
||||
展示共享相簿列表
|
||||
aliyunpan album ls
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
RunShareAlbumList()
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "list-file",
|
||||
Aliases: []string{"lf"},
|
||||
Usage: "展示相簿中的文件",
|
||||
UsageText: cmder.App().Name + " album list-file",
|
||||
Description: `
|
||||
展示相簿中文件,同名的相簿只会展示第一个符合条件的
|
||||
示例:
|
||||
|
||||
展示相簿中文件"我的相簿2022"
|
||||
aliyunpan album list-file "我的相簿2022"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
RunShareAlbumListFile(c.Args().Get(0))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "download-file",
|
||||
Aliases: []string{"df"},
|
||||
Usage: "下载相簿中的所有文件到本地",
|
||||
UsageText: cmder.App().Name + " album download-file",
|
||||
Description: `
|
||||
下载相簿中的所有文件
|
||||
示例:
|
||||
|
||||
下载相簿 "我的相簿2022" 里面的所有文件
|
||||
aliyunpan album download-file 我的相簿2022
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
if len(subArgs) == 0 {
|
||||
fmt.Println("请指定下载的相簿名称")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理saveTo
|
||||
var (
|
||||
saveTo string
|
||||
)
|
||||
if c.String("saveto") != "" {
|
||||
saveTo = filepath.Clean(c.String("saveto"))
|
||||
}
|
||||
|
||||
do := &DownloadOptions{
|
||||
IsPrintStatus: false,
|
||||
IsExecutedPermission: false,
|
||||
IsOverwrite: c.Bool("ow"),
|
||||
SaveTo: saveTo,
|
||||
Parallel: 0,
|
||||
SliceParallel: 0,
|
||||
Load: 0,
|
||||
MaxRetry: pandownload.DefaultDownloadMaxRetry,
|
||||
NoCheck: false,
|
||||
ShowProgress: !c.Bool("np"),
|
||||
DriveId: parseDriveId(c),
|
||||
ExcludeNames: []string{},
|
||||
}
|
||||
|
||||
RunShareAlbumDownloadFile(c.Args(), do)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "ow",
|
||||
Usage: "overwrite, 覆盖已存在的文件",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "saveto",
|
||||
Usage: "将下载的文件直接保存到指定的目录",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "np",
|
||||
Usage: "no progress 不展示下载进度条",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunShareAlbumList() {
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().OpenapiPanClient().ShareAlbumListGetAll()
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "ALBUM_ID", "名称", "更新日期", "创建日期"})
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT})
|
||||
for k, record := range records {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), record.AlbumId, record.Name, record.UpdatedAtStr(), record.CreatedAtStr()})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
|
||||
func getShareAlbumFromName(activeUser *config.PanUser, name string) *aliyunpan.AlbumEntity {
|
||||
records, err := activeUser.PanClient().OpenapiPanClient().ShareAlbumListGetAll()
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
if name == record.Name {
|
||||
return record
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RunShareAlbumListFile(name string) {
|
||||
if len(name) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
record := getShareAlbumFromName(activeUser, name)
|
||||
if record == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fileList, er := activeUser.PanClient().OpenapiPanClient().ShareAlbumListFileGetAll(&aliyunpan.ShareAlbumListFileParam{
|
||||
AlbumId: record.AlbumId,
|
||||
Limit: 100,
|
||||
})
|
||||
if er != nil {
|
||||
fmt.Printf("获取相簿文件列表失败:%s\n", er)
|
||||
return
|
||||
}
|
||||
renderTable(opLs, false, "", fileList)
|
||||
}
|
||||
|
||||
func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
if len(albumNames) == 0 {
|
||||
fmt.Printf("请指定相簿名称\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
if options == nil {
|
||||
options = &DownloadOptions{}
|
||||
}
|
||||
|
||||
if options.MaxRetry < 0 {
|
||||
options.MaxRetry = pandownload.DefaultDownloadMaxRetry
|
||||
}
|
||||
options.IsExecutedPermission = false
|
||||
|
||||
// 设置下载配置
|
||||
cfg := &downloader.Config{
|
||||
Mode: transfer.RangeGenMode_BlockSize,
|
||||
CacheSize: config.Config.CacheSize,
|
||||
BlockSize: MaxDownloadRangeSize,
|
||||
MaxRate: config.Config.MaxDownloadRate,
|
||||
InstanceStateStorageFormat: downloader.InstanceStateStorageFormatJSON,
|
||||
ShowProgress: options.ShowProgress,
|
||||
ExcludeNames: options.ExcludeNames,
|
||||
}
|
||||
if cfg.CacheSize == 0 {
|
||||
cfg.CacheSize = int(DownloadCacheSize)
|
||||
}
|
||||
|
||||
// 设置下载最大并发量
|
||||
if options.Parallel < 1 {
|
||||
options.Parallel = config.Config.MaxDownloadParallel
|
||||
if options.Parallel == 0 {
|
||||
options.Parallel = config.DefaultFileDownloadParallelNum
|
||||
}
|
||||
}
|
||||
if options.Parallel > config.MaxFileDownloadParallelNum {
|
||||
options.Parallel = config.MaxFileDownloadParallelNum
|
||||
}
|
||||
// 设置单个文件下载分片线程数
|
||||
if options.SliceParallel < 1 { // 用户没有主动设置,则使用下面自动配置的策略
|
||||
if options.Parallel > 1 {
|
||||
// 如果是多文件下载,则一个文件只能同时使用1个线程
|
||||
options.SliceParallel = 1
|
||||
} else {
|
||||
// 如果是单文件下载,则可以使用3个最大线程
|
||||
options.SliceParallel = 3
|
||||
}
|
||||
}
|
||||
|
||||
// 保存文件的本地根文件夹
|
||||
originSaveRootPath := ""
|
||||
if options.SaveTo != "" {
|
||||
originSaveRootPath = options.SaveTo
|
||||
} else {
|
||||
// 使用默认的保存路径
|
||||
originSaveRootPath = GetActiveUser().GetSavePath("")
|
||||
}
|
||||
fi, err1 := os.Stat(originSaveRootPath)
|
||||
if err1 != nil && !os.IsExist(err1) {
|
||||
os.MkdirAll(originSaveRootPath, 0777) // 首先在本地创建目录
|
||||
} else {
|
||||
if !fi.IsDir() {
|
||||
fmt.Println("本地保存路径不是文件夹,请删除或者创建对应的文件夹:", originSaveRootPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 下载缓存为: %s\n\n", options.Parallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
||||
|
||||
var (
|
||||
panClient = activeUser.PanClient()
|
||||
)
|
||||
cfg.MaxParallel = options.Parallel
|
||||
cfg.SliceParallel = options.SliceParallel
|
||||
|
||||
var (
|
||||
executor = taskframework.TaskExecutor{
|
||||
IsFailedDeque: true, // 统计失败的列表
|
||||
}
|
||||
statistic = &pandownload.DownloadStatistic{}
|
||||
)
|
||||
// 配置执行器任务并发数,即同时下载文件并发数
|
||||
executor.SetParallel(cfg.MaxParallel)
|
||||
|
||||
// 全局速度统计
|
||||
globalSpeedsStat := &speeds.Speeds{}
|
||||
|
||||
// 处理队列
|
||||
allShareAlbumList, err := activeUser.PanClient().OpenapiPanClient().ShareAlbumListGetAll()
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
for k := range albumNames {
|
||||
var record *aliyunpan.AlbumEntity
|
||||
for _, album := range allShareAlbumList {
|
||||
if album.Name == albumNames[k] {
|
||||
record = album
|
||||
break
|
||||
}
|
||||
}
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
// 获取相簿下的所有文件
|
||||
fileList, er := activeUser.PanClient().OpenapiPanClient().ShareAlbumListFileGetAll(&aliyunpan.ShareAlbumListFileParam{
|
||||
AlbumId: record.AlbumId,
|
||||
Limit: 100,
|
||||
})
|
||||
if er != nil {
|
||||
fmt.Printf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k])
|
||||
continue
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
fmt.Printf("相簿里面没有文件: %s\n", albumNames[k])
|
||||
continue
|
||||
}
|
||||
|
||||
// 相薄文件是没有子文件夹的,这个fileList就是该相册里面的全部文件了,所以不需要再遍历子文件夹了
|
||||
idx := 0
|
||||
for {
|
||||
if idx >= len(fileList) {
|
||||
break
|
||||
}
|
||||
f := fileList.Item(idx)
|
||||
idx += 1
|
||||
// 处理实况照片
|
||||
if f.IsAlbumLivePhotoFile() {
|
||||
// 如果是实况照片,则需要下载图片+视频两个文件
|
||||
// 获取下载链接
|
||||
durl, apierr := activeUser.PanClient().OpenapiPanClient().ShareAlbumGetFileDownloadUrl(&aliyunpan.ShareAlbumGetFileUrlParam{
|
||||
AlbumId: f.AlbumId,
|
||||
DriveId: f.DriveId,
|
||||
FileId: f.FileId,
|
||||
})
|
||||
if apierr != nil {
|
||||
logger.Verbosef("ERROR: get album file download url error: %s\n", f.FileId)
|
||||
fmt.Printf("\n下载照片失败: %s\n", f.FileName)
|
||||
continue
|
||||
}
|
||||
if durl.StreamsUrl != nil { // 实况图片(照片+视频)下载链接
|
||||
// 照片文件
|
||||
photoFile := cloneFileEntity(f)
|
||||
photoFileSize := int64(0)
|
||||
if durl.StreamsUrl.Heic != "" {
|
||||
beforeStr, _ := strings.CutSuffix(f.FileName, ".livp")
|
||||
photoFile.FileName = beforeStr + ".HEIC"
|
||||
photoFile.FileExtension = "heic"
|
||||
photoFileSize = getHttpDownloadFileSize(durl.StreamsUrl.Heic)
|
||||
} else if durl.StreamsUrl.Jpeg != "" {
|
||||
beforeStr, _ := strings.CutSuffix(f.FileName, ".livp")
|
||||
photoFile.FileName = beforeStr + ".JPG"
|
||||
photoFile.FileExtension = "jpg"
|
||||
photoFileSize = getHttpDownloadFileSize(durl.StreamsUrl.Jpeg)
|
||||
}
|
||||
photoFile.FileSize = photoFileSize
|
||||
fileList = append(fileList, photoFile)
|
||||
|
||||
// 视频文件
|
||||
videoFile := cloneFileEntity(f)
|
||||
videoFile.FileSize = f.FileSize - photoFileSize
|
||||
if durl.StreamsUrl.Mov != "" {
|
||||
beforeStr, _ := strings.CutSuffix(f.FileName, ".livp")
|
||||
videoFile.FileName = beforeStr + ".MOV"
|
||||
videoFile.FileExtension = "mov"
|
||||
}
|
||||
fileList = append(fileList, videoFile)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 补全虚拟网盘路径,规则:/共享相册/<相簿名称>/文件名称
|
||||
f.Path = "/共享相册/" + albumNames[k] + "/" + f.FileName
|
||||
|
||||
// 生成下载项
|
||||
newCfg := *cfg
|
||||
unit := pandownload.DownloadTaskUnit{
|
||||
Cfg: &newCfg, // 复制一份新的cfg
|
||||
PanClient: panClient,
|
||||
VerbosePrinter: panCommandVerbose,
|
||||
ParentTaskExecutor: &executor,
|
||||
DownloadStatistic: statistic,
|
||||
IsPrintStatus: options.IsPrintStatus,
|
||||
IsExecutedPermission: options.IsExecutedPermission,
|
||||
IsOverwrite: options.IsOverwrite,
|
||||
NoCheck: options.NoCheck,
|
||||
FilePanSource: global.AlbumSource,
|
||||
FilePanPath: f.Path,
|
||||
DriveId: f.DriveId, // 一个相簿的文件会来自多个网盘(资源库/备份盘)
|
||||
GlobalSpeedsStat: globalSpeedsStat,
|
||||
FileRecorder: nil,
|
||||
}
|
||||
|
||||
// 设置相簿文件信息
|
||||
unit.SetFileInfo(global.AlbumSource, f)
|
||||
|
||||
// 设置储存的路径
|
||||
if options.SaveTo != "" {
|
||||
unit.OriginSaveRootPath = options.SaveTo
|
||||
unit.SavePath = filepath.Join(options.SaveTo, f.Path)
|
||||
} else {
|
||||
// 使用默认的保存路径
|
||||
unit.OriginSaveRootPath = GetActiveUser().GetSavePath("")
|
||||
unit.SavePath = GetActiveUser().GetSavePath(f.Path)
|
||||
}
|
||||
info := executor.Append(&unit, options.MaxRetry)
|
||||
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始计时
|
||||
statistic.StartTimer()
|
||||
|
||||
// 开始执行
|
||||
executor.Execute()
|
||||
|
||||
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
|
||||
|
||||
// 输出失败的文件列表
|
||||
failedList := executor.FailedDeque()
|
||||
if failedList.Size() != 0 {
|
||||
fmt.Printf("以下文件下载失败: \n")
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
for e := failedList.Shift(); e != nil; e = failedList.Shift() {
|
||||
item := e.(*taskframework.TaskInfoItem)
|
||||
tb.Append([]string{item.Info.Id(), item.Unit.(*pandownload.DownloadTaskUnit).FilePanPath})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
}
|
||||
|
||||
func cloneFileEntity(entity *aliyunpan.FileEntity) *aliyunpan.FileEntity {
|
||||
data, err := json.Marshal(entity) // 序列化原始对象
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
newEntity := &aliyunpan.FileEntity{}
|
||||
err = json.Unmarshal(data, newEntity) // 反序列化到新的对象中
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return newEntity
|
||||
}
|
||||
|
||||
func getHttpDownloadFileSize(fileUrl string) int64 {
|
||||
client := requester.NewHTTPClient()
|
||||
client.SetKeepAlive(true)
|
||||
client.SetTimeout(10 * time.Minute)
|
||||
// header
|
||||
headers := map[string]string{
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
"referer": "https://www.aliyundrive.com/",
|
||||
}
|
||||
resp, err := client.Req("GET", fileUrl, nil, headers)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
fileSize := resp.ContentLength
|
||||
return fileSize
|
||||
}
|
||||
@@ -0,0 +1,787 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/file/downloader"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
|
||||
"github.com/tickstep/aliyunpan/internal/taskframework"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
AlbumFileCategoryOption string
|
||||
)
|
||||
|
||||
var (
|
||||
ImageOnlyOption AlbumFileCategoryOption = "image"
|
||||
VideoOnlyOption AlbumFileCategoryOption = "video"
|
||||
ImageVideoOnlyOption AlbumFileCategoryOption = "image_video"
|
||||
AllFileOption AlbumFileCategoryOption = "none"
|
||||
)
|
||||
|
||||
func CmdAlbumWeb() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "albumw",
|
||||
Usage: "个人相册",
|
||||
UsageText: cmder.App().Name + " albumw",
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
},
|
||||
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "展示相簿列表",
|
||||
UsageText: cmder.App().Name + " album list",
|
||||
Description: `
|
||||
示例:
|
||||
|
||||
展示相簿列表
|
||||
aliyunpan albumw ls
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumList()
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "new",
|
||||
Aliases: []string{""},
|
||||
Usage: "创建相簿",
|
||||
UsageText: cmder.App().Name + " album new",
|
||||
Description: `
|
||||
示例:
|
||||
|
||||
新建相簿,名称为:我的相簿2022
|
||||
aliyunpan albumw new "我的相簿2022"
|
||||
|
||||
新建相簿,名称为:我的相簿2022,描述为:存放2022所有文件
|
||||
aliyunpan albumw new "我的相簿2022" "存放2022所有文件"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumCreate(c.Args().Get(0), c.Args().Get(1))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "rm",
|
||||
Aliases: []string{""},
|
||||
Usage: "删除相簿",
|
||||
UsageText: cmder.App().Name + " album rm",
|
||||
Description: `
|
||||
删除相簿,同名的相簿只会删除第一个符合条件的
|
||||
示例:
|
||||
|
||||
删除名称为"我的相簿2022"的相簿
|
||||
aliyunpan albumw rm "我的相簿2022"
|
||||
|
||||
删除名称为"我的相簿2022-1" 和 "我的相簿2022-2"的相簿
|
||||
aliyunpan albumw rm "我的相簿2022-1" "我的相簿2022-2"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumDelete(c.Args())
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "rename",
|
||||
Aliases: []string{""},
|
||||
Usage: "重命名相簿",
|
||||
UsageText: cmder.App().Name + " album rename",
|
||||
Description: `
|
||||
重命名相簿,同名的相簿只会修改第一个符合条件的
|
||||
示例:
|
||||
|
||||
重命名相簿"我的相簿2022"为新的名称"我的相簿2022-new"
|
||||
aliyunpan albumw rename "我的相簿2022" "我的相簿2022-new"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumRename(c.Args().Get(0), c.Args().Get(1))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "list-file",
|
||||
Aliases: []string{"lf"},
|
||||
Usage: "展示相簿中的文件",
|
||||
UsageText: cmder.App().Name + " album list-file",
|
||||
Description: `
|
||||
展示相簿中文件,同名的相簿只会展示第一个符合条件的
|
||||
示例:
|
||||
|
||||
展示相簿中文件"我的相簿2022"
|
||||
aliyunpan albumw list-file "我的相簿2022"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumListFile(c.Args().Get(0))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "rm-file",
|
||||
Aliases: []string{"rf"},
|
||||
Usage: "移除相簿中的文件",
|
||||
UsageText: cmder.App().Name + " album rm-file",
|
||||
Description: `
|
||||
移除相簿中的文件,同名的相簿只会移除第一个符合条件的
|
||||
示例:
|
||||
|
||||
移除相簿 "我的相簿2022" 中的文件 1.png 2.png
|
||||
aliyunpan albumw rm-file 我的相簿2022 1.png 2.png
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
if len(subArgs) < 2 {
|
||||
fmt.Println("请指定移除的文件")
|
||||
return nil
|
||||
}
|
||||
RunAlbumRmFile(subArgs[0], subArgs[1:])
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "add-file",
|
||||
Aliases: []string{"af"},
|
||||
Usage: "增加(文件/相册)网盘文件到相簿中",
|
||||
UsageText: cmder.App().Name + " album add-file",
|
||||
Description: `
|
||||
增加文件到相簿中
|
||||
示例:
|
||||
|
||||
增加当前目录下的 1.png 2.png 文件到相簿 "我的相簿2022" 中
|
||||
aliyunpan albumw add-file 我的相簿2022 1.png 2.png
|
||||
|
||||
增加当前目录下的 myFolder 文件夹下所有文件到相簿 "我的相簿2022" 中
|
||||
aliyunpan albumw add-file 我的相簿2022 myFolder
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
if len(subArgs) < 2 {
|
||||
fmt.Println("请指定增加的文件")
|
||||
return nil
|
||||
}
|
||||
RunAlbumAddFile(subArgs[0], subArgs[1:], ImageVideoOnlyOption)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "download-file",
|
||||
Aliases: []string{"df"},
|
||||
Usage: "下载相簿中的所有文件到本地",
|
||||
UsageText: cmder.App().Name + " album download-file",
|
||||
Description: `
|
||||
下载相簿中的所有文件
|
||||
示例:
|
||||
|
||||
下载相簿 "我的相簿2022" 里面的所有文件
|
||||
aliyunpan albumw download-file 我的相簿2022
|
||||
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
if len(subArgs) == 0 {
|
||||
fmt.Println("请指定下载的相簿名称")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理saveTo
|
||||
var (
|
||||
saveTo string
|
||||
)
|
||||
if c.String("saveto") != "" {
|
||||
saveTo = filepath.Clean(c.String("saveto"))
|
||||
}
|
||||
|
||||
do := &DownloadOptions{
|
||||
IsPrintStatus: false,
|
||||
IsExecutedPermission: false,
|
||||
IsOverwrite: c.Bool("ow"),
|
||||
SaveTo: saveTo,
|
||||
Parallel: 0,
|
||||
Load: 0,
|
||||
MaxRetry: pandownload.DefaultDownloadMaxRetry,
|
||||
NoCheck: false,
|
||||
ShowProgress: !c.Bool("np"),
|
||||
DriveId: parseDriveId(c),
|
||||
ExcludeNames: []string{},
|
||||
}
|
||||
|
||||
RunAlbumDownloadFile(c.Args(), do)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "ow",
|
||||
Usage: "overwrite, 覆盖已存在的文件",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "saveto",
|
||||
Usage: "将下载的文件直接保存到指定的目录",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "np",
|
||||
Usage: "no progress 不展示下载进度条",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunAlbumList() {
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{})
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "ALBUM_ID", "名称", "文件数量", "创建日期", "修改日期"})
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT})
|
||||
for k, record := range records {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), record.AlbumId, record.Name, strconv.Itoa(record.FileCount),
|
||||
record.CreatedAtStr(), record.UpdatedAtStr()})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
|
||||
func RunAlbumCreate(name, description string) {
|
||||
if name == "" {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
_, err := activeUser.PanClient().WebapiPanClient().AlbumCreate(&aliyunpan_web.AlbumCreateParam{
|
||||
Name: name,
|
||||
Description: description,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("创建相簿失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("创建相簿成功: %s\n", name)
|
||||
}
|
||||
|
||||
func RunAlbumDelete(nameList []string) {
|
||||
if len(nameList) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{})
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
for i, name := range nameList {
|
||||
if name == record.Name {
|
||||
nameList = append(nameList[:i], nameList[i+1:]...)
|
||||
_, err := activeUser.PanClient().WebapiPanClient().AlbumDelete(&aliyunpan_web.AlbumDeleteParam{
|
||||
AlbumId: record.AlbumId,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("删除相簿失败: %s\n", name)
|
||||
return
|
||||
} else {
|
||||
fmt.Printf("删除相簿成功: %s\n", name)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getAlbumFromName(activeUser *config.PanUser, name string) *aliyunpan.AlbumEntity {
|
||||
records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{})
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
if name == record.Name {
|
||||
return record
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RunAlbumRename(name, newName string) {
|
||||
if len(name) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
if len(newName) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
record := getAlbumFromName(activeUser, name)
|
||||
if record == nil {
|
||||
return
|
||||
}
|
||||
_, err := activeUser.PanClient().WebapiPanClient().AlbumEdit(&aliyunpan_web.AlbumEditParam{
|
||||
AlbumId: record.AlbumId,
|
||||
Description: record.Description,
|
||||
Name: newName,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("重命名相簿失败: %s\n", name)
|
||||
return
|
||||
} else {
|
||||
fmt.Printf("重命名相簿成功: %s -> %s\n", name, newName)
|
||||
}
|
||||
}
|
||||
|
||||
func RunAlbumListFile(name string) {
|
||||
if len(name) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
record := getAlbumFromName(activeUser, name)
|
||||
if record == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{
|
||||
AlbumId: record.AlbumId,
|
||||
})
|
||||
if er != nil {
|
||||
fmt.Printf("获取相簿文件列表失败:%s\n", er)
|
||||
return
|
||||
}
|
||||
renderTable(opLs, false, "", fileList)
|
||||
}
|
||||
|
||||
func RunAlbumRmFile(name string, nameList []string) {
|
||||
if len(name) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
if len(nameList) == 0 {
|
||||
fmt.Printf("指定文件不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
album := getAlbumFromName(activeUser, name)
|
||||
if album == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{
|
||||
AlbumId: album.AlbumId,
|
||||
})
|
||||
if er != nil {
|
||||
fmt.Printf("获取相簿文件列表失败:%s\n", er)
|
||||
return
|
||||
}
|
||||
param := &aliyunpan_web.AlbumDeleteFileParam{
|
||||
AlbumId: album.AlbumId,
|
||||
DriveFileList: []aliyunpan.FileBatchActionParam{},
|
||||
}
|
||||
for _, file := range fileList {
|
||||
if len(nameList) == 0 {
|
||||
break
|
||||
}
|
||||
for i, name := range nameList {
|
||||
if name == file.FileName {
|
||||
nameList = append(nameList[:i], nameList[i+1:]...)
|
||||
param.AddFileItem(file.DriveId, file.FileId)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1-500 范围
|
||||
if len(param.DriveFileList) == 0 {
|
||||
fmt.Printf("没有符合的文件\n")
|
||||
return
|
||||
}
|
||||
// delete file
|
||||
_, e := activeUser.PanClient().WebapiPanClient().AlbumDeleteFile(param)
|
||||
if e != nil {
|
||||
fmt.Printf("删除相簿文件失败:%s\n", e)
|
||||
return
|
||||
}
|
||||
fmt.Printf("删除相簿文件成功:%s\n", name)
|
||||
}
|
||||
|
||||
// RunAlbumAddFile 增加网盘文件到相簿
|
||||
func RunAlbumAddFile(albumName string, filePathList []string, filterOption AlbumFileCategoryOption) {
|
||||
activeUser := GetActiveUser()
|
||||
|
||||
if albumName == "" {
|
||||
fmt.Printf("必须指定相簿名称\n")
|
||||
return
|
||||
}
|
||||
album := getAlbumFromName(activeUser, albumName)
|
||||
if album == nil {
|
||||
fmt.Printf("相簿不存在\n")
|
||||
return
|
||||
}
|
||||
|
||||
paths, err := makePathAbsolute(activeUser.ActiveDriveId, filePathList...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if len(paths) == 0 {
|
||||
fmt.Printf("没有有效的文件\n")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("正在获取增加的文件信息,该操作可能会非常耗费时间,请耐心等待...\n")
|
||||
param := &aliyunpan_web.AlbumAddFileParam{
|
||||
AlbumId: album.AlbumId,
|
||||
DriveFileList: []aliyunpan.FileBatchActionParam{},
|
||||
}
|
||||
for k := range paths {
|
||||
filePath := paths[k]
|
||||
fileInfo, apierr := activeUser.PanClient().WebapiPanClient().FileInfoByPath(activeUser.ActiveDriveId, filePath)
|
||||
if apierr != nil {
|
||||
fmt.Printf("获取文件信息失败: %s\n", filePath)
|
||||
continue
|
||||
}
|
||||
if fileInfo.IsFile() {
|
||||
// file
|
||||
if isFileMatchCondition(fileInfo, filterOption) {
|
||||
param.AddFileItem(fileInfo.DriveId, fileInfo.FileId)
|
||||
}
|
||||
} else {
|
||||
// folder
|
||||
activeUser.PanClient().WebapiPanClient().FilesDirectoriesRecurseList(activeUser.ActiveDriveId, fileInfo.Path, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
|
||||
if apiError != nil {
|
||||
logger.Verbosef("%s\n", apiError)
|
||||
return true
|
||||
}
|
||||
if !fd.IsFolder() {
|
||||
if isFileMatchCondition(fd, filterOption) {
|
||||
param.AddFileItem(fd.DriveId, fd.FileId)
|
||||
}
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
return true
|
||||
})
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
if len(param.DriveFileList) == 0 {
|
||||
fmt.Printf("没有符合的文件\n")
|
||||
return
|
||||
}
|
||||
// add file
|
||||
_, e := activeUser.PanClient().WebapiPanClient().AlbumAddFile(param)
|
||||
if e != nil {
|
||||
fmt.Printf("增加相簿文件失败:%s\n", e)
|
||||
return
|
||||
}
|
||||
fmt.Printf("增加相簿文件成功:%s\n", albumName)
|
||||
}
|
||||
|
||||
func isFileMatchCondition(fileInfo *aliyunpan.FileEntity, filterOption AlbumFileCategoryOption) bool {
|
||||
if fileInfo == nil {
|
||||
return false
|
||||
}
|
||||
if filterOption == ImageOnlyOption {
|
||||
return fileInfo.Category == "image"
|
||||
} else if filterOption == VideoOnlyOption {
|
||||
return fileInfo.Category == "video"
|
||||
} else if filterOption == ImageVideoOnlyOption {
|
||||
return fileInfo.Category == "image" || fileInfo.Category == "video"
|
||||
} else if filterOption == AllFileOption {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
if len(albumNames) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
activeUser.PanClient().WebapiPanClient().EnableCache()
|
||||
activeUser.PanClient().WebapiPanClient().ClearCache()
|
||||
defer activeUser.PanClient().WebapiPanClient().DisableCache()
|
||||
// pan token expired checker
|
||||
continueFlag := int32(0)
|
||||
atomic.StoreInt32(&continueFlag, 0)
|
||||
defer func() {
|
||||
atomic.StoreInt32(&continueFlag, 1)
|
||||
}()
|
||||
go func(flag *int32) {
|
||||
for atomic.LoadInt32(flag) == 0 {
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
re := RefreshWebTokenInNeed(activeUser)
|
||||
if re.Code == panlogin.SUCCESS {
|
||||
logger.Verboseln("update access token for download task")
|
||||
}
|
||||
}
|
||||
}(&continueFlag)
|
||||
|
||||
if options == nil {
|
||||
options = &DownloadOptions{}
|
||||
}
|
||||
|
||||
if options.MaxRetry < 0 {
|
||||
options.MaxRetry = pandownload.DefaultDownloadMaxRetry
|
||||
}
|
||||
options.IsExecutedPermission = false
|
||||
|
||||
// 设置下载配置
|
||||
cfg := &downloader.Config{
|
||||
Mode: transfer.RangeGenMode_BlockSize,
|
||||
CacheSize: config.Config.CacheSize,
|
||||
BlockSize: MaxDownloadRangeSize,
|
||||
MaxRate: config.Config.MaxDownloadRate,
|
||||
InstanceStateStorageFormat: downloader.InstanceStateStorageFormatJSON,
|
||||
ShowProgress: options.ShowProgress,
|
||||
ExcludeNames: options.ExcludeNames,
|
||||
}
|
||||
if cfg.CacheSize == 0 {
|
||||
cfg.CacheSize = int(DownloadCacheSize)
|
||||
}
|
||||
|
||||
// 设置下载最大并发量
|
||||
if options.Parallel < 1 {
|
||||
options.Parallel = config.Config.MaxDownloadParallel
|
||||
if options.Parallel == 0 {
|
||||
options.Parallel = config.DefaultFileDownloadParallelNum
|
||||
}
|
||||
}
|
||||
if options.Parallel > config.MaxFileDownloadParallelNum {
|
||||
options.Parallel = config.MaxFileDownloadParallelNum
|
||||
}
|
||||
|
||||
// 保存文件的本地根文件夹
|
||||
originSaveRootPath := ""
|
||||
if options.SaveTo != "" {
|
||||
originSaveRootPath = options.SaveTo
|
||||
} else {
|
||||
// 使用默认的保存路径
|
||||
originSaveRootPath = GetActiveUser().GetSavePath("")
|
||||
}
|
||||
fi, err1 := os.Stat(originSaveRootPath)
|
||||
if err1 != nil && !os.IsExist(err1) {
|
||||
os.MkdirAll(originSaveRootPath, 0777) // 首先在本地创建目录
|
||||
} else {
|
||||
if !fi.IsDir() {
|
||||
fmt.Println("本地保存路径不是文件夹,请删除或者创建对应的文件夹:", originSaveRootPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 下载缓存为: %s\n\n", options.Parallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
||||
|
||||
var (
|
||||
panClient = activeUser.PanClient()
|
||||
)
|
||||
cfg.MaxParallel = options.Parallel
|
||||
|
||||
var (
|
||||
executor = taskframework.TaskExecutor{
|
||||
IsFailedDeque: true, // 统计失败的列表
|
||||
}
|
||||
statistic = &pandownload.DownloadStatistic{}
|
||||
)
|
||||
// 配置执行器任务并发数,即同时下载文件并发数
|
||||
executor.SetParallel(cfg.MaxParallel)
|
||||
|
||||
// 全局速度统计
|
||||
globalSpeedsStat := &speeds.Speeds{}
|
||||
|
||||
// 处理队列
|
||||
for k := range albumNames {
|
||||
record := getAlbumFromName(activeUser, albumNames[k])
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
// 获取相簿下的所有文件
|
||||
fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{
|
||||
AlbumId: record.AlbumId,
|
||||
})
|
||||
if er != nil {
|
||||
fmt.Printf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k])
|
||||
continue
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
fmt.Printf("相簿里面没有文件: %s\n", albumNames[k])
|
||||
continue
|
||||
}
|
||||
for _, f := range fileList {
|
||||
// 补全虚拟网盘路径,规则:/<相簿名称>/文件名称
|
||||
f.Path = "/" + albumNames[k] + "/" + f.FileName
|
||||
|
||||
// 生成下载项
|
||||
newCfg := *cfg
|
||||
unit := pandownload.DownloadTaskUnit{
|
||||
Cfg: &newCfg, // 复制一份新的cfg
|
||||
PanClient: panClient,
|
||||
VerbosePrinter: panCommandVerbose,
|
||||
ParentTaskExecutor: &executor,
|
||||
DownloadStatistic: statistic,
|
||||
IsPrintStatus: options.IsPrintStatus,
|
||||
IsExecutedPermission: options.IsExecutedPermission,
|
||||
IsOverwrite: options.IsOverwrite,
|
||||
NoCheck: options.NoCheck,
|
||||
FilePanPath: f.Path,
|
||||
DriveId: f.DriveId, // 必须使用文件的DriveId,因为一个相簿的文件会来自多个网盘(资源库/备份盘)
|
||||
GlobalSpeedsStat: globalSpeedsStat,
|
||||
FileRecorder: nil,
|
||||
}
|
||||
|
||||
// TODO: 相册下载需要重构
|
||||
// 设置相簿文件信息
|
||||
//unit.SetFileInfo(pandownload.AlbumFileSource, f)
|
||||
|
||||
// 设置储存的路径
|
||||
if options.SaveTo != "" {
|
||||
unit.OriginSaveRootPath = options.SaveTo
|
||||
unit.SavePath = filepath.Join(options.SaveTo, f.Path)
|
||||
} else {
|
||||
// 使用默认的保存路径
|
||||
unit.OriginSaveRootPath = GetActiveUser().GetSavePath("")
|
||||
unit.SavePath = GetActiveUser().GetSavePath(f.Path)
|
||||
}
|
||||
info := executor.Append(&unit, options.MaxRetry)
|
||||
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始计时
|
||||
statistic.StartTimer()
|
||||
|
||||
// 开始执行
|
||||
executor.Execute()
|
||||
|
||||
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
|
||||
|
||||
// 输出失败的文件列表
|
||||
failedList := executor.FailedDeque()
|
||||
if failedList.Size() != 0 {
|
||||
fmt.Printf("以下文件下载失败: \n")
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
for e := failedList.Shift(); e != nil; e = failedList.Shift() {
|
||||
item := e.(*taskframework.TaskInfoItem)
|
||||
tb.Append([]string{item.Info.Id(), item.Unit.(*pandownload.DownloadTaskUnit).FilePanPath})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func CmdCd() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "cd",
|
||||
Category: "阿里云盘",
|
||||
Usage: "切换工作目录",
|
||||
Description: `
|
||||
aliyunpan cd <目录, 绝对路径或相对路径>
|
||||
|
||||
示例:
|
||||
|
||||
切换 /我的资源 工作目录:
|
||||
aliyunpan cd /我的资源
|
||||
|
||||
切换 /我的资源 工作目录,使用通配符:
|
||||
aliyunpan cd /我的*
|
||||
|
||||
切换上级目录:
|
||||
aliyunpan cd ..
|
||||
|
||||
切换根目录:
|
||||
aliyunpan cd /
|
||||
`,
|
||||
Before: ReloadConfigFunc,
|
||||
After: SaveConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() == 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
RunChangeDirectory(parseDriveId(c), c.Args().Get(0))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CmdPwd() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "pwd",
|
||||
Usage: "输出工作目录",
|
||||
UsageText: cmder.App().Name + " pwd",
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
activeUser := config.Config.ActiveUser()
|
||||
if activeUser.IsFileDriveActive() {
|
||||
fmt.Println(activeUser.Workdir)
|
||||
} else if activeUser.IsResourceDriveActive() {
|
||||
fmt.Println(activeUser.ResourceWorkdir)
|
||||
} else if activeUser.IsAlbumDriveActive() {
|
||||
fmt.Println(activeUser.AlbumWorkdir)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunChangeDirectory(driveId, targetPath string) {
|
||||
user := config.Config.ActiveUser()
|
||||
targetPath = user.PathJoin(driveId, targetPath)
|
||||
|
||||
// 获取目标路径文件信息
|
||||
targetPathInfo, err := user.PanClient().OpenapiPanClient().FileInfoByPath(driveId, targetPath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 适配通配符路径获取目标文件信息(弃用,容易触发风控)
|
||||
//files, err := matchPathByShellPattern(driveId, targetPath)
|
||||
//if err != nil {
|
||||
// fmt.Println(err)
|
||||
// return
|
||||
//}
|
||||
//
|
||||
//var targetPathInfo *aliyunpan.FileEntity
|
||||
//if len(files) == 1 {
|
||||
// targetPathInfo = files[0]
|
||||
//} else {
|
||||
// for _, f := range files {
|
||||
// if f.IsFolder() {
|
||||
// targetPathInfo = f
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
if targetPathInfo == nil {
|
||||
fmt.Println("路径不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if !targetPathInfo.IsFolder() {
|
||||
fmt.Printf("错误: %s 不是一个目录 (文件夹)\n", targetPathInfo.Path)
|
||||
return
|
||||
}
|
||||
|
||||
if user.IsFileDriveActive() {
|
||||
user.Workdir = targetPathInfo.Path
|
||||
user.WorkdirFileEntity = *targetPathInfo
|
||||
} else if user.IsResourceDriveActive() {
|
||||
user.ResourceWorkdir = targetPathInfo.Path
|
||||
user.ResourceWorkdirFileEntity = *targetPathInfo
|
||||
} else if user.IsAlbumDriveActive() {
|
||||
user.AlbumWorkdir = targetPathInfo.Path
|
||||
user.AlbumWorkdirFileEntity = *targetPathInfo
|
||||
}
|
||||
|
||||
fmt.Printf("改变工作目录: %s\n", targetPathInfo.Path)
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdutil"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/library/crypto"
|
||||
"github.com/tickstep/library-go/getip"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
type ()
|
||||
|
||||
const (
|
||||
cryptoDescription = `
|
||||
可用的方法 <method>:
|
||||
aes-128-ctr, aes-192-ctr, aes-256-ctr,
|
||||
aes-128-cfb, aes-192-cfb, aes-256-cfb,
|
||||
aes-128-ofb, aes-192-ofb, aes-256-ofb.
|
||||
|
||||
密钥 <key>:
|
||||
aes-128 对应key长度为16, aes-192 对应key长度为24, aes-256 对应key长度为32,
|
||||
如果key长度不符合, 则自动修剪key, 舍弃超出长度的部分, 长度不足的部分用'\0'填充.
|
||||
|
||||
GZIP <disable-gzip>:
|
||||
在文件加密之前, 启用GZIP压缩文件; 文件解密之后启用GZIP解压缩文件, 默认启用,
|
||||
如果不启用, 则无法检测文件是否解密成功, 解密文件时会保留源文件, 避免解密失败造成文件数据丢失.`
|
||||
)
|
||||
|
||||
var ErrBadArgs = errors.New("参数错误")
|
||||
var ErrNotLogined = errors.New("未登录账号")
|
||||
|
||||
func GetActivePanClient() *config.PanClient {
|
||||
return config.Config.ActiveUser().PanClient()
|
||||
}
|
||||
|
||||
func GetActiveUser() *config.PanUser {
|
||||
return config.Config.ActiveUser()
|
||||
}
|
||||
|
||||
func parseDriveId(c *cli.Context) string {
|
||||
driveId := config.Config.ActiveUser().ActiveDriveId
|
||||
if c.IsSet("driveId") {
|
||||
driveId = c.String("driveId")
|
||||
}
|
||||
return driveId
|
||||
}
|
||||
|
||||
func CmdConfig() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "config",
|
||||
Usage: "显示和修改程序配置项",
|
||||
Description: "显示和修改程序配置项",
|
||||
Category: "配置",
|
||||
Before: ReloadConfigFunc,
|
||||
After: SaveConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Printf("----\n当前配置目录: %s\n运行 %s config set 可进行设置配置\n\n当前配置:\n", config.GetConfigDir(), cmder.App().Name)
|
||||
config.Config.PrintTable()
|
||||
return nil
|
||||
},
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "set",
|
||||
Usage: "修改程序配置项",
|
||||
UsageText: cmder.App().Name + " config set [arguments...]",
|
||||
Description: `
|
||||
注意:
|
||||
可通过设置环境变量 ALIYUNPAN_CONFIG_DIR, 指定配置文件存放的目录.
|
||||
|
||||
cache_size 的值支持可选设置单位, 单位不区分大小写, b 和 B 均表示字节的意思, 如 64KB, 1MB, 32kb, 65536b, 65536
|
||||
max_download_rate, max_upload_rate 的值支持可选设置单位, 单位为每秒的传输速率, 后缀'/s' 可省略, 如 2MB/s, 2MB, 2m, 2mb 均为一个意思
|
||||
|
||||
例子:
|
||||
aliyunpan config set -cache_size 64KB
|
||||
aliyunpan config set -cache_size 16384 -max_download_parallel 200 -savedir D:/download`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NumFlags() <= 0 || c.NArg() > 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
if c.IsSet("cache_size") {
|
||||
err := config.Config.SetCacheSizeByStr(c.String("cache_size"))
|
||||
if err != nil {
|
||||
fmt.Printf("设置 cache_size 错误: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if c.IsSet("max_download_parallel") {
|
||||
config.Config.MaxDownloadParallel = c.Int("max_download_parallel")
|
||||
}
|
||||
if c.IsSet("max_upload_parallel") {
|
||||
config.Config.MaxUploadParallel = c.Int("max_upload_parallel")
|
||||
}
|
||||
if c.IsSet("max_download_rate") {
|
||||
err := config.Config.SetMaxDownloadRateByStr(c.String("max_download_rate"))
|
||||
if err != nil {
|
||||
fmt.Printf("设置 max_download_rate 错误: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if c.IsSet("max_upload_rate") {
|
||||
err := config.Config.SetMaxUploadRateByStr(c.String("max_upload_rate"))
|
||||
if err != nil {
|
||||
fmt.Printf("设置 max_upload_rate 错误: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if c.IsSet("savedir") {
|
||||
config.Config.SaveDir = c.String("savedir")
|
||||
}
|
||||
if c.IsSet("proxy") {
|
||||
config.Config.SetProxy(c.String("proxy"))
|
||||
}
|
||||
if c.IsSet("local_addrs") {
|
||||
config.Config.SetLocalAddrs(c.String("local_addrs"))
|
||||
}
|
||||
if c.IsSet("ip_type") {
|
||||
config.Config.SetPreferIPType(c.String("ip_type"))
|
||||
}
|
||||
if c.IsSet("file_record_config") {
|
||||
config.Config.SetFileRecorderConfig(c.String("file_record_config"))
|
||||
}
|
||||
if c.IsSet("device_id") {
|
||||
config.Config.SetDeviceId(c.String("device_id"))
|
||||
}
|
||||
|
||||
err := config.Config.Save()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
config.Config.PrintTable()
|
||||
fmt.Printf("\n保存配置成功!\n\n")
|
||||
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "cache_size",
|
||||
Usage: "下载缓存,单位:KB",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "max_download_parallel",
|
||||
Usage: "下载文件最大并发量",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "max_upload_parallel",
|
||||
Usage: "上传文件最大并发量",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "max_download_rate",
|
||||
Usage: "限制最大下载速度, 0代表不限制",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "max_upload_rate",
|
||||
Usage: "限制最大上传速度, 0代表不限制",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "savedir",
|
||||
Usage: "下载文件的储存目录",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "proxy",
|
||||
Usage: "设置代理, 支持 http/socks5 代理",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "local_addrs",
|
||||
Usage: "设置本地网卡地址, 多个地址用逗号隔开",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "ip_type",
|
||||
Usage: "设置域名解析IP优先类型",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "file_record_config",
|
||||
Usage: "设置是否开启上传、下载、同步文件的结果记录功能",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "device_id",
|
||||
Usage: "设置客户端ID,24位的字符串",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CmdTool() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "tool",
|
||||
Usage: "工具箱",
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
},
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "getip",
|
||||
Usage: "获取IP地址",
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Printf("内网IP地址: \n")
|
||||
for _, address := range cmdutil.ListAddresses() {
|
||||
fmt.Printf("%s\n", address)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
|
||||
ipAddr, err := getip.IPInfoFromTechainBaiduByClient(config.Config.HTTPClient(""))
|
||||
if err != nil {
|
||||
fmt.Printf("获取公网IP错误: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("公网IP地址: %s\n", ipAddr)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "enc",
|
||||
Usage: "加密文件",
|
||||
UsageText: cmder.App().Name + " enc -method=<method> -key=<key> [files...]",
|
||||
Description: cryptoDescription,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() <= 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, filePath := range c.Args() {
|
||||
encryptedFilePath, err := crypto.EncryptFile(c.String("method"), []byte(c.String("key")), filePath, !c.Bool("disable-gzip"))
|
||||
if err != nil {
|
||||
fmt.Printf("%s\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("加密成功, %s -> %s\n", filePath, encryptedFilePath)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "method",
|
||||
Usage: "加密方法",
|
||||
Value: "aes-128-ctr",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "key",
|
||||
Usage: "加密密钥",
|
||||
Value: cmder.App().Name,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "disable-gzip",
|
||||
Usage: "不启用GZIP",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "dec",
|
||||
Usage: "解密文件",
|
||||
UsageText: cmder.App().Name + " dec -method=<method> -key=<key> [files...]",
|
||||
Description: cryptoDescription,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() <= 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, filePath := range c.Args() {
|
||||
decryptedFilePath, err := crypto.DecryptFile(c.String("method"), []byte(c.String("key")), filePath, !c.Bool("disable-gzip"))
|
||||
if err != nil {
|
||||
fmt.Printf("%s\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("解密成功, %s -> %s\n", filePath, decryptedFilePath)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "method",
|
||||
Usage: "加密方法",
|
||||
Value: "aes-128-ctr",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "key",
|
||||
Usage: "加密密钥",
|
||||
Value: cmder.App().Name,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "disable-gzip",
|
||||
Usage: "不启用GZIP",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRapidUploadItem_createRapidUploadLink(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestRapidUploadItem_newRapidUploadItem(t *testing.T) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func CmdCp() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "cp",
|
||||
Usage: "复制文件/目录",
|
||||
UsageText: `
|
||||
aliyunpan cp <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>`,
|
||||
Description: `
|
||||
同网盘内复制文件或者目录。支持通配符匹配复制文件,通配符当前只能匹配文件名,不能匹配文件路径。
|
||||
|
||||
示例:
|
||||
|
||||
将 /我的资源/1.mp4 复制到 根目录 /
|
||||
aliyunpan cp /我的资源/1.mp4 /
|
||||
|
||||
将 /我的资源 目录下所有的.png文件 复制到 /我的图片 目录下面,使用通配符匹配
|
||||
aliyunpan cp /我的资源/*.png /我的图片
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() <= 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
RunCopy(parseDriveId(c), c.Args()...)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunCopy 执行复制文件/目录
|
||||
func RunCopy(driveId string, paths ...string) {
|
||||
activeUser := GetActiveUser()
|
||||
cacheCleanPaths := []string{}
|
||||
opFileList, targetFile, _, err := getFileInfo(driveId, paths...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if targetFile == nil {
|
||||
fmt.Println("目标文件不存在")
|
||||
return
|
||||
}
|
||||
if opFileList == nil || len(opFileList) == 0 {
|
||||
fmt.Println("没有有效的文件可复制")
|
||||
return
|
||||
}
|
||||
cacheCleanPaths = append(cacheCleanPaths, targetFile.Path)
|
||||
|
||||
failedCopyFiles := []*aliyunpan.FileEntity{}
|
||||
successCopyFiles := []*aliyunpan.FileEntity{}
|
||||
for _, mfi := range opFileList {
|
||||
_, er := activeUser.PanClient().OpenapiPanClient().FileCopy(&aliyunpan.FileCopyParam{
|
||||
DriveId: driveId,
|
||||
FileId: mfi.FileId,
|
||||
ToParentFileId: targetFile.FileId,
|
||||
})
|
||||
if er == nil {
|
||||
successCopyFiles = append(successCopyFiles, mfi)
|
||||
} else {
|
||||
failedCopyFiles = append(failedCopyFiles, mfi)
|
||||
}
|
||||
}
|
||||
|
||||
if len(failedCopyFiles) > 0 {
|
||||
fmt.Println("以下文件复制失败:")
|
||||
for _, f := range failedCopyFiles {
|
||||
fmt.Println(f.Path)
|
||||
}
|
||||
fmt.Println("")
|
||||
}
|
||||
if len(successCopyFiles) > 0 {
|
||||
pnt := func() {
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "文件/目录"})
|
||||
for k, rs := range successCopyFiles {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), rs.Path})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
fmt.Println("操作成功, 以下文件已复制到目标目录: ", targetFile.Path)
|
||||
pnt()
|
||||
activeUser.DeleteCache(cacheCleanPaths)
|
||||
} else {
|
||||
fmt.Println("无法复制文件,请稍后重试")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/file/downloader"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"github.com/tickstep/aliyunpan/internal/taskframework"
|
||||
"github.com/tickstep/aliyunpan/internal/ui"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
type (
|
||||
//DownloadOptions 下载可选参数
|
||||
DownloadOptions struct {
|
||||
// DownloadActionId 下载动作ID,唯一标识一次下载动作的ID。这个ID每次下载任务启动的时候会自动生成,同一次下载任务下载的文件,这个ID都是同一个值。
|
||||
DownloadActionId string
|
||||
IsPrintStatus bool
|
||||
IsExecutedPermission bool
|
||||
IsOverwrite bool
|
||||
SaveTo string
|
||||
Parallel int // 文件下载最大线程数,即同时进行下载文件的数量,阿里云盘限制最大为3
|
||||
SliceParallel int // 单个文件分片下载最大线程数,阿里云盘限制最大为3
|
||||
Load int
|
||||
MaxRetry int
|
||||
NoCheck bool
|
||||
ShowProgress bool
|
||||
DriveId string
|
||||
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
|
||||
IsMultiUserDownload bool // 是否启用多用户联合下载
|
||||
IsUseUIDashboard bool // 是否使用UI下载面板显示下载进度
|
||||
}
|
||||
|
||||
// LocateDownloadOption 获取下载链接可选参数
|
||||
LocateDownloadOption struct {
|
||||
FromPan bool
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// MaxDownloadRangeSize 文件片段最大值
|
||||
MaxDownloadRangeSize = 55 * converter.MB
|
||||
|
||||
// DownloadCacheSize 默认每个线程下载缓存大小
|
||||
DownloadCacheSize = 64 * converter.KB
|
||||
)
|
||||
|
||||
func CmdDownload() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "download",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "下载文件/目录",
|
||||
UsageText: cmder.App().Name + " download <文件/目录路径1> <文件/目录2> <文件/目录3> ...",
|
||||
Description: `
|
||||
下载的文件默认保存到, 程序所在目录的 download/ 目录。支持软链接文件,包括Linux/macOS(ln命令)和Windows(mklink命令)创建的符号链接文件。
|
||||
通过 aliyunpan config set -savedir <savedir>, 自定义保存的目录。支持多个文件或目录下载,支持自动跳过下载重名的文件!
|
||||
|
||||
示例:
|
||||
设置保存目录, 保存到 D:\Downloads
|
||||
注意区别反斜杠 "\" 和 斜杠 "/" !!!
|
||||
aliyunpan config set -savedir D:\\Downloads
|
||||
或者
|
||||
aliyunpan config set -savedir D:/Downloads
|
||||
|
||||
下载 /我的资源/1.mp4
|
||||
aliyunpan download /我的资源/1.mp4
|
||||
|
||||
下载 /我的资源 目录下面所有的mp4文件,使用通配符
|
||||
aliyunpan download /我的资源/*.mp4
|
||||
|
||||
下载 /我的资源 整个目录!!
|
||||
aliyunpan download /我的资源
|
||||
|
||||
下载 /我的资源 整个目录,但是排除所有的jpg文件
|
||||
aliyunpan download -exn "\.jpg$" /我的资源
|
||||
|
||||
下载 /我的资源/1.mp4 并保存下载的文件到本地的 d:/panfile
|
||||
aliyunpan download --saveto d:/panfile /我的资源/1.mp4
|
||||
|
||||
使用多用户联合下载 /我的资源/1.mp4 文件。必须保证所有登录的用户在相同的网盘(备份盘/资源盘)下,相同的路径下,有相同的文件
|
||||
aliyunpan download /我的资源/1.mp4 -md
|
||||
|
||||
参考:
|
||||
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式。在正则表达式中,^表示匹配开头,$表示匹配结尾。
|
||||
1)排除@eadir文件或者文件夹:-exn "^@eadir$"
|
||||
2)排除.jpg文件:-exn "\.jpg$"
|
||||
3)排除.号开头的文件:-exn "^\."
|
||||
4)排除~号开头的文件:-exn "^~"
|
||||
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() == 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理saveTo
|
||||
var (
|
||||
saveTo string
|
||||
)
|
||||
if c.Bool("save") {
|
||||
// 使用当前工作目录
|
||||
pwd, _ := os.Getwd()
|
||||
saveTo = path.Clean(pwd)
|
||||
} else if c.String("saveto") != "" {
|
||||
saveTo = filepath.Clean(c.String("saveto"))
|
||||
}
|
||||
|
||||
do := &DownloadOptions{
|
||||
DownloadActionId: utils.UuidStr(),
|
||||
IsPrintStatus: c.Bool("status"),
|
||||
IsExecutedPermission: c.Bool("x"),
|
||||
IsOverwrite: c.Bool("ow"),
|
||||
SaveTo: saveTo,
|
||||
Parallel: c.Int("p"),
|
||||
SliceParallel: c.Int("sp"),
|
||||
Load: 0,
|
||||
MaxRetry: c.Int("retry"),
|
||||
NoCheck: c.Bool("nocheck"),
|
||||
ShowProgress: !c.Bool("np"),
|
||||
DriveId: parseDriveId(c),
|
||||
ExcludeNames: c.StringSlice("exn"),
|
||||
IsMultiUserDownload: c.Bool("md"),
|
||||
IsUseUIDashboard: c.Bool("ui"),
|
||||
}
|
||||
|
||||
RunDownload(c.Args(), do)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "ow",
|
||||
Usage: "overwrite, 覆盖已存在的文件",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "status",
|
||||
Usage: "输出所有线程的工作状态",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "save",
|
||||
Usage: "将下载的文件直接保存到当前工作目录",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "saveto",
|
||||
Usage: "将下载的文件直接保存到指定的目录",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "x",
|
||||
Usage: "为文件加上执行权限, (windows系统无效)",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "p",
|
||||
Usage: "parallel,指定同时进行下载文件的数量(取值范围:1 ~ 3)",
|
||||
Value: 1,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "sp",
|
||||
Usage: "slice parallel,指定单个文件下载的最大线程(分片)数(取值范围:1 ~ 3)",
|
||||
Value: 0,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "retry",
|
||||
Usage: "下载失败最大重试次数",
|
||||
Value: pandownload.DefaultDownloadMaxRetry,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "nocheck",
|
||||
Usage: "下载文件完成后不校验文件",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "np",
|
||||
Usage: "no progress 不展示下载进度条",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "exn",
|
||||
Usage: "exclude name,指定排除的文件夹或者文件的名称,被排除的文件不会进行下载,只支持正则表达式。支持同时排除多个名称,每一个名称就是一个exn参数",
|
||||
Value: nil,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "md",
|
||||
Usage: "(BETA) Multi-User Download,使用多用户联合下载,可以对单一文件叠加所有登录用户的下载速度",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "ui",
|
||||
Usage: "(BETA) 使用UI面板显示下载详情和进度,更加直观和友好",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunDownload 执行下载网盘内文件
|
||||
func RunDownload(paths []string, options *DownloadOptions) {
|
||||
activeUser := GetActiveUser()
|
||||
activeUser.PanClient().OpenapiPanClient().EnableCache()
|
||||
activeUser.PanClient().OpenapiPanClient().ClearCache()
|
||||
defer activeUser.PanClient().OpenapiPanClient().DisableCache()
|
||||
|
||||
//// pan token expired checker
|
||||
//continueFlag := int32(0)
|
||||
//atomic.StoreInt32(&continueFlag, 0)
|
||||
//defer func() {
|
||||
// atomic.StoreInt32(&continueFlag, 1)
|
||||
//}()
|
||||
//go func(flag *int32) {
|
||||
// for atomic.LoadInt32(flag) == 0 {
|
||||
// time.Sleep(time.Duration(1) * time.Minute)
|
||||
// if RefreshWebTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
// logger.Verboseln("update access token for download task")
|
||||
// }
|
||||
// }
|
||||
//}(&continueFlag)
|
||||
|
||||
if options == nil {
|
||||
options = &DownloadOptions{}
|
||||
}
|
||||
|
||||
if options.MaxRetry < 0 {
|
||||
options.MaxRetry = pandownload.DefaultDownloadMaxRetry
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// windows下不加执行权限
|
||||
options.IsExecutedPermission = false
|
||||
}
|
||||
|
||||
// 设置下载配置
|
||||
cfg := &downloader.Config{
|
||||
Mode: transfer.RangeGenMode_BlockSize,
|
||||
CacheSize: config.Config.CacheSize,
|
||||
BlockSize: MaxDownloadRangeSize,
|
||||
MaxRate: config.Config.MaxDownloadRate,
|
||||
InstanceStateStorageFormat: downloader.InstanceStateStorageFormatJSON,
|
||||
ShowProgress: options.ShowProgress,
|
||||
ExcludeNames: options.ExcludeNames,
|
||||
}
|
||||
if cfg.CacheSize == 0 {
|
||||
cfg.CacheSize = int(DownloadCacheSize)
|
||||
}
|
||||
|
||||
// 设置下载最大并发量
|
||||
if options.Parallel < 1 {
|
||||
options.Parallel = config.Config.MaxDownloadParallel
|
||||
if options.Parallel == 0 {
|
||||
options.Parallel = config.DefaultFileDownloadParallelNum
|
||||
}
|
||||
}
|
||||
if options.Parallel > config.MaxFileDownloadParallelNum {
|
||||
options.Parallel = config.MaxFileDownloadParallelNum
|
||||
}
|
||||
|
||||
// 设置单个文件下载分片线程数
|
||||
if options.SliceParallel < 1 { // 用户没有主动设置,则使用下面自动配置的策略
|
||||
if options.Parallel > 1 {
|
||||
// 如果是多文件下载,则一个文件只能同时使用1个线程
|
||||
options.SliceParallel = 1
|
||||
} else {
|
||||
// 如果是单文件下载,则可以使用3个最大线程
|
||||
options.SliceParallel = 3
|
||||
}
|
||||
}
|
||||
|
||||
// 保存文件的本地根文件夹
|
||||
originSaveRootPath := ""
|
||||
if options.SaveTo != "" {
|
||||
originSaveRootPath = options.SaveTo
|
||||
} else {
|
||||
// 使用默认的保存路径
|
||||
originSaveRootPath = GetActiveUser().GetSavePath("")
|
||||
}
|
||||
fi, err1 := os.Stat(originSaveRootPath)
|
||||
if err1 != nil && !os.IsExist(err1) {
|
||||
os.MkdirAll(originSaveRootPath, 0777) // 首先在本地创建目录
|
||||
} else {
|
||||
if !fi.IsDir() {
|
||||
fmt.Println("本地保存路径不是文件夹,请删除或者创建对应的文件夹:", originSaveRootPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
paths, err := makePathAbsolute(options.DriveId, paths...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 多用户下载的辅助账号列表
|
||||
var subPanClientList []*config.PanClient
|
||||
if options.IsMultiUserDownload { // 多用户下载
|
||||
c := config.Config
|
||||
for _, u := range config.Config.UserList {
|
||||
if u.UserId == activeUser.UserId {
|
||||
// 当前登录用户,作为主用户,跳过
|
||||
continue
|
||||
}
|
||||
// 初始化客户端
|
||||
user, err := config.SetupUserByCookie(u.OpenapiToken, u.WebapiToken,
|
||||
u.TicketId, u.UserId,
|
||||
c.DeviceId, c.DeviceName,
|
||||
c.ClientId, c.ClientSecret)
|
||||
if err != nil {
|
||||
logger.Verboseln("setup user error")
|
||||
continue
|
||||
}
|
||||
if subPanClientList == nil {
|
||||
subPanClientList = []*config.PanClient{}
|
||||
}
|
||||
subPanClientList = append(subPanClientList, user.PanClient())
|
||||
}
|
||||
|
||||
if subPanClientList == nil || len(subPanClientList) == 0 {
|
||||
fmt.Printf("\n当前登录用户只有一个,无法启用多用户联合下载\n")
|
||||
subPanClientList = nil
|
||||
}
|
||||
}
|
||||
if subPanClientList != nil || len(subPanClientList) > 0 {
|
||||
// 已启用多用户下载
|
||||
userCount := len(subPanClientList) + 1
|
||||
fmt.Printf("\n*** 已启用多用户联合下载,用户数: %d ***\n", userCount)
|
||||
// 多用户下载,并发数必须为1,以获得最大下载速度
|
||||
options.Parallel = 1
|
||||
// 阿里OpenAPI规定:文件分片下载的并发数为3,即某用户使用 App 时,可以同时下载 1 个文件的 3 个分片,或者同时下载 3 个文件的各 1 个分片。
|
||||
options.SliceParallel = userCount * 3
|
||||
}
|
||||
|
||||
var (
|
||||
panClient = activeUser.PanClient()
|
||||
)
|
||||
cfg.MaxParallel = options.Parallel
|
||||
cfg.SliceParallel = options.SliceParallel
|
||||
|
||||
var (
|
||||
executor = taskframework.TaskExecutor{
|
||||
IsFailedDeque: true, // 统计失败的列表
|
||||
}
|
||||
statistic = &pandownload.DownloadStatistic{}
|
||||
)
|
||||
// 配置执行器任务并发数,即同时下载文件并发数
|
||||
executor.SetParallel(cfg.MaxParallel)
|
||||
|
||||
// 全局速度统计
|
||||
globalSpeedsStat := &speeds.Speeds{}
|
||||
|
||||
// 下载统计UI面板
|
||||
var dashboard *ui.DashboardPanel = nil
|
||||
if options.IsUseUIDashboard && options.ShowProgress && !options.IsPrintStatus && ui.IsTerminal(os.Stdout) {
|
||||
dashboard = ui.NewDashboardPanel(ui.DashboardPanelDownload, cfg.MaxParallel, globalSpeedsStat, &ui.DashboardOptions{
|
||||
Title: "下载统计UI面板",
|
||||
ActiveSlots: 3, // 下载文件进度展示,最大同时展示3个
|
||||
MaxHistory: 50, // 下载日志显示,最多同时显示50条,这个会按照窗口大小进行自适应显示
|
||||
})
|
||||
}
|
||||
logf := func(format string, a ...interface{}) {
|
||||
if dashboard != nil {
|
||||
dashboard.Logf(format, a...)
|
||||
return
|
||||
}
|
||||
fmt.Printf(format, a...)
|
||||
}
|
||||
|
||||
// 下载记录器
|
||||
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/download_file_records.csv")
|
||||
|
||||
// 输出当前下载配置信息
|
||||
logf("\n[0] 当前文件下载最大并发量为: %d, 单文件下载分片线程数为: %d, 下载缓存为: %s\n", options.Parallel, options.SliceParallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
||||
|
||||
// 阿里云盘下载并发超额警告,阿里云盘下载最大并发数为3
|
||||
if options.Parallel*options.SliceParallel > 3 {
|
||||
logf("[0] 警告:当前下载并发配置数已超过阿里云盘限制的最大并发数,下载任务会被风控容易导致下载失败\n")
|
||||
}
|
||||
|
||||
// 处理队列
|
||||
for k := range paths {
|
||||
// 使用通配符匹配
|
||||
fileList, err2 := matchPathByShellPattern(options.DriveId, paths[k])
|
||||
if err2 != nil {
|
||||
logf("获取文件出错,请稍后重试: %s\n", paths[k])
|
||||
continue
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
// 文件不存在
|
||||
logf("文件不存在: %s\n", paths[k])
|
||||
continue
|
||||
}
|
||||
// 排序,按名称排序,从小到大
|
||||
sort.Slice(fileList, func(i, j int) bool {
|
||||
return fileList[i].FileName < fileList[j].FileName
|
||||
})
|
||||
// 逐一下载
|
||||
for _, f := range fileList {
|
||||
newCfg := *cfg
|
||||
|
||||
// 是否排除下载
|
||||
if utils.IsExcludeFile(f.Path, &newCfg.ExcludeNames) {
|
||||
logf("排除文件: %s\n", f.Path)
|
||||
continue
|
||||
}
|
||||
|
||||
// 匹配的文件
|
||||
unit := pandownload.DownloadTaskUnit{
|
||||
DownloadActionId: options.DownloadActionId,
|
||||
Cfg: &newCfg, // 复制一份新的cfg
|
||||
PanClient: panClient,
|
||||
SubPanClientList: subPanClientList,
|
||||
VerbosePrinter: panCommandVerbose,
|
||||
ParentTaskExecutor: &executor,
|
||||
DownloadStatistic: statistic,
|
||||
IsPrintStatus: options.IsPrintStatus,
|
||||
IsExecutedPermission: options.IsExecutedPermission,
|
||||
IsOverwrite: options.IsOverwrite,
|
||||
NoCheck: options.NoCheck,
|
||||
FilePanSource: global.FileSource,
|
||||
FilePanPath: f.Path,
|
||||
DriveId: options.DriveId,
|
||||
GlobalSpeedsStat: globalSpeedsStat,
|
||||
FileRecorder: fileRecorder,
|
||||
UI: dashboard,
|
||||
}
|
||||
|
||||
// 设置储存的路径
|
||||
if options.SaveTo != "" {
|
||||
unit.OriginSaveRootPath = options.SaveTo
|
||||
unit.SavePath = filepath.Join(options.SaveTo, f.Path)
|
||||
} else {
|
||||
// 使用默认的保存路径
|
||||
unit.OriginSaveRootPath = GetActiveUser().GetSavePath("")
|
||||
unit.SavePath = GetActiveUser().GetSavePath(f.Path)
|
||||
}
|
||||
info := executor.Append(&unit, options.MaxRetry)
|
||||
if dashboard != nil {
|
||||
dashboard.RegisterTask(info.Id(), f.Path, f.FileSize, f.IsFile())
|
||||
}
|
||||
logf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始计时
|
||||
statistic.StartTimer()
|
||||
|
||||
// 启动UI面板显示
|
||||
if dashboard != nil {
|
||||
dashboard.Start()
|
||||
}
|
||||
|
||||
// 开始执行
|
||||
executor.Execute()
|
||||
|
||||
// 关闭UI面板
|
||||
if dashboard != nil {
|
||||
dashboard.Close()
|
||||
}
|
||||
|
||||
// 完成下载,输出统计结果
|
||||
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
|
||||
|
||||
// 输出失败的文件列表
|
||||
failedList := executor.FailedDeque()
|
||||
if failedList.Size() != 0 {
|
||||
fmt.Printf("以下文件下载失败: \n")
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
for e := failedList.Shift(); e != nil; e = failedList.Shift() {
|
||||
item := e.(*taskframework.TaskInfoItem)
|
||||
tb.Append([]string{item.Info.Id(), item.Unit.(*pandownload.DownloadTaskUnit).FilePanPath})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CmdDrive() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "drive",
|
||||
Usage: "切换网盘(备份盘/资源库)",
|
||||
Description: `
|
||||
切换已登录账号的阿里云盘的工作网盘(备份盘/资源库)
|
||||
如果运行该条命令没有提供参数, 程序将会列出所有的网盘列表, 供选择切换.
|
||||
|
||||
示例:
|
||||
aliyunpan drive
|
||||
aliyunpan drive <driveId>
|
||||
`,
|
||||
Category: "阿里云盘账号",
|
||||
Before: ReloadConfigFunc,
|
||||
After: SaveConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
inputData := c.Args().Get(0)
|
||||
targetDriveId := strings.TrimSpace(inputData)
|
||||
RunSwitchDriveList(targetDriveId)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunSwitchDriveList(targetDriveId string) {
|
||||
currentDriveId := config.Config.ActiveUser().ActiveDriveId
|
||||
var activeDriveInfo *config.DriveInfo = nil
|
||||
driveList, renderStr := getDriveOptionList()
|
||||
|
||||
if driveList == nil || len(driveList) == 0 {
|
||||
fmt.Println("切换网盘失败")
|
||||
return
|
||||
}
|
||||
|
||||
if targetDriveId == "" {
|
||||
// show option list
|
||||
fmt.Println(renderStr)
|
||||
|
||||
// 提示输入 index
|
||||
var index string
|
||||
fmt.Printf("输入要切换的网盘 # 值 > ")
|
||||
_, err := fmt.Scanln(&index)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if n, err1 := strconv.Atoi(index); err1 == nil && (n-1) >= 0 && (n-1) < len(driveList) {
|
||||
activeDriveInfo = driveList[n-1]
|
||||
} else {
|
||||
fmt.Printf("切换网盘失败, 请检查 # 值是否正确\n")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 直接切换
|
||||
for _, driveInfo := range driveList {
|
||||
if driveInfo.DriveId == targetDriveId {
|
||||
activeDriveInfo = driveInfo
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if activeDriveInfo == nil {
|
||||
fmt.Printf("切换网盘失败\n")
|
||||
return
|
||||
}
|
||||
|
||||
config.Config.ActiveUser().ActiveDriveId = activeDriveInfo.DriveId
|
||||
activeUser := config.Config.ActiveUser()
|
||||
if currentDriveId != config.Config.ActiveUser().ActiveDriveId {
|
||||
// clear the drive work path
|
||||
if activeUser.IsFileDriveActive() {
|
||||
if activeUser.Workdir == "" {
|
||||
config.Config.ActiveUser().Workdir = "/"
|
||||
config.Config.ActiveUser().WorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
|
||||
}
|
||||
} else if activeUser.IsResourceDriveActive() {
|
||||
if activeUser.ResourceWorkdir == "" {
|
||||
config.Config.ActiveUser().ResourceWorkdir = "/"
|
||||
config.Config.ActiveUser().ResourceWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
|
||||
}
|
||||
} else if activeUser.IsAlbumDriveActive() {
|
||||
if activeUser.AlbumWorkdir == "" {
|
||||
config.Config.ActiveUser().AlbumWorkdir = "/"
|
||||
config.Config.ActiveUser().AlbumWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("切换到网盘:%s\n", activeDriveInfo.DriveName)
|
||||
}
|
||||
|
||||
func getDriveOptionList() (config.DriveInfoList, string) {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
|
||||
driveList := activeUser.DriveList
|
||||
builder := &strings.Builder{}
|
||||
tb := cmdtable.NewTable(builder)
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_CENTER})
|
||||
tb.SetHeader([]string{"#", "drive_id", "网盘名称"})
|
||||
|
||||
for k, info := range driveList {
|
||||
if info.IsAlbumDrive() {
|
||||
continue
|
||||
}
|
||||
tb.Append([]string{strconv.Itoa(k + 1), info.DriveId, info.DriveName})
|
||||
}
|
||||
tb.Render()
|
||||
return driveList, builder.String()
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdliner"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
_ "github.com/tickstep/library-go/requester"
|
||||
"github.com/urfave/cli"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CmdLogin() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "login",
|
||||
Usage: "登录阿里云盘账号",
|
||||
Description: `
|
||||
示例:
|
||||
1.常规登录,按提示一步一步来即可
|
||||
aliyunpan login
|
||||
|
||||
`,
|
||||
Category: "阿里云盘账号",
|
||||
Before: ReloadConfigFunc, // 每次进行登录动作的时候需要调用刷新配置
|
||||
After: SaveConfigFunc, // 登录完成需要调用保存配置
|
||||
Action: func(c *cli.Context) error {
|
||||
ticketId := ""
|
||||
openToken := &config.PanClientToken{}
|
||||
webToken := &config.PanClientToken{}
|
||||
var err error
|
||||
ticketId, openToken, webToken, err = RunLogin()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
cloudUser, err := config.SetupUserByCookie(openToken, webToken,
|
||||
ticketId, "",
|
||||
config.Config.DeviceId, config.Config.DeviceName,
|
||||
config.Config.ClientId, config.Config.ClientSecret)
|
||||
if cloudUser == nil {
|
||||
fmt.Println("登录失败: ", err)
|
||||
return nil
|
||||
}
|
||||
cloudUser.TicketId = ticketId
|
||||
config.Config.SetActiveUser(cloudUser)
|
||||
fmt.Println("阿里云盘登录成功: ", cloudUser.Nickname)
|
||||
return nil
|
||||
},
|
||||
// 命令的附加options参数说明,使用 help panlogin 命令即可查看
|
||||
Flags: []cli.Flag{},
|
||||
}
|
||||
}
|
||||
|
||||
func CmdLogout() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "logout",
|
||||
Usage: "退出阿里帐号",
|
||||
Description: "退出当前登录的帐号",
|
||||
Category: "阿里云盘账号",
|
||||
Before: ReloadConfigFunc,
|
||||
After: SaveConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.NumLogins() == 0 {
|
||||
fmt.Println("未设置任何帐号, 不能退出")
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
confirm string
|
||||
activeUser = config.Config.ActiveUser()
|
||||
)
|
||||
if activeUser == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !c.Bool("y") {
|
||||
fmt.Printf("确认退出当前帐号: %s ? (y/n) > ", activeUser.Nickname)
|
||||
_, err := fmt.Scanln(&confirm)
|
||||
if err != nil || (confirm != "y" && confirm != "Y") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 云端注销登录
|
||||
//if _, e := activeUser.PanClient().WebapiPanClient().DeviceLogout(); e != nil {
|
||||
// fmt.Printf("登出设备失败,请手动在网页端进行登出: %s\n", e.String())
|
||||
//}
|
||||
|
||||
// 删除用户信息
|
||||
deletedUser, err := config.Config.DeleteUser(activeUser.UserId)
|
||||
if err != nil {
|
||||
fmt.Printf("退出用户 %s, 失败, 错误: %s\n", activeUser.Nickname, err)
|
||||
}
|
||||
|
||||
fmt.Printf("退出用户成功: %s\n", deletedUser.Nickname)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "y",
|
||||
Usage: "确认退出帐号",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunLogin() (ticketId string, openapiToken, webapiToken *config.PanClientToken, error error) {
|
||||
h := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
|
||||
|
||||
// web login request
|
||||
qrCodeUrlResult, err := h.GetQRCodeLoginUrl("")
|
||||
if err != nil {
|
||||
fmt.Println("登录出错:", err)
|
||||
return "", nil, nil, err
|
||||
}
|
||||
ticketId = qrCodeUrlResult.TokenId
|
||||
loginUrl := &strings.Builder{}
|
||||
if global.IsSupportNoneOpenApiCommands {
|
||||
// 兼容以前的版本
|
||||
fmt.Fprintf(loginUrl, "https://openapi.alipan.com/oauth/authorize?client_id=%s&redirect_uri=https%%3A%%2F%%2Fapi.tickstep.com%%2Fauth%%2Ftickstep%%2Faliyunpan%%2Ftoken%%2Fopenapi%%2F%s%%2Fauth&scope=user:base,file:all:read,file:all:write,file:share:write,album:shared:read",
|
||||
config.Config.ClientId, ticketId)
|
||||
} else {
|
||||
fmt.Fprintf(loginUrl, "https://openapi.alipan.com/oauth/authorize?client_id=%s&redirect_uri=https%%3A%%2F%%2Fapi.tickstep.com%%2Fauth%%2Ftickstep%%2Faliyunpan%%2Ftoken%%2Fopenapi%%2F%s%%2Fauth2&scope=user:base,file:all:read,file:all:write,file:share:write,album:shared:read",
|
||||
config.Config.ClientId, ticketId)
|
||||
}
|
||||
fmt.Printf("请在浏览器打开以下链接进行登录,链接有效时间为5分钟。\n注意:你需要进行一次授权一次扫码的两次登录。\n%s\n\n", loginUrl)
|
||||
|
||||
// handler waiting
|
||||
line := cmdliner.NewLiner()
|
||||
defer line.Close()
|
||||
line.State.Prompt("请在浏览器里面完成扫码登录,然后再按Enter键继续...")
|
||||
|
||||
// get login token
|
||||
comToken, er := h.GetLoginToken(ticketId)
|
||||
if er != nil {
|
||||
return ticketId, nil, nil, fmt.Errorf("登录失败,请稍后尝试重新登录")
|
||||
}
|
||||
|
||||
return ticketId,
|
||||
&config.PanClientToken{
|
||||
AccessToken: comToken.Openapi.AccessToken,
|
||||
Expired: comToken.Openapi.Expired,
|
||||
},
|
||||
&config.PanClientToken{
|
||||
AccessToken: comToken.Webapi.AccessToken,
|
||||
Expired: comToken.Webapi.Expired,
|
||||
},
|
||||
nil
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type (
|
||||
// LsOptions 列目录可选项
|
||||
LsOptions struct {
|
||||
Total bool
|
||||
}
|
||||
|
||||
// SearchOptions 搜索可选项
|
||||
SearchOptions struct {
|
||||
Total bool
|
||||
Recurse bool
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
opLs int = iota
|
||||
opSearch
|
||||
)
|
||||
|
||||
func CmdLs() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "ls",
|
||||
Aliases: []string{"l", "ll"},
|
||||
Usage: "列出目录",
|
||||
UsageText: cmder.App().Name + " ls <目录>",
|
||||
Description: `
|
||||
列出当前工作目录内的文件和目录, 或指定目录内的文件和目录
|
||||
|
||||
示例:
|
||||
|
||||
列出 我的资源 内的文件和目录
|
||||
aliyunpan ls 我的资源
|
||||
|
||||
绝对路径
|
||||
aliyunpan ls /我的资源
|
||||
|
||||
列出 我的资源 内的文件和目录,使用通配符
|
||||
aliyunpan ls /我的*
|
||||
|
||||
详细列出 我的资源 内的文件和目录
|
||||
aliyunpan ll /我的资源
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
After: SaveConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
orderBy aliyunpan.FileOrderBy = aliyunpan.FileOrderByUpdatedAt
|
||||
orderSort aliyunpan.FileOrderDirection = aliyunpan.FileOrderDirectionDesc
|
||||
)
|
||||
|
||||
switch {
|
||||
case c.IsSet("asc"):
|
||||
orderSort = aliyunpan.FileOrderDirectionAsc
|
||||
case c.IsSet("desc"):
|
||||
orderSort = aliyunpan.FileOrderDirectionDesc
|
||||
default:
|
||||
orderSort = aliyunpan.FileOrderDirectionDesc
|
||||
}
|
||||
|
||||
switch {
|
||||
case c.IsSet("time"):
|
||||
orderBy = aliyunpan.FileOrderByUpdatedAt
|
||||
case c.IsSet("name"):
|
||||
orderBy = aliyunpan.FileOrderByName
|
||||
case c.IsSet("size"):
|
||||
orderBy = aliyunpan.FileOrderBySize
|
||||
default:
|
||||
orderBy = aliyunpan.FileOrderByUpdatedAt
|
||||
}
|
||||
|
||||
RunLs(parseDriveId(c), c.Args().Get(0), &LsOptions{
|
||||
Total: c.Bool("l") || c.Parent().Args().Get(0) == "ll",
|
||||
}, orderBy, orderSort)
|
||||
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "asc",
|
||||
Usage: "升序排序",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "desc",
|
||||
Usage: "降序排序",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "time",
|
||||
Usage: "根据修改时间排序",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "name",
|
||||
Usage: "根据文件名排序",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "size",
|
||||
Usage: "根据大小排序",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunLs(driveId, targetPath string, lsOptions *LsOptions,
|
||||
orderBy aliyunpan.FileOrderBy, orderDirection aliyunpan.FileOrderDirection) {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
targetPath = activeUser.PathJoin(driveId, targetPath)
|
||||
|
||||
// 获取目标路径文件信息
|
||||
targetPathInfo, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, targetPath)
|
||||
if err != nil {
|
||||
if err.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
fmt.Println("指定目录不存在: " + targetPath)
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 适配通配符路径获取目标文件信息(弃用,容易触发风控)
|
||||
//files, err := matchPathByShellPattern(driveId, targetPath)
|
||||
//if err != nil {
|
||||
// fmt.Println(err)
|
||||
// return
|
||||
//}
|
||||
//var targetPathInfo *aliyunpan.FileEntity
|
||||
//if len(files) == 1 {
|
||||
// targetPathInfo = files[0]
|
||||
//} else {
|
||||
// for _, f := range files {
|
||||
// if f.IsFolder() {
|
||||
// targetPathInfo = f
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
if targetPathInfo == nil {
|
||||
fmt.Println("目录路径不存在")
|
||||
return
|
||||
}
|
||||
|
||||
fileList := aliyunpan.FileList{}
|
||||
fileListParam := &aliyunpan.FileListParam{}
|
||||
fileListParam.ParentFileId = targetPathInfo.FileId
|
||||
fileListParam.DriveId = driveId
|
||||
fileListParam.OrderBy = orderBy
|
||||
fileListParam.OrderDirection = orderDirection
|
||||
if targetPathInfo.IsFolder() {
|
||||
fileResult, err1 := activeUser.PanClient().OpenapiPanClient().FileListGetAll(fileListParam, 200)
|
||||
if err1 != nil {
|
||||
fmt.Println(err1)
|
||||
return
|
||||
}
|
||||
fileList = fileResult
|
||||
} else {
|
||||
fileList = append(fileList, targetPathInfo)
|
||||
}
|
||||
renderTable(opLs, lsOptions.Total, targetPathInfo.Path, fileList)
|
||||
}
|
||||
|
||||
func renderTable(op int, isTotal bool, path string, files aliyunpan.FileList) {
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
var (
|
||||
fN, dN int64
|
||||
showPath string
|
||||
)
|
||||
|
||||
switch op {
|
||||
case opLs:
|
||||
showPath = "文件(目录)"
|
||||
case opSearch:
|
||||
showPath = "路径"
|
||||
}
|
||||
|
||||
if isTotal {
|
||||
tb.SetHeader([]string{"#", "file_id", "文件大小", "文件SHA1", "文件大小(原始)", "创建日期", "修改日期", showPath})
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
|
||||
for k, file := range files {
|
||||
if file.IsFolder() {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, "-", "-", "-", file.CreatedAt, file.UpdatedAt, file.FileName + aliyunpan.PathSeparator})
|
||||
continue
|
||||
}
|
||||
|
||||
switch op {
|
||||
case opLs:
|
||||
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, converter.ConvertFileSize(file.FileSize, 2), file.ContentHash, strconv.FormatInt(file.FileSize, 10), file.CreatedAt, file.UpdatedAt, file.FileName})
|
||||
case opSearch:
|
||||
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, converter.ConvertFileSize(file.FileSize, 2), file.ContentHash, strconv.FormatInt(file.FileSize, 10), file.CreatedAt, file.UpdatedAt, file.Path})
|
||||
}
|
||||
}
|
||||
fN, dN = files.Count()
|
||||
tb.Append([]string{"", "", "总: " + converter.ConvertFileSize(files.TotalSize(), 2), "", "", "", fmt.Sprintf("文件总数: %d, 目录总数: %d", fN, dN)})
|
||||
} else {
|
||||
tb.SetHeader([]string{"#", "文件大小", "修改日期", showPath})
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
|
||||
for k, file := range files {
|
||||
if file.IsFolder() {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), "-", file.UpdatedAt, file.FileName + aliyunpan.PathSeparator})
|
||||
continue
|
||||
}
|
||||
|
||||
switch op {
|
||||
case opLs:
|
||||
tb.Append([]string{strconv.Itoa(k + 1), converter.ConvertFileSize(file.FileSize, 2), file.UpdatedAt, file.FileName})
|
||||
case opSearch:
|
||||
tb.Append([]string{strconv.Itoa(k + 1), converter.ConvertFileSize(file.FileSize, 2), file.UpdatedAt, file.Path})
|
||||
}
|
||||
}
|
||||
fN, dN = files.Count()
|
||||
tb.Append([]string{"", "总: " + converter.ConvertFileSize(files.TotalSize(), 2), "", fmt.Sprintf("文件总数: %d, 目录总数: %d", fN, dN)})
|
||||
}
|
||||
fmt.Printf("\n当前目录: %s\n", path)
|
||||
fmt.Printf("----\n")
|
||||
tb.Render()
|
||||
fmt.Printf("----\n")
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func CmdMkdir() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "mkdir",
|
||||
Usage: "创建目录",
|
||||
UsageText: cmder.App().Name + " mkdir <目录>",
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() == 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
RunMkdir(parseDriveId(c), c.Args().Get(0))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunMkdir(driveId, name string) {
|
||||
activeUser := GetActiveUser()
|
||||
fullpath := activeUser.PathJoin(driveId, name)
|
||||
rs := &aliyunpan.MkdirResult{}
|
||||
err := apierror.NewFailedApiError("")
|
||||
rs, err = activeUser.PanClient().OpenapiPanClient().MkdirByFullPath(driveId, fullpath)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("创建文件夹失败:" + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if rs.FileId != "" {
|
||||
fmt.Println("创建文件夹成功: ", fullpath)
|
||||
|
||||
// cache
|
||||
activeUser.DeleteCache(GetAllPathFolderByPath(fullpath))
|
||||
} else {
|
||||
fmt.Println("创建文件夹失败: ", fullpath)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func CmdMv() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "mv",
|
||||
Usage: "移动文件/目录",
|
||||
UsageText: `
|
||||
aliyunpan mv <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>`,
|
||||
Description: `
|
||||
注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败。支持通配符匹配移动文件,通配符当前只能匹配文件名,不能匹配文件路径。
|
||||
|
||||
示例:
|
||||
|
||||
将 /我的资源/1.mp4 移动到 根目录 /
|
||||
aliyunpan mv /我的资源/1.mp4 /
|
||||
|
||||
将 /我的资源 目录下所有的.png文件 移动到 /我的图片 目录下面,使用通配符匹配
|
||||
aliyunpan mv /我的资源/*.png /我的图片
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() <= 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
RunMove(parseDriveId(c), c.Args()...)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunMove 执行移动文件/目录
|
||||
func RunMove(driveId string, paths ...string) {
|
||||
activeUser := GetActiveUser()
|
||||
cacheCleanPaths := []string{}
|
||||
opFileList, targetFile, _, err := getFileInfo(driveId, paths...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if targetFile == nil {
|
||||
fmt.Println("目标文件不存在")
|
||||
return
|
||||
}
|
||||
if opFileList == nil || len(opFileList) == 0 {
|
||||
fmt.Println("没有有效的文件可移动")
|
||||
return
|
||||
}
|
||||
cacheCleanPaths = append(cacheCleanPaths, targetFile.Path)
|
||||
|
||||
failedMoveFiles := []*aliyunpan.FileEntity{}
|
||||
successMoveFiles := []*aliyunpan.FileEntity{}
|
||||
for _, mfi := range opFileList {
|
||||
fmr, er := activeUser.PanClient().OpenapiPanClient().FileMove(&aliyunpan.FileMoveParam{
|
||||
DriveId: driveId,
|
||||
FileId: mfi.FileId,
|
||||
ToDriveId: driveId,
|
||||
ToParentFileId: targetFile.FileId,
|
||||
})
|
||||
if er != nil || !fmr.Success {
|
||||
failedMoveFiles = append(failedMoveFiles, mfi)
|
||||
} else {
|
||||
successMoveFiles = append(successMoveFiles, mfi)
|
||||
}
|
||||
cacheCleanPaths = append(cacheCleanPaths, path.Dir(mfi.Path))
|
||||
}
|
||||
|
||||
if len(failedMoveFiles) > 0 {
|
||||
fmt.Println("以下文件移动失败:")
|
||||
for _, f := range failedMoveFiles {
|
||||
fmt.Println(f.Path)
|
||||
}
|
||||
fmt.Println("")
|
||||
}
|
||||
if len(successMoveFiles) > 0 {
|
||||
pnt := func() {
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "文件/目录"})
|
||||
for k, rs := range successMoveFiles {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), rs.Path})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
fmt.Println("操作成功, 以下文件已移动到目标目录: ", targetFile.Path)
|
||||
pnt()
|
||||
} else {
|
||||
fmt.Println("无法移动文件,请稍后重试")
|
||||
}
|
||||
activeUser.DeleteCache(cacheCleanPaths)
|
||||
}
|
||||
|
||||
func getFileInfo(driveId string, paths ...string) (opFileList []*aliyunpan.FileEntity, targetFile *aliyunpan.FileEntity, failedPaths []string, error error) {
|
||||
if len(paths) <= 1 {
|
||||
return nil, nil, nil, fmt.Errorf("请指定目标文件夹路径")
|
||||
}
|
||||
activeUser := GetActiveUser()
|
||||
// the last one is the target file path
|
||||
targetFilePath := path.Clean(paths[len(paths)-1])
|
||||
absolutePath := activeUser.PathJoin(driveId, targetFilePath)
|
||||
targetFile, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, absolutePath)
|
||||
if err != nil || !targetFile.IsFolder() {
|
||||
return nil, nil, nil, fmt.Errorf("指定目标文件夹不存在")
|
||||
}
|
||||
|
||||
for idx := 0; idx < (len(paths) - 1); idx++ {
|
||||
absolutePath = path.Clean(activeUser.PathJoin(driveId, paths[idx]))
|
||||
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
|
||||
if err1 != nil {
|
||||
failedPaths = append(failedPaths, absolutePath)
|
||||
continue
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
// 文件不存在
|
||||
failedPaths = append(failedPaths, absolutePath)
|
||||
continue
|
||||
}
|
||||
for _, f := range fileList {
|
||||
// 移动匹配的文件
|
||||
opFileList = append(opFileList, f)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
type QuotaInfo struct {
|
||||
// 已使用个人空间大小
|
||||
UsedSize int64
|
||||
// 个人空间总大小
|
||||
Quota int64
|
||||
}
|
||||
|
||||
func CmdQuota() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "quota",
|
||||
Usage: "获取当前帐号空间配额",
|
||||
Description: "获取网盘的总储存空间, 和已使用的储存空间",
|
||||
Category: "阿里云盘账号",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
q, err := RunGetQuotaInfo()
|
||||
if err == nil {
|
||||
fmt.Printf("账号: %s, uid: %s, 个人空间总额: %s, 个人空间已使用: %s, 比率: %.2f%%\n",
|
||||
config.Config.ActiveUser().Nickname, config.Config.ActiveUser().UserId,
|
||||
converter.ConvertFileSize(q.Quota, 2), converter.ConvertFileSize(q.UsedSize, 2),
|
||||
100*float64(q.UsedSize)/float64(q.Quota))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunGetQuotaInfo() (quotaInfo *QuotaInfo, error error) {
|
||||
user, err := GetActivePanClient().OpenapiPanClient().GetUserInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &QuotaInfo{
|
||||
UsedSize: int64(user.UsedSize),
|
||||
Quota: int64(user.TotalSize),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CmdRecycle() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "recycle",
|
||||
Usage: "回收站",
|
||||
Description: `
|
||||
回收站操作.
|
||||
|
||||
示例:
|
||||
|
||||
1. 从回收站还原两个文件, 其中的两个文件的 file_id 分别为 1013792297798440 和 643596340463870
|
||||
aliyunpan recycle restore 1013792297798440 643596340463870
|
||||
|
||||
2. 从回收站删除两个文件, 其中的两个文件的 file_id 分别为 1013792297798440 和 643596340463870
|
||||
aliyunpan recycle delete 1013792297798440 643596340463870
|
||||
|
||||
3. 清空回收站, 程序不会进行二次确认, 谨慎操作!!!
|
||||
aliyunpan recycle delete -all
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NumFlags() <= 0 || c.NArg() <= 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls", "l"},
|
||||
Usage: "列出回收站文件列表",
|
||||
UsageText: cmder.App().Name + " recycle list",
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunRecycleList(parseDriveId(c))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "restore",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "还原回收站文件或目录",
|
||||
UsageText: cmder.App().Name + " recycle restore <file_id 1> <file_id 2> <file_id 3> ...",
|
||||
Description: `根据文件/目录的 fs_id, 还原回收站指定的文件或目录`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
if c.NArg() <= 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
RunRecycleRestore(parseDriveId(c), c.Args()...)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "删除回收站文件或目录 / 清空回收站",
|
||||
UsageText: cmder.App().Name + " recycle delete [-all] <file_id 1> <file_id 2> <file_id 3> ...",
|
||||
Description: `根据文件/目录的 file_id 或 -all 参数, 删除回收站指定的文件或目录或清空回收站`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
if c.Bool("all") {
|
||||
// 清空回收站
|
||||
RunRecycleClear(parseDriveId(c))
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.NArg() <= 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
RunRecycleDelete(parseDriveId(c), c.Args()...)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "all",
|
||||
Usage: "清空回收站, 程序不会进行二次确认, 谨慎操作!!!",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunRecycleList 执行列出回收站文件列表
|
||||
func RunRecycleList(driveId string) {
|
||||
panClient := GetActivePanClient()
|
||||
fdl, err := panClient.WebapiPanClient().RecycleBinFileListGetAll(&aliyunpan_web.RecycleBinFileListParam{
|
||||
DriveId: driveId,
|
||||
Limit: 100,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "file_id", "文件/目录名", "文件大小", "创建日期", "修改日期"})
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
|
||||
for k, file := range fdl {
|
||||
fn := file.FileName
|
||||
fs := converter.ConvertFileSize(file.FileSize, 2)
|
||||
if file.IsFolder() {
|
||||
fn = fn + "/"
|
||||
fs = "-"
|
||||
}
|
||||
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, fn, fs, file.CreatedAt, file.UpdatedAt})
|
||||
}
|
||||
|
||||
tb.Render()
|
||||
}
|
||||
|
||||
// RunRecycleRestore 执行还原回收站文件或目录
|
||||
func RunRecycleRestore(driveId string, fidStrList ...string) {
|
||||
panClient := GetActivePanClient()
|
||||
restoreFileList := []*aliyunpan.FileBatchActionParam{}
|
||||
|
||||
for _, fid := range fidStrList {
|
||||
restoreFileList = append(restoreFileList, &aliyunpan.FileBatchActionParam{
|
||||
DriveId: driveId,
|
||||
FileId: fid,
|
||||
})
|
||||
}
|
||||
|
||||
if len(restoreFileList) == 0 {
|
||||
fmt.Printf("没有需要还原的文件")
|
||||
return
|
||||
}
|
||||
|
||||
rbfr, err := panClient.WebapiPanClient().RecycleBinFileRestore(restoreFileList)
|
||||
if rbfr != nil && len(rbfr) > 0 {
|
||||
fmt.Printf("还原文件成功\n")
|
||||
return
|
||||
}
|
||||
|
||||
if len(rbfr) == 0 && err != nil {
|
||||
fmt.Printf("还原文件失败:%s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// RunRecycleDelete 执行删除回收站文件或目录
|
||||
func RunRecycleDelete(driveId string, fidStrList ...string) {
|
||||
panClient := GetActivePanClient()
|
||||
deleteFileList := []*aliyunpan.FileBatchActionParam{}
|
||||
|
||||
for _, fid := range fidStrList {
|
||||
deleteFileList = append(deleteFileList, &aliyunpan.FileBatchActionParam{
|
||||
DriveId: driveId,
|
||||
FileId: fid,
|
||||
})
|
||||
}
|
||||
|
||||
if len(deleteFileList) == 0 {
|
||||
fmt.Printf("没有需要删除的文件")
|
||||
return
|
||||
}
|
||||
|
||||
rbfr, err := panClient.WebapiPanClient().RecycleBinFileDelete(deleteFileList)
|
||||
if rbfr != nil && len(rbfr) > 0 {
|
||||
fmt.Printf("彻底删除文件成功\n")
|
||||
return
|
||||
}
|
||||
|
||||
if len(rbfr) == 0 && err != nil {
|
||||
fmt.Printf("彻底删除文件失败:%s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// RunRecycleClear 清空回收站
|
||||
func RunRecycleClear(driveId string) {
|
||||
panClient := GetActivePanClient()
|
||||
|
||||
// 提交清空回收站异步任务
|
||||
r, err := panClient.WebapiPanClient().RecycleBinFileClear(&aliyunpan_web.RecycleBinFileClearParam{
|
||||
DriveId: driveId,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Verboseln(err)
|
||||
fmt.Printf("当前无法清空回收站,请稍后重试\n")
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
ar, err1 := panClient.WebapiPanClient().AsyncTaskQueryStatus(&aliyunpan_web.AsyncTaskQueryStatusParam{
|
||||
AsyncTaskId: r.AsyncTaskId,
|
||||
})
|
||||
if err1 != nil {
|
||||
logger.Verboseln(err1)
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
if ar.Status == "Succeed" {
|
||||
fmt.Printf("清空回收站成功\n")
|
||||
return
|
||||
} else {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
fmt.Printf("清空回收站失败,请稍后重试\n")
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apiutil"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
fileItem struct {
|
||||
file *aliyunpan.FileEntity
|
||||
newFileName string
|
||||
}
|
||||
fileArray []*fileItem
|
||||
)
|
||||
|
||||
func newFileItem(file *aliyunpan.FileEntity) *fileItem {
|
||||
return &fileItem{
|
||||
file: file,
|
||||
newFileName: "",
|
||||
}
|
||||
}
|
||||
|
||||
func (x fileArray) Len() int {
|
||||
return len(x)
|
||||
}
|
||||
func (x fileArray) Less(i, j int) bool {
|
||||
return x[i].file.FileName < x[j].file.FileName
|
||||
}
|
||||
func (x fileArray) Swap(i, j int) {
|
||||
x[i], x[j] = x[j], x[i]
|
||||
}
|
||||
|
||||
func CmdRename() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "rename",
|
||||
Usage: "重命名文件",
|
||||
UsageText: `重命名文件:
|
||||
aliyunpan rename <旧文件/目录名> <新文件/目录名>`,
|
||||
Description: `
|
||||
示例:
|
||||
1. 将文件 1.mp4 重命名为 2.mp4
|
||||
aliyunpan rename 1.mp4 2.mp4
|
||||
|
||||
2. 将文件 /test/1.mp4 重命名为 /test/2.mp4
|
||||
要求必须是同一个文件目录内
|
||||
aliyunpan rename /test/1.mp4 /test/2.mp4
|
||||
|
||||
批量重命名,规则:rename [expression] [replacement] [file]
|
||||
其中,
|
||||
expression - 表达式,即文件名中需要去掉的部分,如果为*代表旧的文件名全部去掉
|
||||
replacement - 需要替换的新文件名,如果包含 # 号会按顺序替换成数字编号
|
||||
file - 文件匹配模式,指定需要查找的文件匹配字符串,支持通配符,例如:*.mp4 即当前目录下所有的mp4文件
|
||||
|
||||
3. 批量重命名,将当前目录下所有.mp4文件中包含的字符串 "我的视频" 全部改成 "视频"
|
||||
aliyunpan rename 我的视频 视频 *.mp4
|
||||
|
||||
4. 批量重命名,将当前目录下所有.mp4文件全部进行 "视频+编号.mp4" 的重命名操作,旧的名称全部去掉,即:视频001.mp4, 视频002.mp4, 视频003.mp4...
|
||||
aliyunpan rename * 视频###.mp4 *.mp4
|
||||
|
||||
5. 批量重命名,将当前目录下所有.mp4文件全部进行 "视频+编号.mp4" 的重命名操作,旧的名称全部去掉,直接重命名无需人工确认操作
|
||||
aliyunpan rename -y * 视频###.mp4 *.mp4
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() != 2 && c.NArg() != 3 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if c.NArg() == 2 {
|
||||
RunRename(parseDriveId(c), c.Args().Get(0), c.Args().Get(1))
|
||||
} else if c.NArg() == 3 {
|
||||
// 批量重命名
|
||||
RunRenameBatch(c.Bool("y"), parseDriveId(c), c.Args().Get(0), c.Args().Get(1), c.Args().Get(2))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "y",
|
||||
Usage: "跳过人工确认,对批量操作有效",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunRename(driveId string, oldName string, newName string) {
|
||||
if oldName == "" {
|
||||
fmt.Println("请指定命名文件")
|
||||
return
|
||||
}
|
||||
if newName == "" {
|
||||
fmt.Println("请指定文件新名称")
|
||||
return
|
||||
}
|
||||
activeUser := GetActiveUser()
|
||||
oldName = activeUser.PathJoin(driveId, strings.TrimSpace(oldName))
|
||||
newName = activeUser.PathJoin(driveId, strings.TrimSpace(newName))
|
||||
if path.Dir(oldName) != path.Dir(newName) {
|
||||
fmt.Println("只能命名同一个目录的文件")
|
||||
return
|
||||
}
|
||||
if !apiutil.CheckFileNameValid(path.Base(newName)) {
|
||||
fmt.Println("文件名不能包含特殊字符:" + apiutil.FileNameSpecialChars)
|
||||
return
|
||||
}
|
||||
|
||||
fileId := ""
|
||||
r, err := GetActivePanClient().OpenapiPanClient().FileInfoByPath(driveId, activeUser.PathJoin(driveId, oldName))
|
||||
if err != nil {
|
||||
fmt.Printf("原文件不存在: %s, %s\n", oldName, err)
|
||||
return
|
||||
}
|
||||
fileId = r.FileId
|
||||
|
||||
b, e := activeUser.PanClient().OpenapiPanClient().FileRename(driveId, fileId, path.Base(newName))
|
||||
if e != nil {
|
||||
fmt.Println(e.Err)
|
||||
return
|
||||
}
|
||||
if !b {
|
||||
fmt.Println("重命名文件失败")
|
||||
return
|
||||
}
|
||||
fmt.Printf("重命名文件成功:%s -> %s\n", path.Base(oldName), path.Base(newName))
|
||||
activeUser.DeleteOneCache(path.Dir(newName))
|
||||
}
|
||||
|
||||
// RunRenameBatch 批量重命名文件
|
||||
func RunRenameBatch(skipConfirm bool, driveId string, expression, replacement, filePattern string) {
|
||||
if len(expression) == 0 {
|
||||
fmt.Println("旧文件名不能为空")
|
||||
return
|
||||
}
|
||||
if !apiutil.CheckFileNameValid(replacement) {
|
||||
fmt.Println("新文件名不能包含特殊字符:" + apiutil.FileNameSpecialChars)
|
||||
return
|
||||
}
|
||||
|
||||
if len(filePattern) == 0 {
|
||||
fmt.Println("文件匹配模式不能为空")
|
||||
return
|
||||
}
|
||||
if strings.ContainsAny(filePattern, "/") {
|
||||
fmt.Println("文件匹配模式不能包含路径分隔符")
|
||||
return
|
||||
}
|
||||
if strings.ContainsAny(filePattern, "\\") {
|
||||
fmt.Println("文件匹配模式不能包含路径分隔符")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
absolutePath := path.Clean(activeUser.PathJoin(driveId, filePattern))
|
||||
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
|
||||
if err1 != nil {
|
||||
fmt.Println("查询文件出错:" + err1.Error())
|
||||
return
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
fmt.Println("没有找到符合的文件")
|
||||
return
|
||||
}
|
||||
|
||||
// 文件列表按照名字排序
|
||||
files := fileArray{}
|
||||
for _, f := range fileList {
|
||||
files = append(files, newFileItem(f))
|
||||
}
|
||||
if len(files) > 1 {
|
||||
sort.Sort(files)
|
||||
}
|
||||
|
||||
// 依次处理
|
||||
var index = 1
|
||||
for _, file := range files {
|
||||
replacementStr := replaceNumStr(replacement, index)
|
||||
index += 1
|
||||
if expression == "*" {
|
||||
// 旧的名称全部替换
|
||||
file.newFileName = replacementStr
|
||||
} else {
|
||||
// 替换指定的名称片段
|
||||
file.newFileName = strings.ReplaceAll(file.file.FileName, expression, replacementStr)
|
||||
}
|
||||
}
|
||||
|
||||
// 确认
|
||||
if !skipConfirm {
|
||||
fmt.Printf("以下文件将进行对应的重命名\n\n")
|
||||
idx := 1
|
||||
for _, file := range files {
|
||||
fmt.Printf("%d) %s -> %s\n", idx, file.file.FileName, file.newFileName)
|
||||
idx += 1
|
||||
}
|
||||
fmt.Printf("\n是否进行批量重命名,该操作不可逆(y/n): ")
|
||||
confirm := ""
|
||||
_, err := fmt.Scanln(&confirm)
|
||||
if err != nil || (confirm != "y" && confirm != "Y") {
|
||||
fmt.Println("用户取消了操作")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 重命名
|
||||
for _, file := range files {
|
||||
b, e := activeUser.PanClient().OpenapiPanClient().FileRename(driveId, file.file.FileId, file.newFileName)
|
||||
if e != nil {
|
||||
fmt.Println(e.Err)
|
||||
return
|
||||
}
|
||||
if !b {
|
||||
fmt.Println("重命名文件失败")
|
||||
return
|
||||
}
|
||||
fmt.Printf("重命名文件成功:%s -> %s\n", file.file.FileName, file.newFileName)
|
||||
activeUser.DeleteOneCache(path.Dir(file.file.Path))
|
||||
}
|
||||
}
|
||||
|
||||
// replaceNumStr 将#替换成数字编号
|
||||
func replaceNumStr(name string, num int) string {
|
||||
pattern, _ := regexp.Compile("[#]+")
|
||||
r := pattern.FindAllStringSubmatchIndex(name, -1)
|
||||
if len(r) == 0 {
|
||||
return name
|
||||
}
|
||||
newName := name
|
||||
c := r[0]
|
||||
return replaceNumStr(newName[:c[0]]+generateNumStr(num, c[1]-c[0])+newName[c[1]:], num)
|
||||
}
|
||||
func generateNumStr(num, count int) string {
|
||||
s := strconv.Itoa(num)
|
||||
return generateZeroStr(count-len(s)) + s
|
||||
}
|
||||
func generateZeroStr(count int) string {
|
||||
if count <= 0 {
|
||||
return ""
|
||||
}
|
||||
s := ""
|
||||
for i := 0; i < count; i++ {
|
||||
s += "0"
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CmdRm() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "rm",
|
||||
Usage: "删除文件/目录",
|
||||
UsageText: cmder.App().Name + " rm <文件/目录的路径1> <文件/目录2> <文件/目录3> ...",
|
||||
Description: `
|
||||
注意: 删除多个文件和目录时, 请确保每一个文件和目录都存在, 否则删除操作会失败.
|
||||
被删除的文件或目录可在网盘文件回收站找回。支持通配符匹配删除文件,通配符当前只能匹配文件名,不能匹配文件路径。
|
||||
|
||||
示例:
|
||||
|
||||
删除 /我的资源/1.mp4
|
||||
aliyunpan rm /我的资源/1.mp4
|
||||
|
||||
删除 /我的资源/1.mp4 和 /我的资源/2.mp4
|
||||
aliyunpan rm /我的资源/1.mp4 /我的资源/2.mp4
|
||||
|
||||
删除 /我的资源 整个目录 !!
|
||||
aliyunpan rm /我的资源
|
||||
|
||||
删除 /我的资源 目录下面的所有.zip文件,使用通配符匹配
|
||||
aliyunpan rm /我的资源/*.zip
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() == 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
RunRemove(parseDriveId(c), c.Args()...)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func queryFileListByPath(activeUser *config.PanUser, driveId, filePathPattern string) (files []*aliyunpan.FileEntity, e error) {
|
||||
if !strings.ContainsAny(filePathPattern, aliyunpan.ShellPatternCharacters) {
|
||||
// 没有通配符,直接查询
|
||||
fi, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, filePathPattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, fi)
|
||||
} else {
|
||||
// 包含通配符,使用通配符查询
|
||||
files, e = matchPathByShellPattern(driveId, filePathPattern)
|
||||
}
|
||||
return files, e
|
||||
}
|
||||
|
||||
// RunRemove 执行 批量删除文件/目录
|
||||
func RunRemove(driveId string, paths ...string) {
|
||||
activeUser := GetActiveUser()
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
|
||||
cacheCleanDirs := []string{}
|
||||
failedRmPaths := make([]string, 0, len(paths))
|
||||
successDelFileEntity := []*aliyunpan.FileEntity{}
|
||||
|
||||
for _, p := range paths {
|
||||
absolutePath := path.Clean(activeUser.PathJoin(driveId, p))
|
||||
|
||||
// 查询路径对应的文件信息
|
||||
fileList, err1 := queryFileListByPath(activeUser, driveId, absolutePath)
|
||||
if err1 != nil {
|
||||
failedRmPaths = append(failedRmPaths, absolutePath)
|
||||
continue
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
// 跳过,没有满足条件的文件
|
||||
failedRmPaths = append(failedRmPaths, absolutePath)
|
||||
continue
|
||||
}
|
||||
// 调用插件
|
||||
approvedToRemoveFiles := []*aliyunpan.FileEntity{}
|
||||
pluginParam := &plugins.RemoveFilePrepareParams{
|
||||
Count: len(fileList),
|
||||
Items: make([]*plugins.RemoveFilePrepareItem, 0),
|
||||
}
|
||||
for _, f := range fileList {
|
||||
pluginParam.Items = append(pluginParam.Items, &plugins.RemoveFilePrepareItem{
|
||||
DriveId: f.DriveId,
|
||||
DriveFileId: f.FileId,
|
||||
DriveFileName: f.FileName,
|
||||
DriveFilePath: f.Path,
|
||||
DriveFileSize: f.FileSize,
|
||||
DriveFileType: f.FileType,
|
||||
DriveFileUpdatedAt: f.UpdatedAt,
|
||||
DriveFileCreatedAt: f.CreatedAt,
|
||||
})
|
||||
}
|
||||
if removeFilePrepareResult, er := plugin.RemoveFilePrepareCallback(plugins.GetContext(config.Config.ActiveUser()), pluginParam); er == nil && removeFilePrepareResult != nil {
|
||||
for _, f := range fileList {
|
||||
matchResult := false
|
||||
for _, r := range removeFilePrepareResult.Result {
|
||||
if strings.Compare(f.FileId, r.DriveFileId) == 0 {
|
||||
matchResult = true
|
||||
if strings.Compare("yes", r.RemoveApproved) != 0 {
|
||||
// skip this file
|
||||
fmt.Printf("插件不允许删除该文件: %s\n", f.Path)
|
||||
} else {
|
||||
approvedToRemoveFiles = append(approvedToRemoveFiles, f)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matchResult {
|
||||
// 该文件没有确认结果,则默认删除
|
||||
approvedToRemoveFiles = append(approvedToRemoveFiles, f)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 默认删除全部文件
|
||||
approvedToRemoveFiles = fileList
|
||||
}
|
||||
|
||||
for _, f := range approvedToRemoveFiles {
|
||||
// 删除匹配的文件
|
||||
fdr, err := activeUser.PanClient().OpenapiPanClient().FileDelete(&aliyunpan.FileBatchActionParam{
|
||||
DriveId: driveId,
|
||||
FileId: f.FileId,
|
||||
})
|
||||
if err != nil || !fdr.Success {
|
||||
failedRmPaths = append(failedRmPaths, absolutePath)
|
||||
} else {
|
||||
successDelFileEntity = append(successDelFileEntity, f)
|
||||
}
|
||||
cacheCleanDirs = append(cacheCleanDirs, path.Dir(f.Path))
|
||||
}
|
||||
}
|
||||
|
||||
// output
|
||||
if len(failedRmPaths) > 0 {
|
||||
fmt.Println("以下文件删除失败:")
|
||||
for _, fp := range failedRmPaths {
|
||||
fmt.Println(fp)
|
||||
}
|
||||
fmt.Println("")
|
||||
}
|
||||
pnt := func() {
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "文件/目录"})
|
||||
for k := range successDelFileEntity {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), successDelFileEntity[k].Path})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
if len(successDelFileEntity) > 0 {
|
||||
fmt.Println("操作成功, 以下文件/目录已删除, 可在云盘文件回收站找回: ")
|
||||
pnt()
|
||||
activeUser.DeleteCache(cacheCleanDirs)
|
||||
} else {
|
||||
fmt.Println("本次操作没有删除任何文件")
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func CmdSave() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "save",
|
||||
Usage: "保存分享文件/目录",
|
||||
UsageText: `
|
||||
aliyunpan save <分享链接> (<提取码>) <目标目录>`,
|
||||
Description: `
|
||||
注意: 保存大量文件时, 命令完成后可能还需要额外等待一段时间; 分享的根目录下如果包含大量文件或文件夹, 可能存在不稳定的情况
|
||||
|
||||
示例:
|
||||
|
||||
将 公开分享 保存到 根目录 /
|
||||
aliyunpan save ABCD1234wxyz /
|
||||
aliyunpan save https://www.alipan.com/s/ABCD1234wxyz /
|
||||
|
||||
将 私密分享 保存到 指定目录 /资源分享
|
||||
aliyunpan save ABCD1234wxyz akd1 /资源分享
|
||||
aliyunpan save https://www.alipan.com/s/ABCD1234wxyz akd1 /资源分享
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() <= 1 || c.NArg() > 3 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunSave(parseDriveId(c), c.Args()...)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunSave 保存分享的文件
|
||||
func RunSave(driveId string, args ...string) {
|
||||
activeUser := GetActiveUser()
|
||||
|
||||
targetFilePath := path.Clean(args[len(args)-1])
|
||||
absolutePath := activeUser.PathJoin(driveId, targetFilePath)
|
||||
targetFile, err := activeUser.PanClient().WebapiPanClient().FileInfoByPath(driveId, absolutePath)
|
||||
if err != nil || !targetFile.IsFolder() {
|
||||
fmt.Println("指定目标文件夹不存在")
|
||||
return
|
||||
}
|
||||
fmt.Println("保存文件至:", targetFilePath)
|
||||
|
||||
shareID := args[0]
|
||||
if i := strings.Index(shareID, "alipan.com/s/"); i > 0 {
|
||||
shareID = shareID[i+13:]
|
||||
}
|
||||
sharePwd := ""
|
||||
if len(args) == 3 {
|
||||
sharePwd = args[1]
|
||||
}
|
||||
|
||||
token, err := activeUser.PanClient().WebapiPanClient().GetShareToken(shareID, sharePwd)
|
||||
if err != nil {
|
||||
fmt.Println("读取分享链接失败:", err)
|
||||
return
|
||||
}
|
||||
|
||||
list, err := activeUser.PanClient().WebapiPanClient().GetListByShare(token.ShareToken, shareID, "")
|
||||
if err != nil {
|
||||
fmt.Println("读取分享文件列表失败:", err)
|
||||
return
|
||||
}
|
||||
for list.NextMarker != "" {
|
||||
list2, err := activeUser.PanClient().WebapiPanClient().GetListByShare(token.ShareToken, shareID, "")
|
||||
if err != nil {
|
||||
fmt.Println("读取分享文件列表失败:", err)
|
||||
return
|
||||
}
|
||||
list.Items = append(list.Items, list2.Items...)
|
||||
list.NextMarker = list2.NextMarker
|
||||
}
|
||||
|
||||
var params []*aliyunpan_web.FileSaveParam
|
||||
files := make(map[string]*aliyunpan_web.ListByShareItem)
|
||||
for _, item := range list.Items {
|
||||
if item.FileExtension != "" {
|
||||
fmt.Println(" ", item.Name)
|
||||
} else {
|
||||
fmt.Println(" ", item.Name+"/")
|
||||
}
|
||||
files[item.FileID] = item
|
||||
|
||||
params = append(params, &aliyunpan_web.FileSaveParam{
|
||||
ShareID: shareID,
|
||||
FileId: item.FileID,
|
||||
AutoRename: true,
|
||||
ToDriveId: driveId,
|
||||
ToParentFileId: targetFile.FileId,
|
||||
})
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
result, err := activeUser.PanClient().WebapiPanClient().FileCopy(token.ShareToken, params)
|
||||
if err != nil {
|
||||
fmt.Println("保存分享文件失败:", err)
|
||||
return
|
||||
}
|
||||
var ids []string
|
||||
var failedSaveFileIds []string
|
||||
tasks := make(map[string]string)
|
||||
for _, item := range result {
|
||||
if item.AsyncTaskId == "" {
|
||||
if item.Status != 201 {
|
||||
failedSaveFileIds = append(failedSaveFileIds, item.FileId)
|
||||
}
|
||||
} else {
|
||||
tasks[item.AsyncTaskId] = item.FileId
|
||||
ids = append(ids, item.AsyncTaskId)
|
||||
}
|
||||
}
|
||||
if ids != nil {
|
||||
result2, err := activeUser.PanClient().WebapiPanClient().AsyncTaskGet(token.ShareToken, ids)
|
||||
if err != nil {
|
||||
fmt.Println("读取保存结果失败:", err)
|
||||
}
|
||||
|
||||
for _, item := range result2 {
|
||||
if !item.Success {
|
||||
failedSaveFileIds = append(failedSaveFileIds, tasks[item.AsyncTaskId])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if failedSaveFileIds != nil {
|
||||
fmt.Println("以下文件保存失败:")
|
||||
for _, id := range failedSaveFileIds {
|
||||
if v, ok := files[id]; ok {
|
||||
fmt.Println(v.Name)
|
||||
} else {
|
||||
fmt.Println(id)
|
||||
}
|
||||
}
|
||||
fmt.Println("")
|
||||
}
|
||||
fmt.Println("操作成功, 分享文件已保存到目标目录: ", targetFile.Path)
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CmdShare() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "share",
|
||||
Usage: "分享文件/目录",
|
||||
UsageText: cmder.App().Name + " share",
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
},
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "set",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "设置分享文件/目录",
|
||||
UsageText: cmder.App().Name + " share set <文件/目录1> <文件/目录2> ...",
|
||||
Description: `
|
||||
示例:
|
||||
|
||||
创建文件 1.mp4 的分享链接
|
||||
aliyunpan share set -mode 1 1.mp4
|
||||
|
||||
创建 /我的视频/ 目录下所有mp4文件的分享链接,支持通配符
|
||||
aliyunpan share set -mode 1 /我的视频/*.mp4
|
||||
|
||||
创建文件 1.mp4 的分享链接,并指定分享密码为2333
|
||||
aliyunpan share set -mode 1 -sharePwd 2333 1.mp4
|
||||
|
||||
创建文件 1.mp4 的分享链接,并指定有效期为1天
|
||||
aliyunpan share set -mode 1 -time 1 1.mp4
|
||||
|
||||
创建文件 1.mp4 的快传链接
|
||||
aliyunpan share set 1.mp4
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() < 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 有效期
|
||||
et := ""
|
||||
timeFlag := "0"
|
||||
if c.IsSet("time") {
|
||||
timeFlag = c.String("time")
|
||||
}
|
||||
now := time.Now()
|
||||
if timeFlag == "1" {
|
||||
et = now.Add(time.Duration(1) * time.Hour * 24).Format("2006-01-02 15:04:05")
|
||||
} else if timeFlag == "2" {
|
||||
et = now.Add(time.Duration(7) * time.Hour * 24).Format("2006-01-02 15:04:05")
|
||||
} else {
|
||||
et = ""
|
||||
}
|
||||
|
||||
// 密码
|
||||
sharePwd := ""
|
||||
if c.IsSet("sharePwd") {
|
||||
sharePwd = c.String("sharePwd")
|
||||
}
|
||||
if sharePwd == "" {
|
||||
sharePwd = RandomStr(4)
|
||||
}
|
||||
|
||||
// 模式
|
||||
modeFlag := "3"
|
||||
if c.IsSet("mode") {
|
||||
modeFlag = c.String("mode")
|
||||
}
|
||||
if modeFlag == "1" || modeFlag == "2" {
|
||||
if config.Config.ActiveUser().ActiveDriveId != config.Config.ActiveUser().DriveList.GetResourceDriveId() {
|
||||
// 只有资源库才支持私有、公开分享
|
||||
fmt.Println("只有资源库才支持分享链接,其他请使用快传链接")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if modeFlag == "1" {
|
||||
if sharePwd == "" {
|
||||
sharePwd = RandomStr(4)
|
||||
}
|
||||
} else {
|
||||
sharePwd = ""
|
||||
}
|
||||
|
||||
RunOpenShareSet(modeFlag, parseDriveId(c), c.Args(), et, sharePwd)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "time",
|
||||
Usage: "有效期,0-永久,1-1天,2-7天",
|
||||
Value: "0",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "mode",
|
||||
Usage: "模式,1-私密分享,2-公开分享,3-快传",
|
||||
Value: "3",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "sharePwd",
|
||||
Usage: "自定义私密分享密码,4个字符,没有指定则随机生成",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunOpenShareSet 执行分享
|
||||
func RunOpenShareSet(modeFlag, driveId string, paths []string, expiredTime string, sharePwd string) {
|
||||
if len(paths) <= 0 {
|
||||
fmt.Println("请指定文件路径")
|
||||
return
|
||||
}
|
||||
activeUser := GetActiveUser()
|
||||
panClient := activeUser.PanClient()
|
||||
|
||||
allFileList := []*aliyunpan.FileEntity{}
|
||||
for idx := 0; idx < len(paths); idx++ {
|
||||
absolutePath := path.Clean(activeUser.PathJoin(driveId, paths[idx]))
|
||||
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
|
||||
if err1 != nil {
|
||||
fmt.Println("文件不存在: " + absolutePath)
|
||||
continue
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
// 文件不存在
|
||||
fmt.Println("文件不存在: " + absolutePath)
|
||||
continue
|
||||
}
|
||||
// 匹配的文件
|
||||
allFileList = append(allFileList, fileList...)
|
||||
}
|
||||
|
||||
fidList := []string{}
|
||||
for _, f := range allFileList {
|
||||
fidList = append(fidList, f.FileId)
|
||||
}
|
||||
|
||||
if len(fidList) == 0 {
|
||||
fmt.Printf("没有指定有效的文件\n")
|
||||
return
|
||||
}
|
||||
|
||||
// 创建分享类型
|
||||
if modeFlag == "3" {
|
||||
// 快传
|
||||
r, err1 := panClient.OpenapiPanClient().FastShareLinkCreate(aliyunpan.FastShareCreateParam{
|
||||
DriveId: driveId,
|
||||
FileIdList: fidList,
|
||||
})
|
||||
if err1 != nil || r == nil {
|
||||
if err1.Code == apierror.ApiCodeFileShareNotAllowed {
|
||||
fmt.Printf("创建快传链接失败: 该文件类型不允许分享\n")
|
||||
} else {
|
||||
fmt.Printf("创建快传链接失败: %s\n", err1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("创建快传链接成功\n")
|
||||
fmt.Printf("链接:%s\n", strings.ReplaceAll(r.ShareUrl, "https://www.aliyundrive.com", "https://www.alipan.com"))
|
||||
} else {
|
||||
// 分享
|
||||
r, err1 := panClient.OpenapiPanClient().ShareLinkCreate(aliyunpan.ShareCreateParam{
|
||||
DriveId: driveId,
|
||||
SharePwd: sharePwd,
|
||||
Expiration: expiredTime,
|
||||
FileIdList: fidList,
|
||||
})
|
||||
if err1 != nil || r == nil {
|
||||
if err1.Code == apierror.ApiCodeFileShareNotAllowed {
|
||||
fmt.Printf("创建分享链接失败: 该文件类型不允许分享\n")
|
||||
} else {
|
||||
fmt.Printf("创建分享链接失败: %s\n", err1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if modeFlag == "1" {
|
||||
fmt.Printf("创建私密分享链接成功\n")
|
||||
} else if modeFlag == "2" {
|
||||
fmt.Printf("创建公开分享链接成功\n")
|
||||
}
|
||||
shareUrl := strings.ReplaceAll(r.ShareUrl, "https://www.aliyundrive.com", "https://www.alipan.com")
|
||||
if len(sharePwd) > 0 {
|
||||
fmt.Printf("链接:%s 提取码:%s\n", shareUrl, r.SharePwd)
|
||||
} else {
|
||||
fmt.Printf("链接:%s\n", shareUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// web端分享接口
|
||||
func CmdShareWeb() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "sharew",
|
||||
Usage: "分享文件/目录",
|
||||
UsageText: cmder.App().Name + " sharew",
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
},
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "set",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "设置分享文件/目录",
|
||||
UsageText: cmder.App().Name + " share set <文件/目录1> <文件/目录2> ...",
|
||||
Description: `
|
||||
示例:
|
||||
|
||||
创建文件 1.mp4 的分享链接
|
||||
aliyunpan share set -mode 1 1.mp4
|
||||
|
||||
创建 /我的视频/ 目录下所有mp4文件的分享链接,支持通配符
|
||||
aliyunpan share set -mode 1 /我的视频/*.mp4
|
||||
|
||||
创建文件 1.mp4 的分享链接,并指定分享密码为2333
|
||||
aliyunpan share set -mode 1 -sharePwd 2333 1.mp4
|
||||
|
||||
创建文件 1.mp4 的分享链接,并指定有效期为1天
|
||||
aliyunpan share set -mode 1 -time 1 1.mp4
|
||||
|
||||
创建文件 1.mp4 的快传链接
|
||||
aliyunpan share set 1.mp4
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() < 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
et := ""
|
||||
timeFlag := "0"
|
||||
if c.IsSet("time") {
|
||||
timeFlag = c.String("time")
|
||||
}
|
||||
now := time.Now()
|
||||
if timeFlag == "1" {
|
||||
et = now.Add(time.Duration(1) * time.Hour * 24).Format("2006-01-02 15:04:05")
|
||||
} else if timeFlag == "2" {
|
||||
et = now.Add(time.Duration(7) * time.Hour * 24).Format("2006-01-02 15:04:05")
|
||||
} else {
|
||||
et = ""
|
||||
}
|
||||
|
||||
sharePwd := ""
|
||||
if c.IsSet("sharePwd") {
|
||||
sharePwd = c.String("sharePwd")
|
||||
}
|
||||
|
||||
modeFlag := "3"
|
||||
if c.IsSet("mode") {
|
||||
modeFlag = c.String("mode")
|
||||
}
|
||||
if modeFlag == "1" || modeFlag == "2" {
|
||||
if config.Config.ActiveUser().ActiveDriveId != config.Config.ActiveUser().DriveList.GetResourceDriveId() {
|
||||
// 只有资源库才支持私有、公开分享
|
||||
fmt.Println("只有资源库才支持分享链接,其他请使用快传链接")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if modeFlag == "1" {
|
||||
if sharePwd == "" {
|
||||
sharePwd = RandomStr(4)
|
||||
}
|
||||
} else {
|
||||
sharePwd = ""
|
||||
}
|
||||
RunShareSet(modeFlag, parseDriveId(c), c.Args(), et, sharePwd)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "time",
|
||||
Usage: "有效期,0-永久,1-1天,2-7天",
|
||||
Value: "0",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "mode",
|
||||
Usage: "模式,1-私密分享,2-公开分享,3-快传",
|
||||
Value: "3",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "sharePwd",
|
||||
Usage: "自定义私密分享密码,4个字符,没有指定则随机生成",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "列出已分享文件/目录",
|
||||
UsageText: cmder.App().Name + " share list",
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunShareList()
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "cancel",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "取消分享文件/目录",
|
||||
UsageText: cmder.App().Name + " share cancel <shareid_1> <shareid_2> ...",
|
||||
Description: `目前只支持通过分享id (shareid) 来取消分享.`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
if c.NArg() < 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
RunShareCancel(c.Args())
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "export",
|
||||
Usage: "导出分享记录保存到文件",
|
||||
UsageText: cmder.App().Name + " share export <csv file path>",
|
||||
Description: `
|
||||
导出分享记录,并保存到指定的文件。目前支持csv格式
|
||||
|
||||
示例:
|
||||
导出所有有效的分享并保存成文件
|
||||
aliyunpan share export "d:\myfoler\share_list.csv"
|
||||
|
||||
导出所有的分享并保存成文件
|
||||
aliyunpan share export -option 2 "d:\myfoler\share_list.csv"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
if c.NArg() < 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
opt := c.String("option")
|
||||
if opt == "" {
|
||||
opt = "1"
|
||||
}
|
||||
filePath := c.Args()[0]
|
||||
RunShareExport(opt, filePath)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "option",
|
||||
Usage: "导出选项,1-有效分享 2-全部分享",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunShareSet 执行分享
|
||||
func RunShareSet(modeFlag, driveId string, paths []string, expiredTime string, sharePwd string) {
|
||||
if len(paths) <= 0 {
|
||||
fmt.Println("请指定文件路径")
|
||||
return
|
||||
}
|
||||
activeUser := GetActiveUser()
|
||||
panClient := activeUser.PanClient()
|
||||
|
||||
allFileList := []*aliyunpan.FileEntity{}
|
||||
for idx := 0; idx < len(paths); idx++ {
|
||||
absolutePath := path.Clean(activeUser.PathJoin(driveId, paths[idx]))
|
||||
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
|
||||
if err1 != nil {
|
||||
fmt.Println("文件不存在: " + absolutePath)
|
||||
continue
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
// 文件不存在
|
||||
fmt.Println("文件不存在: " + absolutePath)
|
||||
continue
|
||||
}
|
||||
// 匹配的文件
|
||||
allFileList = append(allFileList, fileList...)
|
||||
}
|
||||
|
||||
fidList := []string{}
|
||||
for _, f := range allFileList {
|
||||
fidList = append(fidList, f.FileId)
|
||||
}
|
||||
|
||||
if len(fidList) == 0 {
|
||||
fmt.Printf("没有指定有效的文件\n")
|
||||
return
|
||||
}
|
||||
|
||||
if modeFlag == "3" {
|
||||
// 快传
|
||||
r, err1 := panClient.WebapiPanClient().FastShareLinkCreate(aliyunpan.FastShareCreateParam{
|
||||
DriveId: driveId,
|
||||
FileIdList: fidList,
|
||||
})
|
||||
if err1 != nil || r == nil {
|
||||
if err1.Code == apierror.ApiCodeFileShareNotAllowed {
|
||||
fmt.Printf("创建快传链接失败: 该文件类型不允许分享\n")
|
||||
} else {
|
||||
fmt.Printf("创建快传链接失败: %s\n", err1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("创建快传链接成功\n")
|
||||
fmt.Printf("链接:%s\n", r.ShareUrl)
|
||||
} else {
|
||||
// 分享
|
||||
r, err1 := panClient.WebapiPanClient().ShareLinkCreate(aliyunpan.ShareCreateParam{
|
||||
DriveId: driveId,
|
||||
SharePwd: sharePwd,
|
||||
Expiration: expiredTime,
|
||||
FileIdList: fidList,
|
||||
})
|
||||
if err1 != nil || r == nil {
|
||||
if err1.Code == apierror.ApiCodeFileShareNotAllowed {
|
||||
fmt.Printf("创建分享链接失败: 该文件类型不允许分享\n")
|
||||
} else {
|
||||
fmt.Printf("创建分享链接失败: %s\n", err1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("创建分享链接成功\n")
|
||||
if len(sharePwd) > 0 {
|
||||
fmt.Printf("链接:%s 提取码:%s\n", r.ShareUrl, r.SharePwd)
|
||||
} else {
|
||||
fmt.Printf("链接:%s\n", r.ShareUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RunShareList 执行列出分享列表
|
||||
func RunShareList() {
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().WebapiPanClient().ShareLinkList(activeUser.UserId)
|
||||
if err != nil {
|
||||
fmt.Printf("获取分享列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "ShARE_ID", "分享链接", "提取码", "文件名", "过期时间", "状态"})
|
||||
now := time.Now()
|
||||
for k, record := range records {
|
||||
et := "永久有效"
|
||||
if len(record.Expiration) > 0 {
|
||||
et = record.Expiration
|
||||
}
|
||||
status := "有效"
|
||||
if record.Status == "enabled" {
|
||||
if record.FirstFile == nil {
|
||||
status = "已删除"
|
||||
} else {
|
||||
cz := time.FixedZone("CST", 8*3600)
|
||||
if len(record.Expiration) > 0 {
|
||||
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", record.Expiration, cz)
|
||||
if expiredTime.Unix() < now.Unix() {
|
||||
status = "已过期"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if record.Status == "forbidden" {
|
||||
status = "违规"
|
||||
}
|
||||
|
||||
tb.Append([]string{strconv.Itoa(k + 1), record.ShareId, record.ShareUrl, record.SharePwd,
|
||||
record.ShareName,
|
||||
//record.FileIdList[0],
|
||||
et,
|
||||
status})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
|
||||
// RunShareCancel 执行取消分享
|
||||
func RunShareCancel(shareIdList []string) {
|
||||
if len(shareIdList) == 0 {
|
||||
fmt.Printf("取消分享操作失败, 没有任何 shareid\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
r, err := activeUser.PanClient().WebapiPanClient().ShareLinkCancel(shareIdList)
|
||||
if err != nil {
|
||||
fmt.Printf("取消分享操作失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if r != nil && len(r) > 0 {
|
||||
fmt.Printf("取消分享操作成功\n")
|
||||
} else {
|
||||
fmt.Printf("取消分享操作失败\n")
|
||||
}
|
||||
}
|
||||
|
||||
func RunShareExport(option, saveFilePath string) {
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().WebapiPanClient().ShareLinkList(activeUser.UserId)
|
||||
if err != nil {
|
||||
fmt.Printf("获取分享列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
columns := [][]string{{"序号", "分享ID", "分享链接", "提取码", "文件名", "过期时间", "状态"}}
|
||||
now := time.Now()
|
||||
idx := 1
|
||||
for _, record := range records {
|
||||
et := "永久有效"
|
||||
if len(record.Expiration) > 0 {
|
||||
et = record.Expiration
|
||||
}
|
||||
status := "有效"
|
||||
if record.FirstFile == nil {
|
||||
status = "已删除"
|
||||
if option == "1" {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
cz := time.FixedZone("CST", 8*3600)
|
||||
if len(record.Expiration) > 0 {
|
||||
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", record.Expiration, cz)
|
||||
if expiredTime.Unix() < now.Unix() {
|
||||
status = "已过期"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if option == "1" {
|
||||
if status == "已过期" || status == "已删除" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
line := []string{strconv.Itoa(idx), record.ShareId, record.ShareUrl, record.SharePwd, record.ShareName, et, status}
|
||||
idx += 1
|
||||
columns = append(columns, line)
|
||||
}
|
||||
|
||||
// save to file
|
||||
if ExportCsv(saveFilePath, columns) {
|
||||
fmt.Println("分享导出成功:", saveFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
func ExportCsv(savePath string, data [][]string) bool {
|
||||
folder := filepath.Dir(savePath)
|
||||
if _, err := os.Stat(folder); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
os.MkdirAll(folder, os.ModePerm)
|
||||
}
|
||||
}
|
||||
fp, err := os.Create(savePath) // 创建文件句柄
|
||||
if err != nil {
|
||||
fmt.Println("创建文件["+savePath+"]失败,%v", err)
|
||||
return false
|
||||
}
|
||||
defer fp.Close()
|
||||
fp.WriteString("\xEF\xBB\xBF") // 写入UTF-8 BOM
|
||||
w := csv.NewWriter(fp) //创建一个新的写入文件流
|
||||
w.WriteAll(data)
|
||||
w.Flush()
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"github.com/tickstep/aliyunpan/internal/syncdrive"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CmdSync() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "sync",
|
||||
Usage: "同步备份功能(Beta)",
|
||||
UsageText: cmder.App().Name + " sync",
|
||||
Description: `
|
||||
备份功能。支持备份本地文件到云盘,备份云盘文件到本地两种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
指定本地目录和对应的一个网盘目录,以备份文件。网盘目录必须和本地目录独占使用,不要用作其他用途,不然备份可能会有问题。
|
||||
|
||||
备份功能支持以下模式:
|
||||
1. upload
|
||||
备份本地文件,即上传本地文件到网盘,始终保持本地文件有一个完整的备份在网盘
|
||||
2. download
|
||||
备份云盘文件,即下载网盘文件到本地,始终保持网盘的文件有一个完整的备份在本地
|
||||
|
||||
请输入以下命令查看如何配置和启动:
|
||||
aliyunpan sync start -h
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
},
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "start",
|
||||
Usage: "启动sync同步备份任务",
|
||||
UsageText: cmder.App().Name + " sync start [arguments...]",
|
||||
Description: `
|
||||
备份本地文件到文件网盘,或者备份文件网盘的文件到本地。支持命令行配置启动或者使用备份配置文件启动同步备份任务。
|
||||
|
||||
配置文件保存在:(配置目录)/sync_drive/sync_drive_config.json,样例如下:
|
||||
{
|
||||
"configVer": "1.0",
|
||||
"syncTaskList": [
|
||||
{
|
||||
"name": "设计文档备份",
|
||||
"localFolderPath": "D:/tickstep/Documents/设计文档",
|
||||
"panFolderPath": "/sync_drive/我的文档",
|
||||
"mode": "upload",
|
||||
"policy": "increment",
|
||||
"driveName": "backup"
|
||||
}
|
||||
]
|
||||
}
|
||||
相关字段说明如下:
|
||||
name - 任务名称
|
||||
localFolderPath - 本地目录
|
||||
panFolderPath - 网盘目录
|
||||
mode - 备份模式,支持两种: upload(备份本地文件到云盘),download(备份云盘文件到本地)
|
||||
policy - 备份策略, 支持两种: exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)
|
||||
driveName - 网盘名称,backup(备份盘),resource(资源盘)
|
||||
|
||||
例子:
|
||||
1. 查看帮助
|
||||
aliyunpan sync start -h
|
||||
|
||||
2. 使用命令行配置启动同步备份服务,将本地目录 D:\tickstep\Documents\设计文档 中的文件备份上传到"备份盘"的云盘目录 /sync_drive/我的文档
|
||||
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "upload" -drive "backup"
|
||||
|
||||
3. 使用命令行配置启动同步备份服务,将云盘目录 /sync_drive/我的文档 中的文件备份下载到本地目录 D:\tickstep\Documents\设计文档
|
||||
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "download"
|
||||
|
||||
4. 使用命令行配置启动同步备份服务,将本地目录 D:\tickstep\Documents\设计文档 中的文件备份到云盘目录 /sync_drive/我的文档
|
||||
同时配置下载并发为2,上传并发为1,下载分片大小为256KB,上传分片大小为1MB
|
||||
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "upload" -dp 2 -up 1 -dbs 256 -ubs 1024
|
||||
|
||||
5. 使用配置文件启动同步备份服务,使用配置文件可以支持同时启动多个备份任务。配置文件必须存在,否则启动失败。
|
||||
aliyunpan sync start
|
||||
|
||||
6. 使用配置文件启动同步备份服务,并配置下载并发为2,上传并发为1,下载分片大小为256KB,上传分片大小为1MB
|
||||
aliyunpan sync start -dp 2 -up 1 -dbs 256 -ubs 1024
|
||||
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
activeUser := GetActiveUser()
|
||||
|
||||
if c.String("log") == "true" {
|
||||
syncdrive.LogPrompt = true
|
||||
} else {
|
||||
syncdrive.LogPrompt = false
|
||||
}
|
||||
|
||||
dp := c.Int("dp")
|
||||
if dp == 0 {
|
||||
dp = config.Config.MaxDownloadParallel
|
||||
}
|
||||
if dp == 0 {
|
||||
dp = 2
|
||||
}
|
||||
|
||||
up := c.Int("up")
|
||||
if up == 0 {
|
||||
up = config.Config.MaxUploadParallel
|
||||
}
|
||||
if up == 0 {
|
||||
up = 2
|
||||
}
|
||||
|
||||
downloadBlockSize := int64(c.Int("dbs") * 1024)
|
||||
if downloadBlockSize == 0 {
|
||||
downloadBlockSize = int64(config.Config.CacheSize)
|
||||
}
|
||||
if downloadBlockSize == 0 {
|
||||
downloadBlockSize = int64(256 * 1024)
|
||||
}
|
||||
|
||||
uploadBlockSize := int64(c.Int("ubs") * 1024)
|
||||
if uploadBlockSize == 0 {
|
||||
uploadBlockSize = aliyunpan.DefaultChunkSize
|
||||
}
|
||||
|
||||
var syncOpt syncdrive.SyncPriorityOption = syncdrive.SyncPriorityTimestampFirst
|
||||
//opt := c.String("pri")
|
||||
//if opt == "local" {
|
||||
// syncOpt = syncdrive.SyncPriorityLocalFirst
|
||||
//} else if opt == "pan" {
|
||||
// syncOpt = syncdrive.SyncPriorityPanFirst
|
||||
//} else {
|
||||
// syncOpt = syncdrive.SyncPriorityTimestampFirst
|
||||
//}
|
||||
|
||||
var task *syncdrive.SyncTask
|
||||
localDir := c.String("ldir")
|
||||
panDir := c.String("pdir")
|
||||
mode := c.String("mode")
|
||||
policy := c.String("policy")
|
||||
driveName := c.String("drive")
|
||||
if localDir != "" && panDir != "" {
|
||||
// make path absolute
|
||||
if !utils.IsLocalAbsPath(localDir) {
|
||||
pwd, _ := os.Getwd()
|
||||
localDir = path.Join(pwd, path.Clean(localDir))
|
||||
}
|
||||
panDir = activeUser.PathJoin(activeUser.ActiveDriveId, panDir)
|
||||
if !utils.IsLocalAbsPath(localDir) {
|
||||
fmt.Println("本地目录请指定绝对路径")
|
||||
return nil
|
||||
}
|
||||
if !utils.IsPanAbsPath(panDir) {
|
||||
fmt.Println("网盘目录请指定绝对路径")
|
||||
return nil
|
||||
}
|
||||
//if b, e := utils.PathExists(localDir); e == nil {
|
||||
// if !b {
|
||||
// fmt.Println("本地文件夹不存在:", localDir)
|
||||
// return nil
|
||||
// }
|
||||
//} else {
|
||||
// fmt.Println("本地文件夹不存在:", localDir)
|
||||
// return nil
|
||||
//}
|
||||
task = &syncdrive.SyncTask{}
|
||||
task.LocalFolderPath = path.Clean(strings.ReplaceAll(localDir, "\\", "/"))
|
||||
task.PanFolderPath = panDir
|
||||
task.Mode = syncdrive.Upload
|
||||
if mode == string(syncdrive.Upload) {
|
||||
task.Mode = syncdrive.Upload
|
||||
} else if mode == string(syncdrive.Download) {
|
||||
task.Mode = syncdrive.Download
|
||||
} else if mode == string(syncdrive.SyncTwoWay) {
|
||||
task.Mode = syncdrive.SyncTwoWay
|
||||
} else {
|
||||
task.Mode = syncdrive.Upload
|
||||
}
|
||||
if policy == string(syncdrive.SyncPolicyExclusive) {
|
||||
task.Policy = syncdrive.SyncPolicyExclusive
|
||||
} else {
|
||||
task.Policy = syncdrive.SyncPolicyIncrement
|
||||
}
|
||||
task.Name = path.Base(task.LocalFolderPath)
|
||||
task.Id = utils.Md5Str(task.LocalFolderPath)
|
||||
task.Priority = syncOpt
|
||||
task.UserId = activeUser.UserId
|
||||
|
||||
// drive id
|
||||
task.DriveName = driveName
|
||||
if strings.ToLower(task.DriveName) == "backup" {
|
||||
task.DriveId = activeUser.DriveList.GetFileDriveId()
|
||||
} else if strings.ToLower(task.DriveName) == "resource" {
|
||||
task.DriveId = activeUser.DriveList.GetResourceDriveId()
|
||||
}
|
||||
if len(task.DriveId) == 0 {
|
||||
task.DriveName = "backup"
|
||||
task.DriveId = activeUser.DriveList.GetFileDriveId()
|
||||
}
|
||||
}
|
||||
|
||||
cycleMode := syncdrive.CycleInfiniteLoop
|
||||
if c.String("cycle") == "onetime" {
|
||||
cycleMode = syncdrive.CycleOneTime
|
||||
} else {
|
||||
cycleMode = syncdrive.CycleInfiniteLoop
|
||||
}
|
||||
scanIntervalTime := int64(c.Int("sit") * 60)
|
||||
if scanIntervalTime == 0 {
|
||||
// 默认1分钟
|
||||
scanIntervalTime = 60
|
||||
}
|
||||
RunSync(task, cycleMode, dp, up, downloadBlockSize, uploadBlockSize, syncOpt, c.Int("ldt"), scanIntervalTime)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "drive",
|
||||
Usage: "drive name, 网盘名称,backup(备份盘),resource(资源盘)",
|
||||
Value: "backup",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "ldir",
|
||||
Usage: "local dir, 本地文件夹完整路径",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "pdir",
|
||||
Usage: "pan dir, 云盘文件夹完整路径",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "mode",
|
||||
Usage: "备份模式, 支持两种: upload(备份本地文件到云盘),download(备份云盘文件到本地)",
|
||||
Value: "upload",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "policy",
|
||||
Usage: "备份策略, 支持两种: exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)",
|
||||
Value: "increment",
|
||||
},
|
||||
//cli.StringFlag{
|
||||
// Name: "pri",
|
||||
// Usage: "同步优先级,只对sync模式有效。当网盘和本地存在同名文件,优先使用哪个,选项支持三种: time-时间优先,local-本地优先,pan-网盘优先",
|
||||
// Value: "time",
|
||||
//},
|
||||
cli.StringFlag{
|
||||
Name: "cycle",
|
||||
Usage: "备份周期, 支持两种: infinity(永久循环备份),onetime(只运行一次备份)",
|
||||
Value: "infinity",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "dp",
|
||||
Usage: "download parallel, 下载并发数量,即可以同时并发下载多少个文件。0代表跟从配置文件设置(取值范围:1 ~ 10)",
|
||||
Value: 0,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "up",
|
||||
Usage: "upload parallel, 上传并发数量,即可以同时并发上传多少个文件。0代表跟从配置文件设置(取值范围:1 ~ 10)",
|
||||
Value: 0,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "dbs",
|
||||
Usage: "download block size,下载分片大小,单位KB。推荐值:1024 ~ 10240",
|
||||
Value: 1024,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "ubs",
|
||||
Usage: "upload block size,上传分片大小,单位KB。推荐值:1024 ~ 10240。当上传极大单文件时候请适当调高该值",
|
||||
Value: 10240,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "log",
|
||||
Usage: "是否显示文件备份过程日志,true-显示,false-不显示",
|
||||
Value: "false",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "ldt",
|
||||
Usage: "local delay time,本地文件修改检测延迟间隔,单位秒。如果本地文件会被频繁修改,例如录制视频文件,配置好该时间可以避免上传未录制好的文件。",
|
||||
Value: 3,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "sit",
|
||||
Usage: "scan interval time,扫描文件间隔时间,单位:分钟。",
|
||||
Value: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunSync(defaultTask *syncdrive.SyncTask, cycleMode syncdrive.CycleMode, fileDownloadParallel, fileUploadParallel int, downloadBlockSize, uploadBlockSize int64,
|
||||
flag syncdrive.SyncPriorityOption, localDelayTime int, scanTimeInterval int64) {
|
||||
maxDownloadRate := config.Config.MaxDownloadRate
|
||||
maxUploadRate := config.Config.MaxUploadRate
|
||||
activeUser := GetActiveUser()
|
||||
panClient := activeUser.PanClient()
|
||||
panClient.OpenapiPanClient().ClearCache()
|
||||
panClient.OpenapiPanClient().DisableCache()
|
||||
|
||||
//// pan token expired checker
|
||||
//continueFlag := int32(0)
|
||||
//atomic.StoreInt32(&continueFlag, 0)
|
||||
//defer func() {
|
||||
// atomic.StoreInt32(&continueFlag, 1)
|
||||
//}()
|
||||
//go func(flag *int32) {
|
||||
// for atomic.LoadInt32(flag) == 0 {
|
||||
// time.Sleep(time.Duration(1) * time.Minute)
|
||||
// if RefreshWebTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
// logger.Verboseln("update access token for sync task")
|
||||
// userWebToken := NewWebLoginToken(activeUser.WebapiToken.AccessToken, activeUser.WebapiToken.Expired)
|
||||
// panClient.WebapiPanClient().UpdateToken(userWebToken)
|
||||
// }
|
||||
// }
|
||||
//}(&continueFlag)
|
||||
|
||||
syncFolderRootPath := config.GetSyncDriveDir()
|
||||
if b, e := utils.PathExists(syncFolderRootPath); e == nil {
|
||||
if !b {
|
||||
os.MkdirAll(syncFolderRootPath, 0755)
|
||||
}
|
||||
}
|
||||
|
||||
var tasks []*syncdrive.SyncTask
|
||||
if defaultTask != nil {
|
||||
tasks = []*syncdrive.SyncTask{}
|
||||
tasks = append(tasks, defaultTask)
|
||||
}
|
||||
|
||||
fmt.Println("启动同步备份进程")
|
||||
|
||||
// 文件同步记录器
|
||||
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/sync_file_records.csv")
|
||||
|
||||
option := syncdrive.SyncOption{
|
||||
FileDownloadParallel: fileDownloadParallel,
|
||||
FileUploadParallel: fileUploadParallel,
|
||||
FileDownloadBlockSize: downloadBlockSize,
|
||||
FileUploadBlockSize: uploadBlockSize,
|
||||
MaxDownloadRate: maxDownloadRate,
|
||||
MaxUploadRate: maxUploadRate,
|
||||
SyncPriority: flag,
|
||||
LocalFileModifiedCheckIntervalSec: localDelayTime,
|
||||
FileRecorder: fileRecorder,
|
||||
}
|
||||
syncMgr := syncdrive.NewSyncTaskManager(activeUser, panClient, syncFolderRootPath, option)
|
||||
syncConfigFile := syncMgr.ConfigFilePath()
|
||||
if tasks != nil {
|
||||
syncConfigFile = "(使用命令行配置)"
|
||||
}
|
||||
fmt.Printf("备份配置文件:%s\n下载并发:%d\n上传并发:%d\n下载分片大小:%s\n上传分片大小:%s\n",
|
||||
syncConfigFile, fileDownloadParallel, fileUploadParallel, converter.ConvertFileSize(downloadBlockSize, 2),
|
||||
converter.ConvertFileSize(uploadBlockSize, 2))
|
||||
if _, e := syncMgr.Start(tasks, cycleMode, scanTimeInterval); e != nil {
|
||||
fmt.Println("启动任务失败:", e)
|
||||
return
|
||||
}
|
||||
|
||||
_, ok := os.LookupEnv("ALIYUNPAN_DOCKER")
|
||||
if ok {
|
||||
// in docker container
|
||||
if cycleMode == syncdrive.CycleInfiniteLoop {
|
||||
// 使用休眠以节省CPU资源
|
||||
fmt.Println("本命令不会退出,程序正在以Docker的方式运行。如需退出请借助Docker提供的方式。")
|
||||
for {
|
||||
time.Sleep(60 * time.Second)
|
||||
}
|
||||
} else {
|
||||
for {
|
||||
if syncMgr.IsAllTaskCompletely() {
|
||||
fmt.Println("所有备份任务已完成")
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
syncMgr.DoTaskSyncCompletelyPluginCallback()
|
||||
}
|
||||
} else {
|
||||
if cycleMode == syncdrive.CycleInfiniteLoop {
|
||||
if global.IsAppInCliMode {
|
||||
// in cmd mode
|
||||
c := ""
|
||||
fmt.Println("本命令不会退出,如需要结束同步备份进程请输入y,然后按Enter键进行停止。")
|
||||
for strings.ToLower(c) != "y" {
|
||||
fmt.Scan(&c)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("本命令不会退出,程序正在以非交互的方式运行。如需退出请借助运行环境提供的方式。")
|
||||
logger.Verboseln("App not in CLI mode, not need to listen to input stream")
|
||||
for {
|
||||
time.Sleep(60 * time.Second)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for {
|
||||
if syncMgr.IsAllTaskCompletely() {
|
||||
fmt.Println("所有备份任务已完成")
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
syncMgr.DoTaskSyncCompletelyPluginCallback()
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("正在退出同步备份任务,请稍等...")
|
||||
|
||||
// stop task
|
||||
syncMgr.Stop()
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/urfave/cli"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CmdTree() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "tree",
|
||||
Usage: "列出目录的树形图",
|
||||
UsageText: cmder.App().Name + " tree <目录>",
|
||||
Description: `
|
||||
列出指定目录内的文件和目录, 并以树形图的方式呈现
|
||||
|
||||
示例:
|
||||
|
||||
列出 当前工作目录 内的文件和目录的树形图
|
||||
aliyunpan tree
|
||||
|
||||
列出 /我的资源 内的文件和目录的树形图
|
||||
aliyunpan tree /我的资源
|
||||
|
||||
列出 /我的资源 内的文件和目录的树形图,使用通配符
|
||||
aliyunpan tree /我的*
|
||||
|
||||
列出 /我的资源 内的文件和目录的树形图,并且显示文件对应的完整绝对路径
|
||||
aliyunpan tree -fp /我的资源
|
||||
|
||||
列出 /我的资源 内的文件和目录的树形图,并且显示文件对应的文件大小
|
||||
aliyunpan tree -fs /我的资源
|
||||
|
||||
列出 /我的资源 内的文件和目录的树形图,过滤大于等于 10kb 并且小于等于 10mb 的文件,同时显示文件对应的文件大小
|
||||
aliyunpan tree -fs -minSize=1kb -maxSize=10mb /我的资源
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
minSize := int64(0)
|
||||
if c.IsSet("minSize") {
|
||||
if s, e := converter.ParseFileSizeStr(c.String("minSize")); e == nil {
|
||||
minSize = s
|
||||
}
|
||||
}
|
||||
maxSize := int64(0)
|
||||
if c.IsSet("maxSize") {
|
||||
if s, e := converter.ParseFileSizeStr(c.String("maxSize")); e == nil {
|
||||
maxSize = s
|
||||
}
|
||||
}
|
||||
RunTree(parseDriveId(c), c.Args().Get(0), c.Bool("fp"), c.Bool("fs"), minSize, maxSize)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "fp",
|
||||
Usage: "full path, 树形图显示文件的完整路径",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "fs",
|
||||
Usage: "file size, 树形图显示文件的文件大小",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "minSize",
|
||||
Usage: "min size, 过滤大于等于指定大小的文件,例如:100mb",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "maxSize",
|
||||
Usage: "max size, 过滤小于等于指定大小的文件,例如:1gb",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
indentPrefix = "│ "
|
||||
pathPrefix = "├──"
|
||||
lastFilePrefix = "└──"
|
||||
)
|
||||
|
||||
type (
|
||||
treeStatistic struct {
|
||||
CountOfDir int64
|
||||
CountOfFile int64
|
||||
SizeOfFile int64
|
||||
}
|
||||
|
||||
treeConfig struct {
|
||||
showFullPath bool
|
||||
showFileSize bool
|
||||
minFileSize int64
|
||||
maxFileSize int64
|
||||
}
|
||||
)
|
||||
|
||||
func getTree(driveId, pathStr string, depth int, statistic *treeStatistic, setting *treeConfig) {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
pathStr = activeUser.PathJoin(driveId, pathStr)
|
||||
pathStr = path.Clean(pathStr)
|
||||
|
||||
// 获取目标路径文件信息
|
||||
targetPathInfo, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, pathStr)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 适配通配符路径获取目标文件信息(弃用,容易触发风控)
|
||||
//files, err := matchPathByShellPattern(driveId, pathStr)
|
||||
//if err != nil {
|
||||
// fmt.Println(err)
|
||||
// return
|
||||
//}
|
||||
//var targetPathInfo *aliyunpan.FileEntity
|
||||
//if len(files) == 1 {
|
||||
// targetPathInfo = files[0]
|
||||
//} else {
|
||||
// for _, f := range files {
|
||||
// if f.IsFolder() {
|
||||
// targetPathInfo = f
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
if targetPathInfo == nil {
|
||||
fmt.Println("路径不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if depth == 0 {
|
||||
fmt.Printf("%s\n", targetPathInfo.Path)
|
||||
}
|
||||
|
||||
fileList := aliyunpan.FileList{}
|
||||
fileListParam := &aliyunpan.FileListParam{}
|
||||
fileListParam.ParentFileId = targetPathInfo.FileId
|
||||
fileListParam.DriveId = driveId
|
||||
fileListParam.OrderBy = aliyunpan.FileOrderByName
|
||||
fileListParam.OrderDirection = aliyunpan.FileOrderDirectionAsc
|
||||
if targetPathInfo.IsFolder() {
|
||||
fileResult, err := activeUser.PanClient().OpenapiPanClient().FileListGetAll(fileListParam, 500)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fileList = append(fileList, fileResult...)
|
||||
} else {
|
||||
fileList = append(fileList, targetPathInfo)
|
||||
}
|
||||
|
||||
var (
|
||||
prefix = pathPrefix
|
||||
fN = len(fileList)
|
||||
indentPrefixStr = strings.Repeat(indentPrefix, depth)
|
||||
)
|
||||
for i, file := range fileList {
|
||||
if file.IsFolder() {
|
||||
statistic.CountOfDir += 1
|
||||
if setting.showFullPath {
|
||||
fmt.Printf("%v%v %v/ -> %s\n", indentPrefixStr, pathPrefix, file.FileName, targetPathInfo.Path+"/"+file.FileName)
|
||||
} else {
|
||||
fmt.Printf("%v%v %v/\n", indentPrefixStr, pathPrefix, file.FileName)
|
||||
}
|
||||
getTree(driveId, targetPathInfo.Path+"/"+file.FileName, depth+1, statistic, setting)
|
||||
continue
|
||||
}
|
||||
|
||||
// filter file size
|
||||
if setting.minFileSize > 0 {
|
||||
if file.FileSize < setting.minFileSize {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if setting.maxFileSize > 0 {
|
||||
if file.FileSize > setting.maxFileSize {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
statistic.CountOfFile += 1
|
||||
statistic.SizeOfFile += file.FileSize
|
||||
|
||||
if i+1 == fN {
|
||||
prefix = lastFilePrefix
|
||||
}
|
||||
|
||||
// 文件大小
|
||||
fileName := &strings.Builder{}
|
||||
if setting.showFileSize {
|
||||
fmt.Fprintf(fileName, "%s (%s)", file.FileName, converter.ConvertFileSize(file.FileSize, 2))
|
||||
} else {
|
||||
fmt.Fprintf(fileName, "%s", file.FileName)
|
||||
}
|
||||
|
||||
// 文件完整路径
|
||||
if setting.showFullPath {
|
||||
fmt.Printf("%v%v %v -> %s\n", indentPrefixStr, prefix, fileName.String(), targetPathInfo.Path+"/"+file.FileName)
|
||||
} else {
|
||||
fmt.Printf("%v%v %v\n", indentPrefixStr, prefix, fileName.String())
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// RunTree 列出树形图
|
||||
func RunTree(driveId, pathStr string, showFullPath, showFileSize bool, minSize, maxSize int64) {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
activeUser.PanClient().OpenapiPanClient().ClearCache()
|
||||
activeUser.PanClient().OpenapiPanClient().EnableCache()
|
||||
defer activeUser.PanClient().OpenapiPanClient().DisableCache()
|
||||
pathStr = activeUser.PathJoin(driveId, pathStr)
|
||||
statistic := &treeStatistic{
|
||||
CountOfDir: 0,
|
||||
CountOfFile: 0,
|
||||
SizeOfFile: 0,
|
||||
}
|
||||
setting := &treeConfig{
|
||||
showFullPath: showFullPath,
|
||||
showFileSize: showFileSize,
|
||||
minFileSize: minSize,
|
||||
maxFileSize: maxSize,
|
||||
}
|
||||
getTree(driveId, pathStr, 0, statistic, setting)
|
||||
fmt.Printf("\n%d 个文件夹, %d 个文件, %s 总大小\n", statistic.CountOfDir, statistic.CountOfFile, converter.ConvertFileSize(statistic.SizeOfFile, 2))
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/tickstep/aliyunpan/internal/ui"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
|
||||
"github.com/tickstep/library-go/logger"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdutil"
|
||||
|
||||
"github.com/oleiade/lane"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panupload"
|
||||
"github.com/tickstep/aliyunpan/internal/localfile"
|
||||
"github.com/tickstep/aliyunpan/internal/taskframework"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultUploadMaxAllParallel 默认所有文件并发上传数量,即可以同时并发上传多少个文件
|
||||
DefaultUploadMaxAllParallel = 1
|
||||
// DefaultUploadMaxRetry 默认上传失败最大重试次数
|
||||
DefaultUploadMaxRetry = 3
|
||||
)
|
||||
|
||||
type (
|
||||
// UploadOptions 上传可选项
|
||||
UploadOptions struct {
|
||||
AllParallel int // 所有文件并发上传数量,即可以同时并发上传多少个文件
|
||||
Parallel int // 单个文件并发上传数量
|
||||
MaxRetry int
|
||||
MaxTimeoutSec int // http请求超时时间,单位秒
|
||||
NoRapidUpload bool
|
||||
ShowProgress bool
|
||||
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
|
||||
IsSkipSameName bool // 跳过已存在的文件,即使文件内容不一致(不检查SHA1)
|
||||
DriveId string
|
||||
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行上传,支持正则表达式
|
||||
BlockSize int64 // 分片大小
|
||||
IsUseUIDashboard bool // 是否使用UI面板显示上传进度
|
||||
}
|
||||
)
|
||||
|
||||
var UploadFlags = []cli.Flag{
|
||||
cli.IntFlag{
|
||||
Name: "p",
|
||||
Usage: "本次操作文件上传并发数量,即可以同时并发上传多少个文件。0代表跟从配置文件设置(取值范围:1 ~ 20)",
|
||||
Value: 0,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "retry",
|
||||
Usage: "上传失败最大重试次数",
|
||||
Value: DefaultUploadMaxRetry,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "timeout",
|
||||
Usage: "上传请求超时时间,单位为秒。当遇到网络不好导致上传超时可以尝试调大该值,建议设置30秒以上",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "np",
|
||||
Usage: "no progress 不展示上传进度条",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "ow",
|
||||
Usage: "overwrite, 覆盖已存在的同名文件,注意已存在的文件会被移到回收站",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "skip",
|
||||
Usage: "skip same name, 跳过已存在的同名文件,即使文件内容不一致(不检查SHA1)",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "norapid",
|
||||
Usage: "不检测秒传。跳过费时的SHA1计算直接上传",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "exn",
|
||||
Usage: "exclude name,指定排除的文件夹或者文件的名称,只支持正则表达式。支持同时排除多个名称,每一个名称就是一个exn参数",
|
||||
Value: nil,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "bs",
|
||||
Usage: "block size,上传分片大小,单位KB。推荐值:1024 ~ 10240。当上传极大单文件时候请适当调高该值",
|
||||
Value: 10240,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "ui",
|
||||
Usage: "(BETA) 使用UI面板显示上传详情和进度,更加直观和友好",
|
||||
},
|
||||
}
|
||||
|
||||
func CmdUpload() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "upload",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "上传文件/目录",
|
||||
UsageText: cmder.App().Name + " upload <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>",
|
||||
Description: `
|
||||
上传指定的文件夹或者文件,上传的文件将会保存到 <目标目录>。支持软链接文件,包括Linux/macOS(ln命令)和Windows(mklink命令)创建的符号链接文件。
|
||||
|
||||
示例:
|
||||
1. 将本地的 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录
|
||||
注意区别反斜杠 "\" 和 斜杠 "/" !!!
|
||||
aliyunpan upload C:/Users/Administrator/Desktop/1.mp4 /视频
|
||||
|
||||
2. 将本地的 C:\Users\Administrator\Desktop\1.mp4 和 C:\Users\Administrator\Desktop\2.mp4 上传到网盘 /视频 目录
|
||||
aliyunpan upload C:/Users/Administrator/Desktop/1.mp4 C:/Users/Administrator/Desktop/2.mp4 /视频
|
||||
|
||||
3. 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录
|
||||
aliyunpan upload C:/Users/Administrator/Desktop /视频
|
||||
|
||||
4. 将本地 200GB 极大文件 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录,需要调高上传分片大小
|
||||
aliyunpan upload -bs 30720 C:/Users/Administrator/Desktop/1.mp4 /视频
|
||||
|
||||
5. 使用相对路径
|
||||
aliyunpan upload 1.mp4 /视频
|
||||
|
||||
6. 覆盖上传,已存在的同名文件会被移到回收站
|
||||
aliyunpan upload -ow 1.mp4 /视频
|
||||
|
||||
7. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
|
||||
aliyunpan upload -exn "\.jpg$" C:/Users/Administrator/Video /视频
|
||||
|
||||
8. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
|
||||
aliyunpan upload -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视频
|
||||
|
||||
9. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的 @eadir 文件夹
|
||||
aliyunpan upload -exn "^@eadir$" C:/Users/Administrator/Video /视频
|
||||
|
||||
10. 跳过已存在的同名文件,即使文件内容不一致(不检查SHA1)
|
||||
aliyunpan upload -skip 1.mp4 /视频
|
||||
|
||||
参考:
|
||||
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式。在正则表达式中,^表示匹配开头,$表示匹配结尾。
|
||||
1)排除@eadir文件或者文件夹:-exn "^@eadir$"
|
||||
2)排除.jpg文件:-exn "\.jpg$"
|
||||
3)排除.号开头的文件:-exn "^\."
|
||||
4)排除~号开头的文件:-exn "^~"
|
||||
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() < 2 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
subArgs := c.Args()
|
||||
|
||||
timeout := 0
|
||||
if c.IsSet("timeout") {
|
||||
timeout = c.Int("timeout")
|
||||
if timeout < 0 {
|
||||
timeout = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 获取上传文件锁,保证上传操作单实例
|
||||
//locker := filelocker.NewFileLocker(config.GetLockerDir() + "/aliyunpan-upload")
|
||||
//if e := filelocker.LockFile(locker, 0755, true, 5*time.Second); e != nil {
|
||||
// logger.Verboseln(e)
|
||||
// fmt.Println("本应用其他实例正在执行上传,请先停止或者等待其完成")
|
||||
// return nil
|
||||
//}
|
||||
|
||||
RunUpload(subArgs[:c.NArg()-1], subArgs[c.NArg()-1], &UploadOptions{
|
||||
AllParallel: c.Int("p"), // 多文件上传的时候,允许同时并行上传的文件数量
|
||||
Parallel: 1, // 一个文件同时多少个线程并发上传的数量。阿里云盘只支持单线程按顺序进行文件part数据上传,所以只能是1
|
||||
MaxRetry: c.Int("retry"),
|
||||
MaxTimeoutSec: timeout,
|
||||
NoRapidUpload: c.Bool("norapid"),
|
||||
ShowProgress: !c.Bool("np"),
|
||||
IsOverwrite: c.Bool("ow"),
|
||||
IsSkipSameName: c.Bool("skip"),
|
||||
DriveId: parseDriveId(c),
|
||||
ExcludeNames: c.StringSlice("exn"),
|
||||
BlockSize: int64(c.Int("bs") * 1024),
|
||||
IsUseUIDashboard: c.Bool("ui"),
|
||||
})
|
||||
|
||||
// 释放文件锁
|
||||
//if locker != nil {
|
||||
// filelocker.UnlockFile(locker)
|
||||
//}
|
||||
return nil
|
||||
},
|
||||
Flags: UploadFlags,
|
||||
}
|
||||
}
|
||||
|
||||
// RunUpload 执行文件上传
|
||||
func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
activeUser := GetActiveUser()
|
||||
activeUser.PanClient().OpenapiPanClient().EnableCache()
|
||||
activeUser.PanClient().OpenapiPanClient().ClearCache()
|
||||
defer activeUser.PanClient().OpenapiPanClient().DisableCache()
|
||||
|
||||
if opt == nil {
|
||||
opt = &UploadOptions{}
|
||||
}
|
||||
|
||||
// 检测opt
|
||||
if opt.AllParallel <= 0 {
|
||||
opt.AllParallel = config.Config.MaxUploadParallel
|
||||
if opt.AllParallel == 0 {
|
||||
opt.AllParallel = config.DefaultFileUploadParallelNum
|
||||
}
|
||||
}
|
||||
if opt.AllParallel > config.MaxFileUploadParallelNum {
|
||||
fmt.Printf("警告: 当前上传文件并发数过大,可能会被阿里风控导致上传失败,建议调小。\n")
|
||||
}
|
||||
|
||||
if opt.Parallel <= 0 {
|
||||
opt.Parallel = 1
|
||||
}
|
||||
if opt.MaxRetry < 0 {
|
||||
opt.MaxRetry = DefaultUploadMaxRetry
|
||||
}
|
||||
|
||||
// 超时时间
|
||||
if opt.MaxTimeoutSec > 0 {
|
||||
activeUser.PanClient().OpenapiPanClient().SetTimeout(time.Duration(opt.MaxTimeoutSec) * time.Second)
|
||||
}
|
||||
|
||||
targetDriveName := config.Config.ActiveUser().DriveList.GetDriveNameById(opt.DriveId)
|
||||
fmt.Printf("\n[0] 当前文件上传最大并发量为: %d, 上传分片大小为: %s, 目标网盘: %s\n", opt.AllParallel, converter.ConvertFileSize(opt.BlockSize, 2), targetDriveName)
|
||||
|
||||
savePath = activeUser.PathJoin(opt.DriveId, savePath)
|
||||
_, err1 := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(opt.DriveId, savePath)
|
||||
if err1 != nil {
|
||||
fmt.Printf("警告: 上传文件, 获取云盘路径 %s 错误, %s\n", savePath, err1)
|
||||
}
|
||||
|
||||
switch len(localPaths) {
|
||||
case 0:
|
||||
fmt.Printf("本地路径为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
// 打开上传状态数据库
|
||||
uploadDatabase, err := panupload.NewUploadingDatabase()
|
||||
if err != nil {
|
||||
fmt.Printf("打开上传未完成数据库错误: %s\n", err)
|
||||
return
|
||||
}
|
||||
defer uploadDatabase.Close()
|
||||
|
||||
var (
|
||||
// 使用 task framework
|
||||
executor = &taskframework.TaskExecutor{
|
||||
IsFailedDeque: true, // 失败统计
|
||||
}
|
||||
// 统计
|
||||
statistic = &panupload.UploadStatistic{}
|
||||
|
||||
folderCreateMutex = &sync.Mutex{}
|
||||
|
||||
pluginManger = plugins.NewPluginManager(config.GetPluginDir())
|
||||
)
|
||||
executor.SetParallel(opt.AllParallel)
|
||||
statistic.StartTimer() // 开始计时
|
||||
|
||||
// 全局速度统计
|
||||
globalSpeedsStat := &speeds.Speeds{}
|
||||
|
||||
// 上传统计UI面板
|
||||
var dashboard *ui.DashboardPanel = nil
|
||||
if opt.IsUseUIDashboard && opt.ShowProgress && ui.IsTerminal(os.Stdout) {
|
||||
dashboard = ui.NewDashboardPanel(ui.DashboardPanelUpload, opt.AllParallel, globalSpeedsStat, &ui.DashboardOptions{
|
||||
Title: "上传统计UI面板",
|
||||
ActiveSlots: 3, // 下载文件进度展示,最大同时展示3个
|
||||
MaxHistory: 50, // 下载日志显示,最多同时显示50条,这个会按照窗口大小进行自适应显示
|
||||
})
|
||||
}
|
||||
logf := func(format string, a ...interface{}) {
|
||||
if dashboard != nil {
|
||||
dashboard.Logf(format, a...)
|
||||
return
|
||||
}
|
||||
fmt.Printf(format, a...)
|
||||
}
|
||||
|
||||
// 获取当前插件
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
|
||||
// 上传记录器
|
||||
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/upload_file_records.csv")
|
||||
|
||||
// 遍历指定的文件并创建上传任务
|
||||
for _, curPath := range localPaths {
|
||||
var walkFunc localfile.MyWalkFunc
|
||||
curPath = filepath.Clean(curPath)
|
||||
localPathDir := filepath.Dir(curPath)
|
||||
|
||||
// 是否排除上传
|
||||
if utils.IsExcludeFile(curPath, &opt.ExcludeNames) {
|
||||
logf("排除文件: %s\n", curPath)
|
||||
continue
|
||||
}
|
||||
|
||||
// 避免去除文件名开头的"."
|
||||
if localPathDir == "." {
|
||||
localPathDir = ""
|
||||
}
|
||||
|
||||
walkFunc = func(file localfile.SymlinkFile, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
// skip this error file and continue recurse
|
||||
logger.Verboseln("upload process file: ", file, " error: ", err)
|
||||
return nil
|
||||
}
|
||||
if os.PathSeparator == '\\' {
|
||||
file.LogicPath = cmdutil.ConvertToWindowsPathSeparator(file.LogicPath)
|
||||
file.RealPath = cmdutil.ConvertToWindowsPathSeparator(file.RealPath)
|
||||
}
|
||||
|
||||
// 是否排除上传
|
||||
if utils.IsExcludeFile(file.LogicPath, &opt.ExcludeNames) {
|
||||
logf("排除文件: %s\n", file.LogicPath)
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
subSavePath := strings.TrimPrefix(file.LogicPath, localPathDir)
|
||||
|
||||
// 针对 windows 的目录处理
|
||||
if os.PathSeparator == '\\' {
|
||||
subSavePath = cmdutil.ConvertToUnixPathSeparator(subSavePath)
|
||||
}
|
||||
subSavePath = path.Clean(savePath + aliyunpan.PathSeparator + subSavePath)
|
||||
|
||||
// 插件回调
|
||||
ft := "file"
|
||||
if fi.IsDir() {
|
||||
ft = "folder"
|
||||
}
|
||||
pluginParam := &plugins.UploadFilePrepareParams{
|
||||
LocalFilePath: file.LogicPath,
|
||||
LocalFileName: fi.Name(),
|
||||
LocalFileSize: fi.Size(),
|
||||
LocalFileType: ft,
|
||||
LocalFileUpdatedAt: fi.ModTime().Format("2006-01-02 15:04:05"),
|
||||
DriveId: activeUser.ActiveDriveId,
|
||||
DriveFilePath: strings.TrimPrefix(strings.TrimPrefix(subSavePath, savePath), "/"),
|
||||
}
|
||||
if uploadFilePrepareResult, er := plugin.UploadFilePrepareCallback(plugins.GetContext(activeUser), pluginParam); er == nil && uploadFilePrepareResult != nil {
|
||||
if strings.Compare("yes", uploadFilePrepareResult.UploadApproved) != 0 {
|
||||
// skip upload this file
|
||||
logf("插件禁止该文件上传: %s\n", file.LogicPath)
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if uploadFilePrepareResult.DriveFilePath != "" {
|
||||
targetSavePanRelativePath := strings.TrimPrefix(uploadFilePrepareResult.DriveFilePath, "/")
|
||||
subSavePath = path.Clean(savePath + aliyunpan.PathSeparator + targetSavePanRelativePath)
|
||||
logf("插件修改文件网盘保存路径为: %s\n", subSavePath)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建对应的文件上传任务
|
||||
// 上传里面的文件会创建对应的缺失文件夹
|
||||
if !fi.IsDir() {
|
||||
taskinfo := executor.Append(&panupload.UploadTaskUnit{
|
||||
LocalFileChecksum: localfile.NewLocalSymlinkFileEntity(file),
|
||||
SavePath: subSavePath,
|
||||
DriveId: opt.DriveId,
|
||||
PanClient: activeUser.PanClient(),
|
||||
UploadingDatabase: uploadDatabase,
|
||||
FolderCreateMutex: folderCreateMutex,
|
||||
Parallel: opt.Parallel,
|
||||
NoRapidUpload: opt.NoRapidUpload,
|
||||
BlockSize: opt.BlockSize,
|
||||
UploadStatistic: statistic,
|
||||
ShowProgress: opt.ShowProgress,
|
||||
IsOverwrite: opt.IsOverwrite,
|
||||
IsSkipSameName: opt.IsSkipSameName,
|
||||
GlobalSpeedsStat: globalSpeedsStat,
|
||||
FileRecorder: fileRecorder,
|
||||
UI: dashboard,
|
||||
}, opt.MaxRetry)
|
||||
logf("[%s] 加入上传队列: %s\n", taskinfo.Id(), file.LogicPath)
|
||||
if dashboard != nil {
|
||||
dashboard.RegisterTask(taskinfo.Id(), file.LogicPath, fi.Size(), !fi.IsDir())
|
||||
}
|
||||
} else {
|
||||
// 创建文件夹
|
||||
// 这样空文件夹也可以正确上传
|
||||
saveFilePath := subSavePath
|
||||
if saveFilePath != "/" {
|
||||
folderCreateMutex.Lock()
|
||||
logf("正在检测和创建云盘文件夹: %s\n", saveFilePath)
|
||||
_, apierr1 := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(opt.DriveId, saveFilePath)
|
||||
time.Sleep(1 * time.Second)
|
||||
if apierr1 != nil && apierr1.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
logger.Verbosef("%s 创建云盘文件夹: %s\n", utils.NowTimeStr(), saveFilePath)
|
||||
rs, apierr := activeUser.PanClient().OpenapiPanClient().MkdirByFullPath(opt.DriveId, saveFilePath)
|
||||
if apierr != nil || rs.FileId == "" {
|
||||
logf("创建云盘文件夹失败: %s\n", saveFilePath)
|
||||
}
|
||||
}
|
||||
folderCreateMutex.Unlock()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
file := localfile.NewSymlinkFile(curPath)
|
||||
if err = localfile.WalkAllFile(file, walkFunc); err != nil {
|
||||
if err != filepath.SkipDir {
|
||||
logf("警告: 遍历错误: %s\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 执行上传任务
|
||||
var failedList []*lane.Deque
|
||||
|
||||
// 启动UI面板显示
|
||||
if dashboard != nil {
|
||||
dashboard.Start()
|
||||
}
|
||||
|
||||
executor.Execute()
|
||||
failed := executor.FailedDeque()
|
||||
if failed.Size() > 0 {
|
||||
failedList = append(failedList, failed)
|
||||
}
|
||||
|
||||
// 关闭UI面板
|
||||
if dashboard != nil {
|
||||
dashboard.Close()
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
fmt.Printf("上传结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
|
||||
|
||||
// 输出上传失败的文件列表
|
||||
for _, failed := range failedList {
|
||||
if failed.Size() != 0 {
|
||||
fmt.Printf("以下文件上传失败: \n")
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
for e := failed.Shift(); e != nil; e = failed.Shift() {
|
||||
item := e.(*taskframework.TaskInfoItem)
|
||||
tb.Append([]string{item.Info.Id(), item.Unit.(*panupload.UploadTaskUnit).LocalFileChecksum.Path.LogicPath})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
}
|
||||
activeUser.DeleteCache(GetAllPathFolderByPath(savePath))
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func CmdLoglist() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "loglist",
|
||||
Usage: "列出帐号列表",
|
||||
Description: "列出所有已登录的阿里账号",
|
||||
Category: "阿里云盘账号",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Println(config.Config.UserList.String())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CmdSu() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "su",
|
||||
Usage: "切换阿里账号",
|
||||
Description: `
|
||||
切换已登录的阿里账号:
|
||||
如果运行该条命令没有提供参数, 程序将会列出所有的帐号, 供选择切换.
|
||||
|
||||
示例:
|
||||
aliyunpan su
|
||||
aliyunpan su <uid>
|
||||
`,
|
||||
Category: "阿里云盘账号",
|
||||
Before: ReloadConfigFunc,
|
||||
After: SaveConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() >= 2 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
numLogins := config.Config.NumLogins()
|
||||
|
||||
if numLogins == 0 {
|
||||
fmt.Printf("未设置任何帐号, 不能切换\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
inputData = c.Args().Get(0)
|
||||
uid string
|
||||
)
|
||||
|
||||
if c.NArg() == 1 {
|
||||
// 直接切换
|
||||
uid = inputData
|
||||
} else if c.NArg() == 0 {
|
||||
// 输出所有帐号供选择切换
|
||||
cli.HandleAction(cmder.App().Command("loglist").Action, c)
|
||||
|
||||
// 提示输入 index
|
||||
var index string
|
||||
fmt.Printf("输入要切换帐号的 # 值 > ")
|
||||
_, err := fmt.Scanln(&index)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if n, err1 := strconv.Atoi(index); err1 == nil && (n-1) >= 0 && (n-1) < numLogins {
|
||||
uid = config.Config.UserList[n-1].UserId
|
||||
} else {
|
||||
fmt.Printf("切换用户失败, 请检查 # 值是否正确\n")
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
}
|
||||
|
||||
switchedUser, err := config.Config.SwitchUser(uid)
|
||||
if err != nil {
|
||||
fmt.Printf("切换用户失败, %s\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if switchedUser == nil {
|
||||
switchedUser = TryLogin()
|
||||
}
|
||||
|
||||
if switchedUser != nil {
|
||||
fmt.Printf("切换用户: %s\n", switchedUser.Nickname)
|
||||
} else {
|
||||
fmt.Printf("切换用户失败\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CmdWho() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "who",
|
||||
Usage: "获取当前帐号",
|
||||
Description: "获取当前帐号的信息",
|
||||
Category: "阿里云盘账号",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
os.Exit(1)
|
||||
}
|
||||
activeUser := config.Config.ActiveUser()
|
||||
cloudName := activeUser.GetDriveById(activeUser.ActiveDriveId).DriveName
|
||||
user, _ := GetActivePanClient().OpenapiPanClient().GetUserInfo()
|
||||
thirdParty := "未开通"
|
||||
if user.ThirdPartyVip {
|
||||
thirdParty = "已开通(" + user.ThirdPartyVipExpire + ")"
|
||||
}
|
||||
fmt.Printf("当前帐号UID: %s, 昵称: %s, 三方权益包: %s, 当前使用网盘:%s\n", activeUser.UserId, activeUser.Nickname, thirdParty, cloudName)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
panCommandVerbose = logger.New("PANCOMMAND", config.EnvVerbose)
|
||||
|
||||
saveConfigMutex *sync.Mutex = new(sync.Mutex)
|
||||
|
||||
ReloadConfigFunc = func(c *cli.Context) error {
|
||||
err := config.Config.Reload()
|
||||
if err != nil {
|
||||
fmt.Printf("重载配置错误: %s\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
SaveConfigFunc = func(c *cli.Context) error {
|
||||
saveConfigMutex.Lock()
|
||||
defer saveConfigMutex.Unlock()
|
||||
err := config.Config.Save()
|
||||
if err != nil {
|
||||
fmt.Printf("保存配置错误: %s\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
)
|
||||
|
||||
// RunTestShellPattern 执行测试通配符
|
||||
func RunTestShellPattern(driveId string, pattern string) {
|
||||
acUser := GetActiveUser()
|
||||
files, err := acUser.PanClient().OpenapiPanClient().MatchPathByShellPattern(driveId, GetActiveUser().PathJoin(driveId, pattern))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
for _, f := range *files {
|
||||
fmt.Printf("%s\n", f.Path)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// matchPathByShellPattern 通配符匹配路径,允许返回多个匹配结果
|
||||
func matchPathByShellPattern(driveId string, patterns ...string) (files []*aliyunpan.FileEntity, e error) {
|
||||
acUser := GetActiveUser()
|
||||
for k := range patterns {
|
||||
ps, err := acUser.PanClient().OpenapiPanClient().MatchPathByShellPattern(driveId, acUser.PathJoin(driveId, patterns[k]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, *ps...)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// makePathAbsolute 拼接路径,确定返回路径为绝对路径
|
||||
func makePathAbsolute(driveId string, patterns ...string) (panpaths []string, err error) {
|
||||
acUser := GetActiveUser()
|
||||
for k := range patterns {
|
||||
ps := acUser.PathJoin(driveId, patterns[k])
|
||||
panpaths = append(panpaths, ps)
|
||||
}
|
||||
return panpaths, nil
|
||||
}
|
||||
|
||||
func RandomStr(count int) string {
|
||||
//STR_SET := "abcdefjhijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||
STR_SET := "abcdefjhijklmnopqrstuvwxyz1234567890"
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
str := strings.Builder{}
|
||||
for i := 0; i < count; i++ {
|
||||
str.WriteByte(byte(STR_SET[rand.Intn(len(STR_SET))]))
|
||||
}
|
||||
return str.String()
|
||||
}
|
||||
|
||||
func GetAllPathFolderByPath(pathStr string) []string {
|
||||
dirNames := strings.Split(pathStr, "/")
|
||||
dirs := []string{}
|
||||
p := "/"
|
||||
dirs = append(dirs, p)
|
||||
for _, s := range dirNames {
|
||||
p = path.Join(p, s)
|
||||
dirs = append(dirs, p)
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
// EscapeStr 转义字符串
|
||||
func EscapeStr(s string) string {
|
||||
return url.PathEscape(s)
|
||||
}
|
||||
|
||||
// UnescapeStr 反转义字符串
|
||||
func UnescapeStr(s string) string {
|
||||
r, _ := url.PathUnescape(s)
|
||||
return r
|
||||
}
|
||||
|
||||
func NewWebLoginToken(accessToken string, expired int64) aliyunpan_web.WebLoginToken {
|
||||
webapiToken := &config.PanClientToken{
|
||||
AccessToken: accessToken,
|
||||
Expired: expired,
|
||||
}
|
||||
return aliyunpan_web.WebLoginToken{
|
||||
AccessTokenType: "Bearer",
|
||||
AccessToken: webapiToken.AccessToken,
|
||||
RefreshToken: "",
|
||||
ExpiresIn: 7200,
|
||||
ExpireTime: webapiToken.GetExpiredTimeCstStr(),
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshWebTokenInNeed 刷新 webapi access token
|
||||
func RefreshWebTokenInNeed(activeUser *config.PanUser) *panlogin.CommonResultError {
|
||||
if activeUser == nil {
|
||||
return panlogin.NewCommonResultError("activeUser is nil")
|
||||
}
|
||||
|
||||
// refresh expired web token
|
||||
if activeUser.PanClient().WebapiPanClient() != nil {
|
||||
if activeUser.WebapiToken != nil && len(activeUser.WebapiToken.AccessToken) > 0 {
|
||||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||||
expiredTime := time.Unix(activeUser.WebapiToken.Expired, 0).In(cz)
|
||||
now := time.Now()
|
||||
if (expiredTime.Unix() - now.Unix()) <= (10 * 60) { // 有效期小于10min就刷新
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
params := &plugins.UserTokenRefreshFinishParams{
|
||||
Result: "success",
|
||||
Message: "webapi",
|
||||
OldToken: "",
|
||||
NewToken: "",
|
||||
UpdatedAt: utils.NowTimeStr(),
|
||||
}
|
||||
|
||||
// need update refresh token
|
||||
logger.Verboseln("web access token expired, get new from server")
|
||||
loginHelper := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
|
||||
wt, re := loginHelper.GetWebapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().WebapiPanClient().GetAccessToken())
|
||||
if re != nil {
|
||||
logger.Verboseln("get web token from server error: ", re.Msg)
|
||||
if re.Code != panlogin.SUCCESS {
|
||||
return re
|
||||
}
|
||||
}
|
||||
if wt != nil {
|
||||
params.Result = "success"
|
||||
params.OldToken = activeUser.WebapiToken.AccessToken
|
||||
params.NewToken = wt.AccessToken
|
||||
|
||||
// update for user & client
|
||||
userWebToken := NewWebLoginToken(wt.AccessToken, wt.Expired)
|
||||
activeUser.WebapiToken = &config.PanClientToken{
|
||||
AccessToken: wt.AccessToken,
|
||||
Expired: wt.Expired,
|
||||
}
|
||||
activeUser.PanClient().WebapiPanClient().UpdateToken(userWebToken)
|
||||
logger.Verboseln("get new access token success")
|
||||
|
||||
// plugin callback
|
||||
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
|
||||
}
|
||||
|
||||
// create new signature
|
||||
_, e1 := activeUser.PanClient().WebapiPanClient().CreateSession(nil)
|
||||
if e1 != nil {
|
||||
logger.Verboseln("call CreateSession error in RefreshWebTokenInNeed: " + e1.Error())
|
||||
}
|
||||
return &panlogin.CommonResultError{
|
||||
Code: 0,
|
||||
Msg: "",
|
||||
}
|
||||
} else {
|
||||
// token refresh error
|
||||
// if token has expired, callback plugin api for notify
|
||||
if now.Unix() >= expiredTime.Unix() {
|
||||
params.Result = "fail"
|
||||
params.Message = re.Msg
|
||||
params.OldToken = activeUser.WebapiToken.AccessToken
|
||||
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return &panlogin.CommonResultError{
|
||||
Code: 0,
|
||||
Msg: "web token is validate, not need to refresh",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return panlogin.NewCommonResultError("activeUser is invalidate")
|
||||
}
|
||||
|
||||
// RefreshOpenTokenInNeed 刷新 openapi access token
|
||||
func RefreshOpenTokenInNeed(activeUser *config.PanUser) *panlogin.CommonResultError {
|
||||
if activeUser == nil {
|
||||
return panlogin.NewCommonResultError("activeUser is nil")
|
||||
}
|
||||
|
||||
// OpenAPI的token只有2个小时有效期,需要在token过期之前进行刷新并把获取到的新token存储在本地
|
||||
// refresh expired openapi token
|
||||
if activeUser.PanClient().OpenapiPanClient() != nil {
|
||||
if activeUser.OpenapiToken != nil && len(activeUser.OpenapiToken.AccessToken) > 0 {
|
||||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||||
expiredTime := time.Unix(activeUser.OpenapiToken.Expired, 0).In(cz)
|
||||
now := time.Now().In(cz)
|
||||
if (expiredTime.Unix() - now.Unix()) <= (10 * 60) { // 有效期小于10min就刷新
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
params := &plugins.UserTokenRefreshFinishParams{
|
||||
Result: "success",
|
||||
Message: "openapi",
|
||||
OldToken: "",
|
||||
NewToken: "",
|
||||
UpdatedAt: utils.NowTimeStr(),
|
||||
}
|
||||
|
||||
// need update refresh token
|
||||
logger.Verboseln("openapi access token expired, get new from server")
|
||||
loginHelper := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
|
||||
wt, re := loginHelper.GetOpenapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().OpenapiPanClient().GetAccessToken())
|
||||
if re != nil {
|
||||
logger.Verboseln("get openapi token from server error: ", re.Msg)
|
||||
if re.Code != panlogin.SUCCESS {
|
||||
return re
|
||||
}
|
||||
}
|
||||
if wt != nil {
|
||||
// 存储新token到本地
|
||||
params.Result = "success"
|
||||
params.OldToken = activeUser.OpenapiToken.AccessToken
|
||||
params.NewToken = wt.AccessToken
|
||||
|
||||
// update for user
|
||||
activeUser.OpenapiToken = &config.PanClientToken{
|
||||
AccessToken: wt.AccessToken,
|
||||
Expired: wt.Expired,
|
||||
}
|
||||
logger.Verboseln("get new access token success")
|
||||
|
||||
// plugin callback
|
||||
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
|
||||
}
|
||||
|
||||
return &panlogin.CommonResultError{
|
||||
Code: 0,
|
||||
Msg: "",
|
||||
}
|
||||
} else {
|
||||
// token refresh error
|
||||
// if token has expired, callback plugin api for notify
|
||||
if now.Unix() >= expiredTime.Unix() {
|
||||
params.Result = "fail"
|
||||
params.Message = re.Msg
|
||||
params.OldToken = activeUser.OpenapiToken.AccessToken
|
||||
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return &panlogin.CommonResultError{
|
||||
Code: 0,
|
||||
Msg: "openapi token is validate, not need to refresh",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return panlogin.NewCommonResultError("activeUser is invalidate")
|
||||
}
|
||||
|
||||
// AutomaticallyRefreshTokenTask 自动刷新Token的后台任务
|
||||
func AutomaticallyRefreshTokenTask() {
|
||||
go func() {
|
||||
// 延迟启动自动刷新Token任务,因为主程序启动的时候默认会进行Token的初始化和刷新,避免无效的并行初始化Token
|
||||
time.Sleep(time.Duration(15) * time.Minute)
|
||||
|
||||
// 检测延迟时间(分钟)
|
||||
delayMinuteTime := time.Duration(1) * time.Minute
|
||||
//delayMinuteTime := time.Duration(10) * time.Second // for test
|
||||
// 请求重试次数
|
||||
attempt := 1
|
||||
for {
|
||||
// Token刷新进程,不管是CLI命令行模式,还是直接命令模式,本刷新任务都会执行
|
||||
time.Sleep(delayMinuteTime) // 定时器,每1分钟调用一次
|
||||
|
||||
// 重新加载配置文件
|
||||
ReloadConfigFunc(nil)
|
||||
activeUser := config.Config.ActiveUser()
|
||||
if activeUser != nil && activeUser.UserId != "" && activeUser.PanClient() != nil {
|
||||
// 刷新Openapi端Token
|
||||
errResult := RefreshOpenTokenInNeed(activeUser)
|
||||
attempt += 1
|
||||
if errResult != nil && errResult.Code == panlogin.SUCCESS {
|
||||
// 刷新成功,重置定时器
|
||||
attempt = 1
|
||||
delayMinuteTime = time.Duration(1) * time.Minute
|
||||
|
||||
// 刷新Webapi端Token
|
||||
RefreshWebTokenInNeed(activeUser)
|
||||
|
||||
// 保存新token到配置文件
|
||||
SaveConfigFunc(nil)
|
||||
} else {
|
||||
// 刷新Token失败
|
||||
logger.Verboseln("刷新Token失败:", errResult.Msg)
|
||||
if errResult.Code == panlogin.ERROR_NEED_LOGIN_AGAIN {
|
||||
// token已经失效,需要重新登录
|
||||
logger.Verboseln("token已经失效,需要重新登录,退出自动刷新Token任务")
|
||||
return
|
||||
} else if errResult.Code == panlogin.ERROR {
|
||||
// 访问错误
|
||||
delayMinuteTime = ProgressiveBackoffAlg(attempt)
|
||||
} else if errResult.Code == panlogin.ERROR_TOO_MANY_REQUESTS {
|
||||
// 请求过于频繁
|
||||
delayMinuteTime = ProgressiveBackoffAlg(attempt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ProgressiveBackoffAlg 指数退降算法
|
||||
func ProgressiveBackoffAlg(attempt int) time.Duration {
|
||||
maxRetries := 120
|
||||
baseDelay := float64(1)
|
||||
maxDelay := float64(1440)
|
||||
|
||||
if attempt >= maxRetries {
|
||||
return time.Duration(maxDelay) * time.Minute
|
||||
}
|
||||
|
||||
// 计算当前延迟
|
||||
var delay float64
|
||||
if attempt < 100 {
|
||||
// 前 100 次:线性增长 + 小指数基数(缓慢增加)
|
||||
delay = baseDelay * (1 + float64(attempt)/5) // 调整分母控制增长速度
|
||||
} else {
|
||||
// 100 次后:标准指数退避
|
||||
delay = baseDelay * math.Pow(1.5, float64(attempt-100))
|
||||
if delay > maxDelay {
|
||||
delay = maxDelay
|
||||
}
|
||||
}
|
||||
|
||||
// 添加随机抖动(最多 20% 的扰动)
|
||||
jitterSec := rand.Float64() * delay * 0.2 // ±20% 随机扰动
|
||||
delay += jitterSec
|
||||
return time.Duration(delay * float64(time.Minute))
|
||||
}
|
||||
|
||||
func isIncludeFile(pattern string, fileName string) bool {
|
||||
b, er := filepath.Match(pattern, fileName)
|
||||
if er != nil {
|
||||
return false
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// isMatchWildcardPattern 是否是统配符字符串
|
||||
func isMatchWildcardPattern(name string) bool {
|
||||
return strings.ContainsAny(name, "*") || strings.ContainsAny(name, "?") || strings.ContainsAny(name, "[")
|
||||
}
|
||||
|
||||
// TryLogin 尝试登录,基础应用支持的各类登录方式进行尝试成功登录
|
||||
func TryLogin() *config.PanUser {
|
||||
// can do automatically login?
|
||||
for _, u := range config.Config.UserList {
|
||||
if u.UserId == config.Config.ActiveUID {
|
||||
// login
|
||||
cloudUser, err := config.SetupUserByCookie(u.OpenapiToken, u.WebapiToken,
|
||||
u.TicketId, u.UserId,
|
||||
config.Config.DeviceId, config.Config.DeviceName,
|
||||
config.Config.ClientId, config.Config.ClientSecret)
|
||||
if cloudUser == nil {
|
||||
logger.Verboseln("尝试登录失败: ", err)
|
||||
fmt.Println("尝试登录失败,请使用 login 命令进行重新登录")
|
||||
return nil
|
||||
}
|
||||
|
||||
// save
|
||||
SaveConfigFunc(nil)
|
||||
// reload
|
||||
ReloadConfigFunc(nil)
|
||||
|
||||
return config.Config.ActiveUser()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProgressiveBackoffAlg(t *testing.T) {
|
||||
for attempt := 1; attempt <= 150; attempt++ {
|
||||
delayTime := ProgressiveBackoffAlg(attempt)
|
||||
fmt.Printf("第%d次,延迟分钟: %.2f\n", attempt, delayTime.Minutes())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func CmdXcp() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "xcp",
|
||||
Usage: "备份盘和资源库之间转存文件",
|
||||
UsageText: `
|
||||
aliyunpan xcp <文件/目录1> <文件/目录2> <文件/目录3> ... <目标盘目录>`,
|
||||
Description: `
|
||||
注意: 拷贝多个文件和目录时, 请确保每一个文件和目录都存在, 否则拷贝操作会失败。
|
||||
|
||||
示例:
|
||||
|
||||
当前程序工作在备份盘下,将备份盘 /1.mp4 文件转存复制到 资源库下的 /来自备份盘/video 目录下
|
||||
aliyunpan xcp /1.mp4 /来自备份盘/video
|
||||
|
||||
当前程序工作在资源库下,将资源库 /1.mp4 文件转存复制到 备份盘下的 /来自资源库/video 目录下
|
||||
aliyunpan xcp /1.mp4 /来自资源库/video
|
||||
|
||||
当前程序工作在备份盘下,将 /我的资源 目录下所有的.mp4文件 复制到 /来自备份盘/video 目录下面,使用通配符匹配
|
||||
aliyunpan xcp /我的资源/*.mp4 /来自备份盘/video
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() <= 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
srcDriveId := parseDriveId(c)
|
||||
dstDriveId := ""
|
||||
driveList := config.Config.ActiveUser().DriveList
|
||||
if driveList.GetFileDriveId() == srcDriveId {
|
||||
dstDriveId = driveList.GetResourceDriveId()
|
||||
} else if driveList.GetResourceDriveId() == srcDriveId {
|
||||
dstDriveId = driveList.GetFileDriveId()
|
||||
} else {
|
||||
fmt.Println("不支持该操作")
|
||||
return nil
|
||||
}
|
||||
RunXCopy(srcDriveId, dstDriveId, c.Args()...)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "源网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunXCopy 执行复制文件/目录
|
||||
func RunXCopy(srcDriveId, dstDriveId string, paths ...string) {
|
||||
activeUser := GetActiveUser()
|
||||
cacheCleanPaths := []string{}
|
||||
opFileList, targetFile, _, err := getCrossCopyFileInfo(srcDriveId, dstDriveId, paths...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if targetFile == nil {
|
||||
fmt.Println("目标目录不存在")
|
||||
return
|
||||
}
|
||||
if opFileList == nil || len(opFileList) == 0 {
|
||||
fmt.Println("没有有效的文件可复制")
|
||||
return
|
||||
}
|
||||
cacheCleanPaths = append(cacheCleanPaths, targetFile.Path)
|
||||
|
||||
failedCopyFiles := []*aliyunpan.FileEntity{}
|
||||
copyFileParamList := []string{}
|
||||
fileId2FileEntity := map[string]*aliyunpan.FileEntity{}
|
||||
for _, mfi := range opFileList {
|
||||
fileId2FileEntity[mfi.FileId] = mfi
|
||||
copyFileParamList = append(copyFileParamList, mfi.FileId)
|
||||
cacheCleanPaths = append(cacheCleanPaths, path.Dir(mfi.Path))
|
||||
}
|
||||
fccr, er := activeUser.PanClient().WebapiPanClient().FileCrossDriveCopy(&aliyunpan_web.FileCrossCopyParam{
|
||||
FromDriveId: srcDriveId,
|
||||
FromFileIds: copyFileParamList,
|
||||
ToDriveId: dstDriveId,
|
||||
ToParentFileId: targetFile.FileId,
|
||||
})
|
||||
for _, rs := range fccr {
|
||||
if rs.Status != 201 {
|
||||
failedCopyFiles = append(failedCopyFiles, fileId2FileEntity[rs.SourceFileId])
|
||||
}
|
||||
}
|
||||
|
||||
if len(failedCopyFiles) > 0 {
|
||||
fmt.Println("以下文件复制失败:")
|
||||
for _, f := range failedCopyFiles {
|
||||
fmt.Println(f.FileName)
|
||||
}
|
||||
fmt.Println("")
|
||||
}
|
||||
if er == nil {
|
||||
pnt := func() {
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "文件/目录"})
|
||||
for k, rs := range fccr {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), fileId2FileEntity[rs.SourceFileId].Path})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
fmt.Println("操作成功, 以下文件已复制到目标目录: ", targetFile.Path)
|
||||
pnt()
|
||||
activeUser.DeleteCache(cacheCleanPaths)
|
||||
} else {
|
||||
fmt.Println("无法复制文件,请稍后重试")
|
||||
}
|
||||
}
|
||||
|
||||
func getCrossCopyFileInfo(srcDriveId, dstDriveId string, paths ...string) (opFileList []*aliyunpan.FileEntity, targetFile *aliyunpan.FileEntity, failedPaths []string, error error) {
|
||||
if len(paths) <= 1 {
|
||||
return nil, nil, nil, fmt.Errorf("请指定目标文件夹路径")
|
||||
}
|
||||
activeUser := GetActiveUser()
|
||||
|
||||
// the last one is the target file path
|
||||
targetFilePath := path.Clean(paths[len(paths)-1])
|
||||
absolutePath := activeUser.PathJoin(dstDriveId, targetFilePath)
|
||||
targetFile, err := activeUser.PanClient().WebapiPanClient().FileInfoByPath(dstDriveId, absolutePath)
|
||||
if err != nil || !targetFile.IsFolder() {
|
||||
return nil, nil, nil, fmt.Errorf("指定目标文件夹不存在")
|
||||
}
|
||||
|
||||
// source file list
|
||||
for idx := 0; idx < (len(paths) - 1); idx++ {
|
||||
absolutePath = path.Clean(activeUser.PathJoin(srcDriveId, paths[idx]))
|
||||
fileList, err1 := matchPathByShellPattern(srcDriveId, absolutePath)
|
||||
if err1 != nil {
|
||||
failedPaths = append(failedPaths, absolutePath)
|
||||
continue
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
// 文件不存在
|
||||
failedPaths = append(failedPaths, absolutePath)
|
||||
continue
|
||||
}
|
||||
for _, f := range fileList {
|
||||
// 移动匹配的文件
|
||||
opFileList = append(opFileList, f)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command_local
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/internal/command"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CmdLocalCd 切换本地工作目录
|
||||
func CmdLocalCd() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "lcd",
|
||||
Category: "本地命令",
|
||||
Usage: "切换本地工作目录",
|
||||
Description: `
|
||||
aliyunpan lcd <本地目录, 绝对路径或相对路径>
|
||||
|
||||
示例:
|
||||
|
||||
切换 /我的资源 工作目录:
|
||||
aliyunpan lcd /我的资源
|
||||
|
||||
切换上级目录:
|
||||
aliyunpan lcd ..
|
||||
|
||||
切换根目录:
|
||||
aliyunpan lcd /
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() == 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
RunChangeLocalDirectory(c.Args().Get(0))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
}
|
||||
}
|
||||
|
||||
// CmdLocalPwd 当前本地工作目录
|
||||
func CmdLocalPwd() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "lpwd",
|
||||
Usage: "输出本地工作目录",
|
||||
Category: "本地命令",
|
||||
Before: command.ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
lwd := config.Config.LocalWorkdir
|
||||
if lwd == "" {
|
||||
// 默认为用户主页目录
|
||||
lwd = GetLocalHomeDir()
|
||||
config.Config.LocalWorkdir = lwd
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
lwd = strings.ReplaceAll(lwd, "/", "\\")
|
||||
} else {
|
||||
// Unix-like system, so just assume Unix
|
||||
lwd = strings.ReplaceAll(lwd, "\\", "/")
|
||||
}
|
||||
fmt.Println("本地工作目录: " + lwd)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunChangeLocalDirectory(targetPath string) {
|
||||
targetPath = LocalPathJoin(targetPath)
|
||||
|
||||
// 获取目标路径文件信息
|
||||
localFileInfo, er := os.Stat(targetPath)
|
||||
if er != nil {
|
||||
fmt.Println("目录路径不存在")
|
||||
return
|
||||
}
|
||||
if !localFileInfo.IsDir() {
|
||||
fmt.Printf("错误: %s 不是一个目录 (文件夹)\n", targetPath)
|
||||
return
|
||||
}
|
||||
config.Config.LocalWorkdir = strings.ReplaceAll(targetPath, "\\", "/")
|
||||
fmt.Printf("改变本地工作目录: %s\n", config.Config.LocalWorkdir)
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command_local
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/urfave/cli"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CmdLocalLs() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "lls",
|
||||
Category: "本地命令",
|
||||
Usage: "列出本地目录",
|
||||
Description: `
|
||||
列出当前本地工作目录内的文件和目录, 或指定目录内的文件和目录
|
||||
|
||||
示例:
|
||||
|
||||
列出 我的资源 内的文件和目录
|
||||
aliyunpan lls 我的资源
|
||||
|
||||
绝对路径
|
||||
aliyunpan lls /我的资源
|
||||
|
||||
详细列出 我的资源 内的文件和目录
|
||||
aliyunpan lls /我的资源
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
var (
|
||||
orderBy aliyunpan.FileOrderBy = aliyunpan.FileOrderByUpdatedAt
|
||||
orderSort aliyunpan.FileOrderDirection = aliyunpan.FileOrderDirectionDesc
|
||||
showAllFile bool = false
|
||||
)
|
||||
if c.IsSet("a") {
|
||||
showAllFile = true
|
||||
}
|
||||
|
||||
switch {
|
||||
case c.IsSet("asc"):
|
||||
orderSort = aliyunpan.FileOrderDirectionAsc
|
||||
case c.IsSet("desc"):
|
||||
orderSort = aliyunpan.FileOrderDirectionDesc
|
||||
default:
|
||||
orderSort = aliyunpan.FileOrderDirectionDesc
|
||||
}
|
||||
|
||||
switch {
|
||||
case c.IsSet("time"):
|
||||
orderBy = aliyunpan.FileOrderByUpdatedAt
|
||||
case c.IsSet("name"):
|
||||
orderBy = aliyunpan.FileOrderByName
|
||||
case c.IsSet("size"):
|
||||
orderBy = aliyunpan.FileOrderBySize
|
||||
default:
|
||||
orderBy = aliyunpan.FileOrderByUpdatedAt
|
||||
}
|
||||
|
||||
RunLocalLs(c.Args().Get(0), showAllFile, orderBy, orderSort)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "a",
|
||||
Usage: "显示所有文件",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "asc",
|
||||
Usage: "升序排序",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "desc",
|
||||
Usage: "降序排序",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "time",
|
||||
Usage: "根据修改时间排序",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "name",
|
||||
Usage: "根据文件名排序",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "size",
|
||||
Usage: "根据大小排序",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunLocalLs(targetPath string,
|
||||
showAllFile bool,
|
||||
orderBy aliyunpan.FileOrderBy, orderDirection aliyunpan.FileOrderDirection) {
|
||||
targetPath = LocalPathJoin(targetPath)
|
||||
|
||||
// 获取目标路径文件信息
|
||||
localFileInfo, er := os.Stat(targetPath)
|
||||
if er != nil {
|
||||
fmt.Println("目录路径不存在")
|
||||
return
|
||||
}
|
||||
if !localFileInfo.IsDir() {
|
||||
fmt.Println("指定的路径不是目录")
|
||||
return
|
||||
}
|
||||
|
||||
fileEntryList, err := os.ReadDir(targetPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fileInfoList := []os.FileInfo{}
|
||||
folderInfoList := []os.FileInfo{}
|
||||
for _, entry := range fileEntryList {
|
||||
if fi, e := entry.Info(); e == nil {
|
||||
if strings.HasPrefix(fi.Name(), ".") {
|
||||
if !showAllFile {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if fi.IsDir() {
|
||||
folderInfoList = append(folderInfoList, fi)
|
||||
} else {
|
||||
fileInfoList = append(fileInfoList, fi)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 过滤
|
||||
|
||||
// 排序
|
||||
sortFileInfoList(folderInfoList, orderBy, orderDirection)
|
||||
sortFileInfoList(fileInfoList, orderBy, orderDirection)
|
||||
|
||||
// 合并并渲染列表
|
||||
renderFileList := append(folderInfoList, fileInfoList...)
|
||||
renderTable(targetPath, renderFileList)
|
||||
}
|
||||
|
||||
func sortFileInfoList(fileInfoList []os.FileInfo,
|
||||
orderBy aliyunpan.FileOrderBy, orderDirection aliyunpan.FileOrderDirection) []os.FileInfo {
|
||||
switch orderBy {
|
||||
case aliyunpan.FileOrderByUpdatedAt: // 基于修改时间排序
|
||||
sort.Slice(fileInfoList, func(i, j int) bool {
|
||||
if orderDirection == aliyunpan.FileOrderDirectionAsc {
|
||||
return fileInfoList[i].ModTime().UnixMilli() < fileInfoList[j].ModTime().UnixMilli()
|
||||
} else {
|
||||
return fileInfoList[i].ModTime().UnixMilli() > fileInfoList[j].ModTime().UnixMilli()
|
||||
}
|
||||
})
|
||||
break
|
||||
case aliyunpan.FileOrderByName: // 基于文件名排序
|
||||
sort.Slice(fileInfoList, func(i, j int) bool {
|
||||
if orderDirection == aliyunpan.FileOrderDirectionAsc {
|
||||
return fileInfoList[i].Name() < fileInfoList[j].Name()
|
||||
} else {
|
||||
return fileInfoList[i].Name() > fileInfoList[j].Name()
|
||||
}
|
||||
})
|
||||
break
|
||||
case aliyunpan.FileOrderBySize: // 基于文件大小排序
|
||||
sort.Slice(fileInfoList, func(i, j int) bool {
|
||||
if orderDirection == aliyunpan.FileOrderDirectionAsc {
|
||||
return fileInfoList[i].Size() < fileInfoList[j].Size()
|
||||
} else {
|
||||
return fileInfoList[i].Size() > fileInfoList[j].Size()
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
return fileInfoList
|
||||
}
|
||||
|
||||
func renderTable(path string, files []os.FileInfo) {
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
var (
|
||||
fileCount, dirCount, totalSize int64
|
||||
)
|
||||
|
||||
tb.SetHeader([]string{"#", "文件大小", "修改日期", "文件(目录)"})
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
|
||||
for k, file := range files {
|
||||
if file.IsDir() {
|
||||
dirCount += 1
|
||||
tb.Append([]string{strconv.Itoa(k + 1), "-", file.ModTime().Format("2006-01-02 15:04:05"), file.Name() + aliyunpan.PathSeparator})
|
||||
continue
|
||||
}
|
||||
fileCount += 1
|
||||
totalSize += file.Size()
|
||||
tb.Append([]string{strconv.Itoa(k + 1), converter.ConvertFileSize(file.Size(), 2), file.ModTime().Format("2006-01-02 15:04:05"), file.Name()})
|
||||
}
|
||||
tb.Append([]string{"", "总: " + converter.ConvertFileSize(totalSize, 2), "", fmt.Sprintf("文件总数: %d, 目录总数: %d", fileCount, dirCount)})
|
||||
|
||||
fmt.Printf("\n本地当前目录: %s\n", path)
|
||||
fmt.Printf("----\n")
|
||||
tb.Render()
|
||||
fmt.Printf("----\n")
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package command_local
|
||||
|
||||
import (
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/library/homedir"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetLocalHomeDir 获取本地用户主目录
|
||||
func GetLocalHomeDir() string {
|
||||
// 默认为用户主页目录
|
||||
if hd, e := os.UserHomeDir(); e == nil {
|
||||
return hd
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// LocalPathJoin 拼接本地路径
|
||||
func LocalPathJoin(p string) string {
|
||||
p = path.Clean(strings.ReplaceAll(p, "\\", "/"))
|
||||
if filepath.IsAbs(p) {
|
||||
return p
|
||||
} else if strings.HasPrefix(p, "~") {
|
||||
if d, e := homedir.Expand(p); e == nil {
|
||||
return d
|
||||
}
|
||||
}
|
||||
wd := config.Config.LocalWorkdir
|
||||
if wd == "" {
|
||||
wd = GetLocalHomeDir()
|
||||
}
|
||||
return path.Join(wd, p)
|
||||
}
|
||||
|
||||
// LocalPathDir 获取当前文件夹的父文件夹路径
|
||||
func LocalPathDir(p string) string {
|
||||
p = strings.ReplaceAll(p, "\\", "/")
|
||||
if strings.HasSuffix(p, ":/") { // windows卷标路径,例如:C:\ 、D:\、E:\
|
||||
return p
|
||||
}
|
||||
parentDirPath := path.Dir(LocalPathClean(p))
|
||||
return parentDirPath
|
||||
}
|
||||
|
||||
// LocalPathBase 获取当前路径的文件(文件夹)名
|
||||
func LocalPathBase(p string) string {
|
||||
p = strings.ReplaceAll(p, "\\", "/")
|
||||
if strings.HasSuffix(p, ":/") { // windows卷标路径,例如:C:\ 、D:\、E:\
|
||||
return ""
|
||||
}
|
||||
baseName := path.Base(LocalPathClean(p))
|
||||
return baseName
|
||||
}
|
||||
|
||||
// LocalPathClean 规范化本地文件夹路径
|
||||
func LocalPathClean(p string) string {
|
||||
cleanPath := path.Clean(strings.ReplaceAll(p, "\\", "/"))
|
||||
return cleanPath
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/library-go/expires"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DeleteCache 删除含有 dirs 的缓存
|
||||
func (pu *PanUser) DeleteCache(dirs []string) {
|
||||
cache := pu.cacheOpMap.LazyInitCachePoolOp(pu.ActiveDriveId)
|
||||
for _, v := range dirs {
|
||||
key := v + "_" + "OrderByName"
|
||||
_, ok := cache.Load(key)
|
||||
if ok {
|
||||
cache.Delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteOneCache 删除缓存
|
||||
func (pu *PanUser) DeleteOneCache(dirPath string) {
|
||||
ps := []string{dirPath}
|
||||
pu.DeleteCache(ps)
|
||||
}
|
||||
|
||||
// CacheFilesDirectoriesList 缓存获取
|
||||
func (pu *PanUser) CacheFilesDirectoriesList(pathStr string) (fdl aliyunpan.FileList, apiError *apierror.ApiError) {
|
||||
data := pu.cacheOpMap.CacheOperation(pu.ActiveDriveId, pathStr+"_OrderByName", func() expires.DataExpires {
|
||||
var fi *aliyunpan.FileEntity
|
||||
fi, apiError = pu.panClient.OpenapiPanClient().FileInfoByPath(pu.ActiveDriveId, pathStr)
|
||||
if apiError != nil {
|
||||
return nil
|
||||
}
|
||||
fileListParam := &aliyunpan.FileListParam{
|
||||
DriveId: pu.ActiveDriveId,
|
||||
ParentFileId: fi.FileId,
|
||||
}
|
||||
fdl, apiError = pu.panClient.OpenapiPanClient().FileListGetAll(fileListParam, 200)
|
||||
if apiError != nil {
|
||||
return nil
|
||||
}
|
||||
// construct full path
|
||||
for _, f := range fdl {
|
||||
f.Path = path.Join(pathStr, f.FileName)
|
||||
}
|
||||
return expires.NewDataExpires(fdl, 10*time.Minute)
|
||||
})
|
||||
if apiError != nil {
|
||||
return
|
||||
}
|
||||
return data.Data().(aliyunpan.FileList), nil
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
//ErrNotLogin 未登录帐号错误
|
||||
ErrNotLogin = errors.New("user not login")
|
||||
//ErrConfigFilePathNotSet 未设置配置文件
|
||||
ErrConfigFilePathNotSet = errors.New("config file not set")
|
||||
//ErrConfigFileNotExist 未设置Config, 未初始化
|
||||
ErrConfigFileNotExist = errors.New("config file not exist")
|
||||
//ErrConfigFileNoPermission Config文件无权限访问
|
||||
ErrConfigFileNoPermission = errors.New("config file permission denied")
|
||||
//ErrConfigContentsParseError 解析Config数据错误
|
||||
ErrConfigContentsParseError = errors.New("config contents parse error")
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_open"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
)
|
||||
|
||||
type (
|
||||
// PanClient 云盘客户端
|
||||
PanClient struct {
|
||||
// 网页WEB接口客户端
|
||||
webapiPanClient *aliyunpan_web.WebPanClient
|
||||
// 阿里openapi接口客户端
|
||||
openapiPanClient *aliyunpan_open.OpenPanClient
|
||||
}
|
||||
)
|
||||
|
||||
func NewPanClient(webClient *aliyunpan_web.WebPanClient, openClient *aliyunpan_open.OpenPanClient) *PanClient {
|
||||
return &PanClient{
|
||||
webapiPanClient: webClient,
|
||||
openapiPanClient: openClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PanClient) WebapiPanClient() *aliyunpan_web.WebPanClient {
|
||||
return p.webapiPanClient
|
||||
}
|
||||
|
||||
func (p *PanClient) OpenapiPanClient() *aliyunpan_open.OpenPanClient {
|
||||
return p.openapiPanClient
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/adrg/xdg"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdutil"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdutil/jsonhelper"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/library/homedir"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvDownloadDir 配置下载环境变量
|
||||
EnvDownloadDir = "ALIYUNPAN_DOWNLOAD_DIR"
|
||||
// EnvVerbose 启用调试环境变量
|
||||
EnvVerbose = "ALIYUNPAN_VERBOSE"
|
||||
// EnvConfigDir 配置路径环境变量
|
||||
EnvConfigDir = "ALIYUNPAN_CONFIG_DIR"
|
||||
// ConfigName 配置文件名
|
||||
ConfigName = "aliyunpan_config.json"
|
||||
// ConfigVersion 配置文件版本
|
||||
ConfigVersion string = "1.0"
|
||||
|
||||
// DefaultFileUploadParallelNum 默认的文件上传并发数量
|
||||
DefaultFileUploadParallelNum = 10
|
||||
|
||||
// MaxFileUploadParallelNum 最大文件上传并发数量。过大会被阿里云盘风控,导致无法上传
|
||||
MaxFileUploadParallelNum = 20
|
||||
|
||||
// DefaultFileDownloadParallelNum 默认的文件下载并发数量
|
||||
DefaultFileDownloadParallelNum = 5
|
||||
|
||||
// MaxFileDownloadParallelNum 最大文件下载并发数量。过大会被阿里云盘风控,导致无法下载
|
||||
MaxFileDownloadParallelNum = 3
|
||||
|
||||
// DefaultTokenServiceWebHost 默认的token服务
|
||||
DefaultTokenServiceWebHost = "https://api.tickstep.com"
|
||||
//DefaultTokenServiceWebHost = "http://localhost:8977"
|
||||
|
||||
// DefaultVideoFileExtensions 默认的视频文件后缀
|
||||
DefaultVideoFileExtensions = "mp4,flv,mkv,mov,rm,rmvb,wmv,wma,mv,asf,asx,mpg,mpeg,mpe,3gp,m4v,avi,vob"
|
||||
|
||||
// DefaultDeviceName 默认客户端名称
|
||||
DefaultDeviceName = "Chrome浏览器"
|
||||
|
||||
// DefaultClientId 默认的clientId
|
||||
DefaultClientId = "cf9f70e8fc61430f8ec5ab5cadf31375"
|
||||
)
|
||||
|
||||
var (
|
||||
CmdConfigVerbose = logger.New("CONFIG", EnvVerbose)
|
||||
configFilePath = filepath.Join(GetConfigDir(), ConfigName)
|
||||
|
||||
// Config 配置信息, 由外部调用
|
||||
Config = NewConfig(configFilePath)
|
||||
)
|
||||
|
||||
type UpdateCheckInfo struct {
|
||||
PreferUpdateSrv string `json:"preferUpdateSrv"` // 优先更新服务器,github | tickstep
|
||||
LatestVer string `json:"latestVer"` // 最后检测到的版本
|
||||
CheckTime int64 `json:"checkTime"` // 最后检测的时间戳,单位为秒
|
||||
}
|
||||
|
||||
// PanConfig 配置详情
|
||||
type PanConfig struct {
|
||||
ConfigVer string `json:"configVer"`
|
||||
ActiveUID string `json:"activeUID"`
|
||||
|
||||
UserList PanUserList `json:"userList"`
|
||||
|
||||
CacheSize int `json:"cacheSize"` // 下载缓存
|
||||
MaxDownloadParallel int `json:"maxDownloadParallel"` // 最大下载并发量,即同时下载文件最大数量
|
||||
MaxUploadParallel int `json:"maxUploadParallel"` // 最大上传并发量,即同时上传文件最大数量
|
||||
|
||||
MaxDownloadRate int64 `json:"maxDownloadRate"` // 限制最大下载速度,单位 B/s, 即字节/每秒
|
||||
MaxUploadRate int64 `json:"maxUploadRate"` // 限制最大上传速度,单位 B/s, 即字节/每秒
|
||||
|
||||
SaveDir string `json:"saveDir"` // 下载储存路径
|
||||
|
||||
Proxy string `json:"proxy"` // 代理
|
||||
LocalAddrs string `json:"localAddrs"` // 本地网卡地址
|
||||
PreferIPType string `json:"preferIPType"` // 优先IP类型,IPv4或者IPv6
|
||||
UpdateCheckInfo UpdateCheckInfo `json:"updateCheckInfo"`
|
||||
|
||||
VideoFileExtensions string `json:"videoFileExtensions"`
|
||||
FileRecordConfig string `json:"fileRecordConfig"` // 上传、下载、同步文件的记录,包括失败和成功的
|
||||
|
||||
DeviceId string `json:"deviceId"` // 客户端ID,用于标识登录客户端,阿里单个账号最多允许10个客户端同时登录
|
||||
DeviceName string `json:"deviceName"` // 客户端名称,默认为:Chrome浏览器
|
||||
|
||||
// Openapi客户端信息
|
||||
ClientId string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
|
||||
// 本地工作目录(lcd/lpwd/lls命令使用)
|
||||
LocalWorkdir string `json:"localWorkdir"`
|
||||
|
||||
configFilePath string
|
||||
configFile *os.File
|
||||
fileMu sync.Mutex
|
||||
activeUser *PanUser
|
||||
}
|
||||
|
||||
// NewConfig 返回 PanConfig 指针对象
|
||||
func NewConfig(configFilePath string) *PanConfig {
|
||||
c := &PanConfig{
|
||||
configFilePath: configFilePath,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Init 初始化配置
|
||||
func (c *PanConfig) Init() error {
|
||||
return c.init()
|
||||
}
|
||||
|
||||
// Reload 从文件重载配置
|
||||
func (c *PanConfig) Reload() error {
|
||||
return c.init()
|
||||
}
|
||||
|
||||
// Close 关闭配置文件
|
||||
func (c *PanConfig) Close() error {
|
||||
if c.configFile != nil {
|
||||
err := c.configFile.Close()
|
||||
c.configFile = nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save 保存配置信息到配置文件
|
||||
func (c *PanConfig) Save() error {
|
||||
// 检测配置项是否合法, 不合法则自动修复
|
||||
c.fix()
|
||||
|
||||
err := c.lazyOpenConfigFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.fileMu.Lock()
|
||||
defer c.fileMu.Unlock()
|
||||
|
||||
data, err := jsoniter.MarshalIndent(c, "", " ")
|
||||
if err != nil {
|
||||
// json数据生成失败
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 减掉多余的部分
|
||||
err = c.configFile.Truncate(int64(len(data)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.configFile.Seek(0, os.SEEK_SET)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.configFile.Write(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PanConfig) init() error {
|
||||
if c.configFilePath == "" {
|
||||
return ErrConfigFileNotExist
|
||||
}
|
||||
|
||||
c.initDefaultConfig()
|
||||
err := c.loadConfigFromFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 设置全局代理
|
||||
if c.Proxy != "" {
|
||||
requester.SetGlobalProxy(c.Proxy)
|
||||
}
|
||||
|
||||
// 设置域名解析策略 IPv4 or IPv6
|
||||
t := requester.IPAny
|
||||
if strings.ToLower(c.PreferIPType) == "ipv4" {
|
||||
t = requester.IPv4
|
||||
} else if strings.ToLower(c.PreferIPType) == "ipv6" {
|
||||
t = requester.IPv6
|
||||
}
|
||||
requester.SetPreferIPType(t)
|
||||
|
||||
// 设置本地网卡地址
|
||||
if c.LocalAddrs != "" {
|
||||
ips := ParseLocalAddress(c.LocalAddrs, strings.ToLower(c.PreferIPType))
|
||||
if len(ips) > 0 {
|
||||
logger.Verboseln("bind local address list: ", ips)
|
||||
requester.SetLocalTCPAddrList(ips...)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// lazyOpenConfigFile 打开配置文件
|
||||
func (c *PanConfig) lazyOpenConfigFile() (err error) {
|
||||
if c.configFile != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.fileMu.Lock()
|
||||
os.MkdirAll(filepath.Dir(c.configFilePath), 0755)
|
||||
c.configFile, err = os.OpenFile(c.configFilePath, os.O_CREATE|os.O_RDWR, 0755)
|
||||
c.fileMu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
return ErrConfigFileNoPermission
|
||||
}
|
||||
if os.IsExist(err) {
|
||||
return ErrConfigFileNotExist
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadConfigFromFile 载入配置
|
||||
func (c *PanConfig) loadConfigFromFile() (err error) {
|
||||
err = c.lazyOpenConfigFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 未初始化
|
||||
info, err := c.configFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.Size() == 0 {
|
||||
err = c.Save()
|
||||
return err
|
||||
}
|
||||
|
||||
c.fileMu.Lock()
|
||||
defer c.fileMu.Unlock()
|
||||
|
||||
_, err = c.configFile.Seek(0, os.SEEK_SET)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = jsonhelper.UnmarshalData(c.configFile, c)
|
||||
if err != nil {
|
||||
return ErrConfigContentsParseError
|
||||
}
|
||||
if c.DeviceName == "" {
|
||||
c.DeviceName = DefaultDeviceName
|
||||
}
|
||||
if c.ClientId == "" {
|
||||
c.ClientId = DefaultClientId
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PanConfig) initDefaultConfig() {
|
||||
//c.SaveDir = GetDefaultDownloadDir()
|
||||
c.ConfigVer = ConfigVersion
|
||||
c.VideoFileExtensions = DefaultVideoFileExtensions
|
||||
c.DeviceId = RandomDeviceId() // 生成默认客户端ID
|
||||
c.DeviceName = DefaultDeviceName
|
||||
c.ClientId = DefaultClientId
|
||||
c.FileRecordConfig = "2" // 默认关闭
|
||||
c.PreferIPType = "ipv4" // 默认优先IPv4
|
||||
}
|
||||
|
||||
// GetConfigDir 获取配置路径
|
||||
func GetConfigDir() string {
|
||||
// 按照以下顺序依次获取配置目录
|
||||
// 1.环境变量ALIYUNPAN_CONFIG_DIR => 2. XDG_CONFIG_HOME/aliyunpan => 3. /etc/aliyunpan/ => 4. ~/.aliyunpan/ => 5.当前程序目录
|
||||
|
||||
// 1. 从环境变量读取
|
||||
configDir, ok := os.LookupEnv(EnvConfigDir)
|
||||
if ok {
|
||||
if filepath.IsAbs(configDir) {
|
||||
logger.Verboseln("use config dir from ALIYUNPAN_CONFIG_DIR env: ", configDir)
|
||||
return configDir
|
||||
}
|
||||
// 如果不是绝对路径, 从程序目录寻找
|
||||
configDir = cmdutil.ExecutablePathJoin(configDir)
|
||||
logger.Verboseln("use config dir from ALIYUNPAN_CONFIG_DIR env: ", configDir)
|
||||
return configDir
|
||||
} else {
|
||||
// 2. $XDG_CONFIG_HOME/aliyunpan
|
||||
xdgConfigHome := path.Join(xdg.ConfigHome, "aliyunpan")
|
||||
if IsFolderExist(xdgConfigHome) {
|
||||
logger.Verboseln("use XDG home config dir: ", xdgConfigHome)
|
||||
return xdgConfigHome
|
||||
}
|
||||
|
||||
// 3. /etc/aliyunpan/
|
||||
if runtime.GOOS == "linux" {
|
||||
cd := "/etc/aliyunpan"
|
||||
if IsFolderExist(cd) {
|
||||
logger.Verboseln("use config dir: ", cd)
|
||||
return cd
|
||||
}
|
||||
}
|
||||
|
||||
// 4. ~/.aliyunpan/
|
||||
if runtime.GOOS == "linux" || runtime.GOOS == "windows" {
|
||||
cd, er := homedir.Expand("~/.aliyunpan")
|
||||
if er == nil {
|
||||
if IsFolderExist(cd) {
|
||||
logger.Verboseln("use config dir: ", cd)
|
||||
return cd
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5.当前程序所在目录
|
||||
configDir = cmdutil.ExecutablePathJoin("")
|
||||
logger.Verboseln("use config dir: ", configDir)
|
||||
return configDir
|
||||
}
|
||||
|
||||
// GetDefaultDownloadDir 获取默认的下载目录
|
||||
func GetDefaultDownloadDir() string {
|
||||
// 按照以下顺序依次获取下载目录
|
||||
// 1.环境变量ALIYUNPAN_DOWNLOAD_DIR => 2. XDG_DOWNLOAD_DIR目录 => 3. (当前程序目录)/Downloads
|
||||
|
||||
downloadDir := ""
|
||||
// 设置默认的下载路径
|
||||
switch runtime.GOOS {
|
||||
case "android":
|
||||
// TODO: 获取完整的的下载路径
|
||||
downloadDir = "/sdcard/Download"
|
||||
return downloadDir
|
||||
default:
|
||||
// 1. 环境变量ALIYUNPAN_DOWNLOAD_DIR
|
||||
d, ok := os.LookupEnv(EnvDownloadDir)
|
||||
if ok {
|
||||
logger.Verboseln("use download dir from ALIYUNPAN_DOWNLOAD_DIR env: ", d)
|
||||
downloadDir = d
|
||||
return downloadDir
|
||||
} else {
|
||||
// 2. $XDG_DOWNLOAD_DIR
|
||||
xdgDownloadDir := path.Join(xdg.UserDirs.Download)
|
||||
if IsFolderExist(xdgDownloadDir) {
|
||||
logger.Verboseln("use XDG download dir: ", xdgDownloadDir)
|
||||
return xdgDownloadDir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. (当前程序目录)/Downloads
|
||||
downloadDir = cmdutil.ExecutablePathJoin("Downloads")
|
||||
logger.Verboseln("use default download dir: ", downloadDir)
|
||||
return downloadDir
|
||||
}
|
||||
|
||||
// GetDownloadDir 获取当前下载目录
|
||||
func GetDownloadDir() string {
|
||||
if Config.SaveDir == "" {
|
||||
return GetDefaultDownloadDir()
|
||||
}
|
||||
return Config.SaveDir
|
||||
}
|
||||
|
||||
// GetPluginDir 获取插件文件夹路径
|
||||
func GetPluginDir() string {
|
||||
return strings.TrimSuffix(GetConfigDir(), "/") + "/plugin"
|
||||
}
|
||||
|
||||
// GetPluginKvFile 获取插件KV键值对存储文件
|
||||
func GetPluginKvFile() string {
|
||||
return GetPluginDir() + "/kv.bolt"
|
||||
}
|
||||
|
||||
// GetSyncDriveDir 获取同步备份的文件夹路径
|
||||
func GetSyncDriveDir() string {
|
||||
return strings.TrimSuffix(GetConfigDir(), "/") + "/sync_drive"
|
||||
}
|
||||
|
||||
// GetLogDir 获取日志文件目录路径
|
||||
func GetLogDir() string {
|
||||
return strings.TrimSuffix(GetConfigDir(), "/") + "/logs"
|
||||
}
|
||||
|
||||
// GetLogFilePath 获取日志文件路径
|
||||
func GetLogFilePath() string {
|
||||
dirPath := GetLogDir()
|
||||
if b, e := utils.PathExists(dirPath); e == nil {
|
||||
if !b {
|
||||
os.MkdirAll(dirPath, 0755)
|
||||
}
|
||||
}
|
||||
return dirPath + "/" + "aliyunpan_verbose.log"
|
||||
}
|
||||
|
||||
// GetLockerDir 获取文件锁路径
|
||||
func GetLockerDir() string {
|
||||
return strings.TrimSuffix(GetConfigDir(), "/")
|
||||
}
|
||||
|
||||
func (c *PanConfig) ActiveUser() *PanUser {
|
||||
if c.activeUser == nil {
|
||||
if c.UserList == nil {
|
||||
return nil
|
||||
}
|
||||
if c.ActiveUID == "" {
|
||||
return nil
|
||||
}
|
||||
for _, u := range c.UserList {
|
||||
if u.UserId == c.ActiveUID {
|
||||
if u.PanClient() == nil {
|
||||
// restore client
|
||||
user, err := SetupUserByCookie(u.OpenapiToken, u.WebapiToken,
|
||||
u.TicketId, u.UserId,
|
||||
c.DeviceId, c.DeviceName,
|
||||
c.ClientId, c.ClientSecret)
|
||||
if err != nil {
|
||||
logger.Verboseln("setup user error")
|
||||
return nil
|
||||
}
|
||||
u.panClient = user.panClient
|
||||
u.Nickname = user.Nickname
|
||||
|
||||
if u.ActiveDriveId == "" {
|
||||
u.ActiveDriveId = user.DriveList.GetFileDriveId()
|
||||
}
|
||||
u.DriveList = user.DriveList
|
||||
// check workdir valid or not
|
||||
if user.IsFileDriveActive() {
|
||||
fe, err1 := u.PanClient().OpenapiPanClient().FileInfoByPath(u.ActiveDriveId, u.Workdir)
|
||||
if err1 != nil {
|
||||
// default to root
|
||||
u.Workdir = "/"
|
||||
u.WorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
|
||||
} else {
|
||||
u.WorkdirFileEntity = *fe
|
||||
if u.Workdir == "" {
|
||||
u.Workdir = "/"
|
||||
}
|
||||
}
|
||||
} else if user.IsResourceDriveActive() {
|
||||
fe, err1 := u.PanClient().OpenapiPanClient().FileInfoByPath(u.ActiveDriveId, u.ResourceWorkdir)
|
||||
if err1 != nil {
|
||||
// default to root
|
||||
u.ResourceWorkdir = "/"
|
||||
u.ResourceWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
|
||||
} else {
|
||||
u.ResourceWorkdirFileEntity = *fe
|
||||
if u.ResourceWorkdir == "" {
|
||||
u.ResourceWorkdir = "/"
|
||||
}
|
||||
}
|
||||
} else if user.IsAlbumDriveActive() {
|
||||
fe, err1 := u.PanClient().WebapiPanClient().FileInfoByPath(u.ActiveDriveId, u.AlbumWorkdir)
|
||||
if err1 != nil {
|
||||
// default to root
|
||||
u.AlbumWorkdir = "/"
|
||||
u.AlbumWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
|
||||
} else {
|
||||
u.AlbumWorkdirFileEntity = *fe
|
||||
if u.AlbumWorkdir == "" {
|
||||
u.AlbumWorkdir = "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
c.activeUser = u
|
||||
return u
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return c.activeUser
|
||||
}
|
||||
|
||||
func (c *PanConfig) SetActiveUser(user *PanUser) *PanUser {
|
||||
needToInsert := true
|
||||
for _, u := range c.UserList {
|
||||
if u.UserId == user.UserId {
|
||||
// update user info
|
||||
u.Nickname = user.Nickname
|
||||
u.WebapiToken = user.WebapiToken
|
||||
u.OpenapiToken = user.OpenapiToken
|
||||
u.TicketId = user.TicketId
|
||||
if u.PanClient() != nil && user.PanClient() != nil {
|
||||
u.UpdateClient(user.PanClient().OpenapiPanClient(), user.PanClient().WebapiPanClient())
|
||||
}
|
||||
needToInsert = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if needToInsert {
|
||||
// insert
|
||||
c.UserList = append(c.UserList, user)
|
||||
}
|
||||
|
||||
// setup active user
|
||||
c.ActiveUID = user.UserId
|
||||
// clear active user cache
|
||||
c.activeUser = nil
|
||||
// reload
|
||||
return c.ActiveUser()
|
||||
}
|
||||
|
||||
func (c *PanConfig) fix() {
|
||||
|
||||
}
|
||||
|
||||
// NumLogins 获取登录的用户数量
|
||||
func (c *PanConfig) NumLogins() int {
|
||||
return len(c.UserList)
|
||||
}
|
||||
|
||||
// SwitchUser 切换登录用户
|
||||
func (c *PanConfig) SwitchUser(uid string) (*PanUser, error) {
|
||||
for _, u := range c.UserList {
|
||||
if u.UserId == uid {
|
||||
return c.SetActiveUser(u), nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("未找到指定的账号")
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户,并自动切换登录用户为用户列表第一个
|
||||
func (c *PanConfig) DeleteUser(uid string) (*PanUser, error) {
|
||||
for idx, u := range c.UserList {
|
||||
if u.UserId == uid {
|
||||
// delete user from user list
|
||||
c.UserList = append(c.UserList[:idx], c.UserList[idx+1:]...)
|
||||
c.ActiveUID = ""
|
||||
c.activeUser = nil
|
||||
if len(c.UserList) > 0 {
|
||||
c.SwitchUser(c.UserList[0].UserId)
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("未找到指定的账号")
|
||||
}
|
||||
|
||||
// HTTPClient 返回设置好的 HTTPClient
|
||||
func (c *PanConfig) HTTPClient(ua string) *requester.HTTPClient {
|
||||
client := requester.NewHTTPClient()
|
||||
if ua != "" {
|
||||
client.SetUserAgent(ua)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *PanConfig) GetVideoExtensionList() []string {
|
||||
if c.VideoFileExtensions == "" {
|
||||
return []string{}
|
||||
}
|
||||
exts := strings.Split(c.VideoFileExtensions, ",")
|
||||
r := []string{}
|
||||
for _, ex := range exts {
|
||||
ex = strings.TrimSpace(ex)
|
||||
if ex != "" {
|
||||
r = append(r, strings.ToLower(ex))
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
)
|
||||
|
||||
// SetProxy 设置代理
|
||||
func (c *PanConfig) SetProxy(proxy string) {
|
||||
c.Proxy = proxy
|
||||
requester.SetGlobalProxy(proxy)
|
||||
}
|
||||
|
||||
// SetLocalAddrs 设置localAddrs
|
||||
func (c *PanConfig) SetLocalAddrs(localAddrs string) {
|
||||
c.LocalAddrs = localAddrs
|
||||
ips := ParseLocalAddress(localAddrs, strings.ToLower(c.PreferIPType))
|
||||
if len(ips) > 0 {
|
||||
requester.SetLocalTCPAddrList(ips...)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PanConfig) SetPreferIPType(ipType string) {
|
||||
c.PreferIPType = ipType
|
||||
t := requester.IPAny
|
||||
if strings.ToLower(ipType) == "ipv4" {
|
||||
t = requester.IPv4
|
||||
} else if strings.ToLower(ipType) == "ipv6" {
|
||||
t = requester.IPv6
|
||||
}
|
||||
requester.SetPreferIPType(t)
|
||||
}
|
||||
|
||||
// SetCacheSizeByStr 设置cache_size
|
||||
func (c *PanConfig) SetCacheSizeByStr(sizeStr string) error {
|
||||
size, err := converter.ParseFileSizeStr(sizeStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.CacheSize = int(size)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMaxDownloadRateByStr 设置 max_download_rate
|
||||
func (c *PanConfig) SetMaxDownloadRateByStr(sizeStr string) error {
|
||||
size, err := converter.ParseFileSizeStr(stripPerSecond(sizeStr))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.MaxDownloadRate = size
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMaxUploadRateByStr 设置 max_upload_rate
|
||||
func (c *PanConfig) SetMaxUploadRateByStr(sizeStr string) error {
|
||||
size, err := converter.ParseFileSizeStr(stripPerSecond(sizeStr))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.MaxUploadRate = size
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetFileRecorderConfig 设置文件记录器
|
||||
func (c *PanConfig) SetFileRecorderConfig(config string) error {
|
||||
if config == "1" || config == "2" {
|
||||
c.FileRecordConfig = config
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDeviceId 设置客户端ID
|
||||
func (c *PanConfig) SetDeviceId(deviceId string) error {
|
||||
if deviceId == "" {
|
||||
return nil
|
||||
}
|
||||
c.DeviceId = deviceId
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrintTable 输出表格
|
||||
func (c *PanConfig) PrintTable() {
|
||||
fileRecorderLabel := "禁用"
|
||||
if c.FileRecordConfig == "1" {
|
||||
fileRecorderLabel = "开启"
|
||||
}
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"名称", "值", "建议值", "描述"})
|
||||
tb.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
|
||||
tb.AppendBulk([][]string{
|
||||
[]string{"cache_size", converter.ConvertFileSize(int64(c.CacheSize), 2), "1KB ~ 256KB", "下载缓存, 如果硬盘占用高或下载速度慢, 请尝试调大此值"},
|
||||
[]string{"max_download_parallel", strconv.Itoa(c.MaxDownloadParallel), "1 ~ 3", "最大下载并发量,即同时下载文件最大数量"},
|
||||
[]string{"max_upload_parallel", strconv.Itoa(c.MaxUploadParallel), "1 ~ 20", "最大上传并发量,即同时上传文件最大数量"},
|
||||
[]string{"max_download_rate", showMaxRate(c.MaxDownloadRate), "", "限制单个文件最大下载速度, 0代表不限制"},
|
||||
[]string{"max_upload_rate", showMaxRate(c.MaxUploadRate), "", "限制单个文件最大上传速度, 0代表不限制"},
|
||||
[]string{"savedir", GetDownloadDir(), "", "下载文件的储存目录"},
|
||||
[]string{"proxy", c.Proxy, "", "设置代理, 支持 http/socks5 代理,例如: http://127.0.0.1:8888 或者 socks5://127.0.0.1:8889"},
|
||||
[]string{"local_addrs", c.LocalAddrs, "", "绑定本地网卡地址, 多个地址用逗号隔开,支持网口名称,例如: 127.0.0.1,192.168.100.126,en0,eth0"},
|
||||
[]string{"ip_type", c.PreferIPType, "ipv4-优先IPv4,ipv6-优先IPv6", "设置域名解析IP优先类型。修改后需要重启应用生效"},
|
||||
[]string{"file_record_config", fileRecorderLabel, "1-开启,2-禁用", "设置是否开启上传、下载、同步文件的结果记录,开启后会把结果记录到CSV文件方便后期查看"},
|
||||
[]string{"device_id", c.DeviceId, "", "客户端ID,用于标识登录客户端,阿里单个账号最多允许10个客户端同时在线。修改后需要重启应用生效"},
|
||||
})
|
||||
tb.Render()
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_open"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
|
||||
"github.com/tickstep/library-go/expires/cachemap"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DriveInfo struct {
|
||||
DriveId string `json:"driveId"`
|
||||
DriveName string `json:"driveName"`
|
||||
DriveTag string `json:"driveTag"`
|
||||
}
|
||||
type DriveInfoList []*DriveInfo
|
||||
|
||||
func (d DriveInfoList) GetFileDriveId() string {
|
||||
for _, drive := range d {
|
||||
if drive.DriveTag == "File" {
|
||||
return drive.DriveId
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d DriveInfoList) GetAlbumDriveId() string {
|
||||
for _, drive := range d {
|
||||
if drive.DriveTag == "Album" {
|
||||
return drive.DriveId
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d DriveInfoList) GetResourceDriveId() string {
|
||||
for _, drive := range d {
|
||||
if drive.DriveTag == "Resource" {
|
||||
return drive.DriveId
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d DriveInfoList) GetDriveIdByName(name string) string {
|
||||
for _, drive := range d {
|
||||
if drive.DriveTag == name {
|
||||
return drive.DriveId
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d DriveInfoList) GetDriveNameById(driveId string) string {
|
||||
for _, drive := range d {
|
||||
if drive.DriveId == driveId {
|
||||
return drive.DriveName
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// PanClientToken 授权Token
|
||||
type PanClientToken struct {
|
||||
// AccessToken AccessToken
|
||||
AccessToken string `json:"accessToken"`
|
||||
// Expired 过期时间戳,单位秒
|
||||
Expired int64 `json:"expired"`
|
||||
}
|
||||
|
||||
// GetExpiredTimeCstStr 获取东八区时间字符串
|
||||
func (t *PanClientToken) GetExpiredTimeCstStr() string {
|
||||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||||
tm := time.Unix(t.Expired, 0).In(cz)
|
||||
return tm.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
type PanUser struct {
|
||||
// 用户信息
|
||||
UserId string `json:"userId"`
|
||||
Nickname string `json:"nickname"`
|
||||
AccountName string `json:"accountName"`
|
||||
|
||||
// 文件(备份盘)
|
||||
Workdir string `json:"workdir"`
|
||||
WorkdirFileEntity aliyunpan.FileEntity `json:"workdirFileEntity"`
|
||||
|
||||
// 资源库
|
||||
ResourceWorkdir string `json:"resourceWorkdir"`
|
||||
ResourceWorkdirFileEntity aliyunpan.FileEntity `json:"resourceWorkdirFileEntity"`
|
||||
|
||||
// 相册
|
||||
AlbumWorkdir string `json:"albumWorkdir"`
|
||||
AlbumWorkdirFileEntity aliyunpan.FileEntity `json:"albumWorkdirFileEntity"`
|
||||
|
||||
// 用户网盘信息
|
||||
ActiveDriveId string `json:"activeDriveId"`
|
||||
DriveList DriveInfoList `json:"driveList"`
|
||||
|
||||
// 授权Token信息
|
||||
TicketId string `json:"ticketId"`
|
||||
WebapiToken *PanClientToken `json:"webapiToken"`
|
||||
OpenapiToken *PanClientToken `json:"openapiToken"`
|
||||
|
||||
// API客户端
|
||||
panClient *PanClient `json:"-"`
|
||||
cacheOpMap cachemap.CacheOpMap `json:"-"`
|
||||
}
|
||||
|
||||
type PanUserList []*PanUser
|
||||
|
||||
func SetupUserByCookie(openapiToken, webapiToken *PanClientToken, ticketId, userId, deviceId, deviceName, clientId, clientSecret string) (user *PanUser, err *apierror.ApiError) {
|
||||
tryRefreshWebToken := true
|
||||
tryRefreshOpenToken := true
|
||||
loginHelper := panlogin.NewLoginHelper(DefaultTokenServiceWebHost)
|
||||
|
||||
if openapiToken == nil {
|
||||
return nil, apierror.NewFailedApiError("openapi token is empty")
|
||||
}
|
||||
//if webapiToken == nil {
|
||||
// return nil, apierror.NewFailedApiError("webapi token is empty")
|
||||
//}
|
||||
|
||||
doOpenLoginAct:
|
||||
// setup openapi client
|
||||
openPanClient := aliyunpan_open.NewOpenPanClient(openapi.ApiConfig{
|
||||
TicketId: ticketId,
|
||||
UserId: userId,
|
||||
ClientId: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
}, openapi.ApiToken{
|
||||
AccessToken: openapiToken.AccessToken,
|
||||
ExpiredAt: openapiToken.Expired,
|
||||
}, nil)
|
||||
openPanClient.SetAccessTokenRefreshCallback(func(userId string, newToken openapi.ApiToken) error {
|
||||
// api层自动刷新Token,回调通知程序
|
||||
logger.Verboseln("openapi token refresh, use new token")
|
||||
openapiToken.AccessToken = newToken.AccessToken
|
||||
openapiToken.Expired = newToken.ExpiredAt
|
||||
return nil
|
||||
})
|
||||
|
||||
// open api token maybe expired
|
||||
// check & refresh new one
|
||||
openUserInfo, err := openPanClient.GetUserInfo()
|
||||
if err != nil {
|
||||
if err.Code == apierror.ApiCodeTokenExpiredCode && tryRefreshOpenToken { // 如果是token过期,则尝试刷新
|
||||
tryRefreshOpenToken = false
|
||||
wt, re := loginHelper.GetOpenapiNewToken(ticketId, userId, openapiToken.AccessToken)
|
||||
if re != nil {
|
||||
logger.Verboseln("get openapi token from server error: ", re.Msg)
|
||||
return nil, apierror.NewFailedApiError("get new openapi token error, try login again")
|
||||
}
|
||||
if wt != nil {
|
||||
openapiToken = &PanClientToken{
|
||||
AccessToken: wt.AccessToken,
|
||||
Expired: wt.Expired,
|
||||
}
|
||||
|
||||
// TODO: 检查token有效期
|
||||
}
|
||||
|
||||
// 回到token检测流程,必须确保token是有效
|
||||
time.Sleep(time.Duration(1) * time.Second)
|
||||
goto doOpenLoginAct
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
doWebLoginAct:
|
||||
// setup webapi client
|
||||
var webUserInfo *aliyunpan.UserInfo
|
||||
var err2 *apierror.ApiError
|
||||
var webPanClient *aliyunpan_web.WebPanClient
|
||||
if webapiToken != nil && webapiToken.AccessToken != "" {
|
||||
appConfig := aliyunpan_web.AppConfig{
|
||||
AppId: "25dzX3vbYqktVxyX",
|
||||
DeviceId: deviceId,
|
||||
UserId: userId,
|
||||
Nonce: 0,
|
||||
PublicKey: "",
|
||||
}
|
||||
webPanClient = aliyunpan_web.NewWebPanClient(aliyunpan_web.WebLoginToken{
|
||||
AccessTokenType: "Bearer",
|
||||
AccessToken: webapiToken.AccessToken,
|
||||
RefreshToken: "",
|
||||
ExpiresIn: 7200,
|
||||
ExpireTime: webapiToken.GetExpiredTimeCstStr(),
|
||||
}, aliyunpan_web.AppLoginToken{}, appConfig, aliyunpan_web.SessionConfig{
|
||||
DeviceName: deviceName,
|
||||
ModelName: "Windows网页版",
|
||||
})
|
||||
// web api token maybe expired
|
||||
// check & refresh new one
|
||||
webUserInfo, err2 = webPanClient.GetUserInfo()
|
||||
if err2 != nil {
|
||||
if err2.Code == apierror.ApiCodeTokenExpiredCode && tryRefreshWebToken {
|
||||
tryRefreshWebToken = false
|
||||
wt, e := loginHelper.GetWebapiNewToken(ticketId, userId, webapiToken.AccessToken)
|
||||
if e != nil {
|
||||
logger.Verboseln("get web token from server error: ", e.Msg)
|
||||
}
|
||||
if wt != nil {
|
||||
webapiToken = &PanClientToken{
|
||||
AccessToken: wt.AccessToken,
|
||||
Expired: wt.Expired,
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Duration(1) * time.Second)
|
||||
goto doWebLoginAct
|
||||
}
|
||||
webPanClient = nil
|
||||
//return nil, err2
|
||||
}
|
||||
// web create session
|
||||
if webUserInfo != nil {
|
||||
appConfig.UserId = webUserInfo.UserId
|
||||
webPanClient.UpdateAppConfig(appConfig)
|
||||
r, e := webPanClient.CreateSession(nil)
|
||||
if e != nil {
|
||||
logger.Verboseln("call CreateSession error in SetupUserByCookie: " + e.Error())
|
||||
}
|
||||
if r != nil && !r.Result {
|
||||
logger.Verboseln("上传签名秘钥失败,可能是你账号登录的设备已超最大数量")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// setup PanUser
|
||||
//
|
||||
u := &PanUser{
|
||||
WebapiToken: webapiToken,
|
||||
OpenapiToken: openapiToken,
|
||||
panClient: NewPanClient(webPanClient, openPanClient),
|
||||
Workdir: "/",
|
||||
WorkdirFileEntity: *aliyunpan.NewFileEntityForRootDir(),
|
||||
}
|
||||
u.PanClient().OpenapiPanClient().SetAccessTokenRefreshCallback(func(userId string, newToken openapi.ApiToken) error {
|
||||
// api层自动刷新Token,回调通知程序
|
||||
logger.Verboseln("openapi token refresh, update for user")
|
||||
u.OpenapiToken = &PanClientToken{
|
||||
AccessToken: newToken.AccessToken,
|
||||
Expired: newToken.ExpiredAt,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// setup user info
|
||||
name := "Unknown"
|
||||
if openUserInfo != nil {
|
||||
// fill userId for client
|
||||
u.PanClient().OpenapiPanClient().UpdateUserId(openUserInfo.UserId)
|
||||
if u.PanClient().WebapiPanClient() != nil {
|
||||
u.PanClient().WebapiPanClient().UpdateUserId(openUserInfo.UserId)
|
||||
}
|
||||
|
||||
// update user
|
||||
if openUserInfo.Nickname != "" {
|
||||
name = openUserInfo.Nickname
|
||||
}
|
||||
u.UserId = openUserInfo.UserId
|
||||
u.Nickname = name
|
||||
u.AccountName = openUserInfo.UserName
|
||||
|
||||
// default file drive
|
||||
if u.ActiveDriveId == "" {
|
||||
u.ActiveDriveId = openUserInfo.FileDriveId
|
||||
}
|
||||
|
||||
// drive list
|
||||
u.DriveList = DriveInfoList{
|
||||
{DriveId: openUserInfo.FileDriveId, DriveTag: "File", DriveName: "备份盘"},
|
||||
{DriveId: openUserInfo.ResourceDriveId, DriveTag: "Resource", DriveName: "资源库"},
|
||||
}
|
||||
if webUserInfo != nil {
|
||||
u.AccountName = webUserInfo.UserName
|
||||
u.DriveList = append(u.DriveList, &DriveInfo{DriveId: webUserInfo.AlbumDriveId, DriveTag: "Album", DriveName: "相册"})
|
||||
}
|
||||
} else {
|
||||
// error, maybe the token has expired
|
||||
return nil, apierror.NewFailedApiError("cannot get user info, the token has expired. please login again")
|
||||
}
|
||||
|
||||
// return user
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (pu *PanUser) PanClient() *PanClient {
|
||||
return pu.panClient
|
||||
}
|
||||
|
||||
func (pu *PanUser) UpdateClient(openClient *aliyunpan_open.OpenPanClient, webClient *aliyunpan_web.WebPanClient) {
|
||||
if pu.panClient == nil {
|
||||
pu.panClient = &PanClient{
|
||||
webapiPanClient: webClient,
|
||||
openapiPanClient: openClient,
|
||||
}
|
||||
} else {
|
||||
pu.panClient.webapiPanClient = webClient
|
||||
pu.panClient.openapiPanClient = openClient
|
||||
}
|
||||
}
|
||||
|
||||
// PathJoin 合并工作目录和相对路径p, 若p为绝对路径则忽略
|
||||
func (pu *PanUser) PathJoin(driveId, p string) string {
|
||||
if path.IsAbs(p) {
|
||||
return p
|
||||
}
|
||||
wd := "/"
|
||||
di := pu.GetDriveById(driveId)
|
||||
if di != nil {
|
||||
if di.IsFileDrive() {
|
||||
wd = pu.Workdir
|
||||
} else if di.IsResourceDrive() {
|
||||
wd = pu.ResourceWorkdir
|
||||
} else if di.IsAlbumDrive() {
|
||||
wd = pu.AlbumWorkdir
|
||||
}
|
||||
}
|
||||
return path.Join(wd, p)
|
||||
}
|
||||
|
||||
func (pu *PanUser) FreshWorkdirInfo() {
|
||||
if pu.IsFileDriveActive() {
|
||||
fe, err := pu.PanClient().OpenapiPanClient().FileInfoById(pu.ActiveDriveId, pu.WorkdirFileEntity.FileId)
|
||||
if err != nil {
|
||||
logger.Verboseln("刷新工作目录信息失败")
|
||||
return
|
||||
}
|
||||
pu.WorkdirFileEntity = *fe
|
||||
} else if pu.IsAlbumDriveActive() {
|
||||
fe, err := pu.PanClient().OpenapiPanClient().FileInfoById(pu.ActiveDriveId, pu.AlbumWorkdirFileEntity.FileId)
|
||||
if err != nil {
|
||||
logger.Verboseln("刷新工作目录信息失败")
|
||||
return
|
||||
}
|
||||
pu.AlbumWorkdirFileEntity = *fe
|
||||
}
|
||||
}
|
||||
|
||||
// GetSavePath 根据提供的网盘文件路径 panpath, 返回本地储存路径,
|
||||
// 返回绝对路径, 获取绝对路径出错时才返回相对路径...
|
||||
func (pu *PanUser) GetSavePath(filePanPath string) string {
|
||||
dirStr := filepath.Join(GetDownloadDir(), fmt.Sprintf("%s", pu.UserId), filePanPath)
|
||||
dir, err := filepath.Abs(dirStr)
|
||||
if err != nil {
|
||||
dir = filepath.Clean(dirStr)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func (pu *PanUser) GetDriveByTag(tag string) *DriveInfo {
|
||||
for _, item := range pu.DriveList {
|
||||
if item.DriveTag == tag {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pu *PanUser) GetDriveById(id string) *DriveInfo {
|
||||
for _, item := range pu.DriveList {
|
||||
if item.DriveId == id {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pu *PanUser) GetActiveDriveInfo() *DriveInfo {
|
||||
for _, item := range pu.DriveList {
|
||||
if item.DriveId == pu.ActiveDriveId {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pu *PanUser) IsFileDriveActive() bool {
|
||||
d := pu.GetActiveDriveInfo()
|
||||
return d != nil && d.IsFileDrive()
|
||||
}
|
||||
|
||||
func (pu *PanUser) IsAlbumDriveActive() bool {
|
||||
d := pu.GetActiveDriveInfo()
|
||||
return d != nil && d.IsAlbumDrive()
|
||||
}
|
||||
|
||||
func (pu *PanUser) IsResourceDriveActive() bool {
|
||||
d := pu.GetActiveDriveInfo()
|
||||
return d != nil && d.IsResourceDrive()
|
||||
}
|
||||
|
||||
func (di *DriveInfo) IsFileDrive() bool {
|
||||
return di.DriveTag == "File"
|
||||
}
|
||||
|
||||
func (di *DriveInfo) IsAlbumDrive() bool {
|
||||
return di.DriveTag == "Album"
|
||||
}
|
||||
|
||||
func (di *DriveInfo) IsResourceDrive() bool {
|
||||
return di.DriveTag == "Resource"
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/library/nets"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/crypto"
|
||||
"github.com/tickstep/library-go/ids"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (pl *PanUserList) String() string {
|
||||
builder := &strings.Builder{}
|
||||
|
||||
tb := cmdtable.NewTable(builder)
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER})
|
||||
tb.SetHeader([]string{"#", "uid", "用户名", "昵称"})
|
||||
|
||||
for k, userInfo := range *pl {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), userInfo.UserId, userInfo.AccountName, userInfo.Nickname})
|
||||
}
|
||||
|
||||
tb.Render()
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// GetUserByUserId 根据用户ID获取用户信息
|
||||
func (pl *PanUserList) GetUserByUserId(userId string) *PanUser {
|
||||
for _, u := range *pl {
|
||||
if u.UserId == userId {
|
||||
return u
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AverageParallel 返回平均的下载最大并发量
|
||||
func AverageParallel(parallel, downloadLoad int) int {
|
||||
if downloadLoad < 1 {
|
||||
return 1
|
||||
}
|
||||
|
||||
p := parallel / downloadLoad
|
||||
if p < 1 {
|
||||
return 1
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func stripPerSecond(sizeStr string) string {
|
||||
i := strings.LastIndex(sizeStr, "/")
|
||||
if i < 0 {
|
||||
return sizeStr
|
||||
}
|
||||
return sizeStr[:i]
|
||||
}
|
||||
|
||||
func showMaxRate(size int64) string {
|
||||
if size <= 0 {
|
||||
return "不限制"
|
||||
}
|
||||
return converter.ConvertFileSize(size, 2) + "/s"
|
||||
}
|
||||
|
||||
// EncryptString 加密
|
||||
func EncryptString(text string) string {
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
d := []byte(text)
|
||||
key := []byte(ids.GetUniqueId("aliyunpan", 16))
|
||||
r, e := crypto.EncryptAES(d, key)
|
||||
if e != nil {
|
||||
return text
|
||||
}
|
||||
return hex.EncodeToString(r)
|
||||
}
|
||||
|
||||
// DecryptString 解密
|
||||
func DecryptString(text string) string {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Verboseln("decrypt string failed, maybe the key has been changed")
|
||||
}
|
||||
}()
|
||||
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
d, _ := hex.DecodeString(text)
|
||||
|
||||
// use the machine unique id as the key
|
||||
// but in some OS, this key will be changed if you reinstall the OS
|
||||
key := []byte(ids.GetUniqueId("aliyunpan", 16))
|
||||
r, e := crypto.DecryptAES(d, key)
|
||||
if e != nil {
|
||||
return text
|
||||
}
|
||||
return string(r)
|
||||
}
|
||||
|
||||
// IsFolderExist 判断文件夹是否存在
|
||||
func IsFolderExist(pathStr string) bool {
|
||||
fi, err := os.Stat(pathStr)
|
||||
if err != nil {
|
||||
if os.IsExist(err) {
|
||||
return fi.IsDir()
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
return fi.IsDir()
|
||||
}
|
||||
|
||||
// RandomDeviceId 随机生成device-id
|
||||
func RandomDeviceId() string {
|
||||
count := 24
|
||||
STR_SET := "abcdefjhijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
str := strings.Builder{}
|
||||
for i := 0; i < count; i++ {
|
||||
str.WriteByte(byte(STR_SET[rand.Intn(len(STR_SET))]))
|
||||
}
|
||||
return str.String()
|
||||
}
|
||||
|
||||
// ParseLocalAddress 解析网络接口配置为对应的本地IP地址
|
||||
func ParseLocalAddress(localAddrs string, filterIpType string) []string {
|
||||
allLocalAddress, _ := nets.GetLocalNetInterfaceAddress()
|
||||
localAddrNames := strings.Split(localAddrs, ",")
|
||||
ips := []string{}
|
||||
for _, addr := range localAddrNames {
|
||||
if addr == "" {
|
||||
continue
|
||||
}
|
||||
if net.ParseIP(addr) == nil {
|
||||
// maybe local interface name
|
||||
if localAddr := allLocalAddress.GetByName(addr); localAddr != nil {
|
||||
if localAddr.IPv4 != "" {
|
||||
if filterIpType == "any" || filterIpType == "" { // 不限制
|
||||
ips = append(ips, localAddr.IPv4)
|
||||
} else {
|
||||
if filterIpType == "ipv4" {
|
||||
ips = append(ips, localAddr.IPv4)
|
||||
}
|
||||
}
|
||||
}
|
||||
if localAddr.IPv6 != "" {
|
||||
if filterIpType == "any" || filterIpType == "" { // 不限制
|
||||
ips = append(ips, localAddr.IPv6)
|
||||
} else {
|
||||
if filterIpType == "ipv6" {
|
||||
ips = append(ips, localAddr.IPv6)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ip
|
||||
ips = append(ips, addr)
|
||||
}
|
||||
}
|
||||
return ips
|
||||
}
|
||||