├── .gitignore ├── LICENSE ├── README.md ├── assets ├── android_show_how │ ├── 10_ln.png │ ├── 11_show.png │ ├── 1_start_1.png │ ├── 2_start_2.png │ ├── 3_start_3.png │ ├── 4_apt_install.png │ ├── 5_apt_install_complete.png │ ├── 6_curl.png │ ├── 7_unzip.png │ ├── 8_ls_cd.png │ └── 9_exec.png └── test │ └── test_4g.png ├── baidupcs ├── baidupcs.go ├── download.go ├── error.go ├── file_directory.go ├── quota.go ├── rm_mkdir.go └── util.go ├── build.sh ├── command ├── cd.go ├── command.go ├── download.go ├── ls.go ├── meta.go ├── quota.go ├── rm_mkdir.go └── util.go ├── config ├── check_bduss.go ├── config.go ├── deleter.go ├── get_checker.go └── seter.go ├── downloader ├── downloader.go ├── downloader_block.go ├── downloader_config.go ├── example.go ├── fetch.go ├── http_client.go └── resume_breakpoint.go ├── main.go └── util ├── convert.go ├── error.go ├── log_colorable_prefix.go ├── regexp_pre.go ├── tieba_client_signature.go ├── time.go ├── unsafe_strconv_test.go ├── util.go ├── wait_group.go └── wait_group_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | baidupcs_go 7 | BaiduPCS-Go 8 | httpserver 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | out/ 16 | *.json 17 | *.dl 18 | 19 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 20 | .glide/ 21 | 22 | # others 23 | .Ds_Store 24 | *.txt 25 | download/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BaiduPCS-Go 百度网盘工具箱 beta-v2 2 | This project was largely inspired by [GangZhuo/BaiduPCS](https://github.com/GangZhuo/BaiduPCS) 3 | 4 | # 特色 5 | 6 | 使用百度 BDUSS 登录百度网盘, 支持多用户, [关于 获取百度 BDUSS](https://github.com/iikira/BaiduPCS-Go/wiki/关于-获取百度-BDUSS); 7 | 8 | 网盘内列出文件和目录, **支持通配符匹配路径**, [通配符_百度百科](https://baike.baidu.com/item/通配符); 9 | 10 | 下载网盘内文件, 支持网盘内目录 (文件夹) 下载, 支持多个文件或目录下载, 支持断点续传和高并发**高速**下载; 11 | 12 | > 下载测试: 13 | 14 | > 服务器: 阿里云 15 | 16 | > 下载 4G 文件, 只需 7分29秒 17 | 18 | > ![test_4g](./assets/test/test_4g.png) 19 | 20 | > 自己感受一下吧 21 | 22 | # 程序 编译/交叉编译 说明 23 | 参见 [交叉编译帮助](https://github.com/iikira/BaiduPCS-Go/wiki/交叉编译帮助) 24 | 25 | # 程序 下载/运行 说明 26 | 27 | Go语言程序, 可直接下载使用, [点此查看发布页面 / 下载汇总](https://github.com/iikira/BaiduPCS-Go/releases). 28 | 29 | 如果程序运行时输出乱码, 请检查下终端的编码方式是否为 `UTF-8`. 30 | 31 | 使用本程序, 强烈建议学习一些 linux 基础知识, [Linux20个常用命令](http://blog.csdn.net/xufei512/article/details/53321980). 32 | 33 | 如果未带任何参数运行程序, 程序将会进入独有的 console 模式, 可直接运行相关命令. 34 | 35 | console 模式下, 光标所在行的前缀应为 `BaiduPCS-Go >` 36 | 37 | 程序会提供相关命令的使用说明. 38 | 39 | ## Windows 40 | 41 | 程序应在 命令提示符 (Command Prompt) 或 PowerShell 中运行. 42 | 43 | 也可直接双击程序运行, 具体使用方法请参见 [命令列表及说明](#命令列表及说明) 和 [例子](#举一些例子). 44 | 45 | ## Linux / macOS 46 | 47 | 程序应在 终端 (Terminal) 运行. 48 | 49 | 具体使用方法请参见 [命令列表及说明](#命令列表及说明) 和 [例子](#举一些例子). 50 | 51 | ## Android / iOS 52 | 53 | 安卓, 建议使用软件 [Termux](https://termux.com) 或 [NeoTerm](https://github.com/NeoTerm/NeoTerm/releases) 或 终端模拟器, 以提供终端环境. 54 | 55 | 示例: [Android 运行本 BaiduPCS-Go 程序参考示例](https://github.com/iikira/BaiduPCS-Go/wiki/Android-运行本-BaiduPCS-Go-程序参考示例), 有兴趣的可以参考一下. 56 | 57 | 苹果iOS, 需要越狱, 在 Cydia 搜索下载并安装 MobileTerminal, 以提供终端环境. MobileTerminal 功能有限, 本人建议 设备安装 openssh 后使用 ssh 控制苹果设备, sftp 传输文件. 58 | 59 | 具体使用方法请参见 [命令列表及说明](#命令列表及说明) 和 [例子](#举一些例子). 60 | 61 | # 命令列表及说明 62 | 63 | ## 注意 64 | 65 | 命令的前缀 `BaiduPCS-Go` 为指向程序运行的全路径名 (ARGv 的第一个参数) 66 | 67 | 未带任何其他参数运行程序, 则程序进入 console 模式, 前缀为 `BaiduPCS-Go >`, 则运行以下命令时, 要把命令的前缀 `BaiduPCS-Go` 去掉! 68 | 69 | ## 使用百度 BDUSS 来登录百度帐号 70 | ``` 71 | BaiduPCS-Go login -bduss= 72 | ``` 73 | ``` 74 | BaiduPCS-Go login 75 | 76 | 请输入百度BDUSS值, 回车键提交 > 77 | ``` 78 | 79 | ## 获取当前帐号, 和所有已登录的百度帐号 80 | ``` 81 | BaiduPCS-Go loglist 82 | ``` 83 | 84 | ## 切换已登录的百度帐号 85 | ``` 86 | BaiduPCS-Go chuser -uid=12345678 87 | ``` 88 | ``` 89 | BaiduPCS-Go chuser 90 | 91 | 请输入要切换帐号的 index 值 > 92 | ``` 93 | 94 | ## 退出已登录的百度帐号 95 | ``` 96 | BaiduPCS-Go logout -uid=12345678 97 | ``` 98 | ``` 99 | BaiduPCS-Go logout 100 | 101 | 请输入要退出帐号的 index 值 > 102 | ``` 103 | 104 | ## 获取配额, 即获取网盘总空间, 和已使用空间 105 | ``` 106 | BaiduPCS-Go quota 107 | ``` 108 | 109 | ## 切换工作目录 110 | ``` 111 | BaiduPCS-Go cd <目录> 112 | ``` 113 | 114 | ## 输出当前所在目录 115 | ``` 116 | BaiduPCS-Go pwd 117 | ``` 118 | 119 | ## 列出当前工作目录的文件和目录或指定目录 120 | ``` 121 | BaiduPCS-Go ls 122 | ``` 123 | ``` 124 | BaiduPCS-Go ls <目录> 125 | ``` 126 | 127 | ## 获取单个文件/目录的元信息 (详细信息) 128 | ``` 129 | BaiduPCS-Go meta <文件/目录> 130 | ``` 131 | ``` 132 | # 默认获取根目录元信息 133 | BaiduPCS-Go meta 134 | ``` 135 | 136 | ## 下载文件, 网盘文件或目录的绝对路径或相对路径 137 | ``` 138 | BaiduPCS-Go download <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ... 139 | BaiduPCS-Go d <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ... 140 | ``` 141 | 142 | 已支持多个文件或目录的下载. 143 | 144 | 下载的文件将会保存到, **程序所在目录**的 download/ 目录 (文件夹), 暂不支持指定目录, 重名的文件会自动跳过! 145 | 146 | ## 创建目录 147 | ``` 148 | BaiduPCS-Go mkdir <目录> 149 | ``` 150 | 151 | ## 删除 单个/多个 文件/目录 152 | ``` 153 | BaiduPCS-Go rm <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ... 154 | ``` 155 | 156 | 注意: 删除多个文件和目录时, 请确保每一个文件和目录都存在, 否则删除操作会失败. 157 | 158 | # 举一些例子 159 | 160 | 新手建议: **双击运行程序**, 进入 console 模式; 161 | 162 | console 模式下, 光标所在行的前缀应为 `BaiduPCS-Go >` 163 | 164 | 以下例子的命令, 均为 console 模式下的命令 165 | 166 | 运行命令的正确操作: **输入命令, 按一下回车键 (键盘上的 Enter 键)**, 程序会接收到命令并输出结果 167 | 168 | ## 1. 查看程序使用说明 169 | 170 | console 模式下, 运行命令 `help` 171 | 172 | ## 2. 使用百度 BDUSS 来登录百度帐号 (必做) 173 | 174 | [关于 获取百度 BDUSS](https://github.com/iikira/BaiduPCS-Go/wiki/关于-获取百度-BDUSS) 175 | 176 | console 模式下, 运行命令 `login -h` (注意空格) 查看帮助 177 | 178 | console 模式下, 运行命令 `login` 程序将会提示你输入 百度BDUSS 值 179 | 180 | console 模式下, 运行命令 `login -bduss=` 来设置 百度BDUSS 值, 设置时请替换掉 `` 181 | 182 | ## 3. 切换网盘工作目录 183 | 184 | console 模式下, 运行命令 `cd /我的资源` 将工作目录切换为 `/我的资源` (前提: 该目录存在于网盘) 185 | 186 | 目录支持通配符匹配, 所以你也可以这样: 运行命令 `cd /我的*` 或 `cd /我的??` 将工作目录切换为 `/我的资源`, 简化输入. 187 | 188 | 将工作目录切换为 `/我的资源` 成功后, 运行命令 `cd ..` 切换上级目录, 即将工作目录切换为 `/` 189 | 190 | 为什么要这样设计呢, 举个例子, 191 | 192 | 假设 你要下载 `/我的资源` 内名为 `1.mp4` 和 `2.mp4` 两个文件, 而未切换工作目录, 你需要依次运行以下命令: 193 | 194 | ``` 195 | d /我的资源/1.mp4 196 | d /我的资源/2.mp4 197 | ``` 198 | 199 | 而切换网盘工作目录之后, 依次运行以下命令: 200 | 201 | ``` 202 | cd /我的资源 203 | d 1.mp4 204 | d 2.mp4 205 | ``` 206 | 207 | 这样就达到了简化输入的目的 208 | 209 | ## 4. 网盘内列出文件和目录 210 | 211 | console 模式下, 运行命令 `ls -h` (注意空格) 查看帮助 212 | 213 | console 模式下, 运行命令 `ls` 来列出当前所在目录的文件和目录 214 | 215 | console 模式下, 运行命令 `ls /我的资源` 来列出 `/我的资源` 内的文件和目录 216 | 217 | console 模式下, 运行命令 `ls ..` 来列出当前所在目录的上级目录的文件和目录 218 | 219 | ## 5. 下载文件 220 | 221 | 说明: 下载的文件将会保存到 download/ 目录 (文件夹) 222 | 223 | console 模式下, 运行命令 `d -h` (注意空格) 查看帮助 224 | 225 | console 模式下, 运行命令 `d /我的资源/1.mp4` 来下载位于 `/我的资源/1.mp4` 的文件 `1.mp4` , 该操作等效于运行以下命令: 226 | 227 | ``` 228 | cd /我的资源 229 | d 1.mp4 230 | ``` 231 | 232 | 现在已经支持目录 (文件夹) 下载, 所以, 运行以下命令, 会下载 `/我的资源` 内的所有文件 (违规文件除外): 233 | 234 | ``` 235 | d /我的资源 236 | ``` 237 | 238 | 参见 例6 设置下载最大并发数 239 | 240 | ## 6. 设置下载最大并发数 241 | 242 | console 模式下, 运行命令 `set -h` (注意空格) 查看设置帮助以及可供设置的值 243 | 244 | console 模式下, 运行命令 `set max_parallel 250` 将下载最大并发数设置为 250 245 | 246 | 下载最大并发数建议值: 50~500, 太低下载速度提升不明显甚至速度会变为0, 太高可能会导致程序和系统超负荷 247 | 248 | ## 7. 退出程序 249 | 250 | 运行命令 `quit` 或 `exit` 或 组合键 `Ctrl+C` 或 组合键 `Ctrl+D` 251 | 252 | # 已知问题 253 | 下载进度到最后的时候, 下载速度会大幅降低. 254 | 255 | # TODO 256 | 257 | 1. 网盘内文件或目录的复制, 移动, 删除等操作 258 | 2. 上传文件 -------------------------------------------------------------------------------- /assets/android_show_how/10_ln.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Angey40/BaiduPCS-Go/a2fe341cb83849e2fb2d64bf3576a72652787ddf/assets/android_show_how/10_ln.png -------------------------------------------------------------------------------- /assets/android_show_how/11_show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Angey40/BaiduPCS-Go/a2fe341cb83849e2fb2d64bf3576a72652787ddf/assets/android_show_how/11_show.png -------------------------------------------------------------------------------- /assets/android_show_how/1_start_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Angey40/BaiduPCS-Go/a2fe341cb83849e2fb2d64bf3576a72652787ddf/assets/android_show_how/1_start_1.png -------------------------------------------------------------------------------- /assets/android_show_how/2_start_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Angey40/BaiduPCS-Go/a2fe341cb83849e2fb2d64bf3576a72652787ddf/assets/android_show_how/2_start_2.png -------------------------------------------------------------------------------- /assets/android_show_how/3_start_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Angey40/BaiduPCS-Go/a2fe341cb83849e2fb2d64bf3576a72652787ddf/assets/android_show_how/3_start_3.png -------------------------------------------------------------------------------- /assets/android_show_how/4_apt_install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Angey40/BaiduPCS-Go/a2fe341cb83849e2fb2d64bf3576a72652787ddf/assets/android_show_how/4_apt_install.png -------------------------------------------------------------------------------- /assets/android_show_how/5_apt_install_complete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Angey40/BaiduPCS-Go/a2fe341cb83849e2fb2d64bf3576a72652787ddf/assets/android_show_how/5_apt_install_complete.png -------------------------------------------------------------------------------- /assets/android_show_how/6_curl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Angey40/BaiduPCS-Go/a2fe341cb83849e2fb2d64bf3576a72652787ddf/assets/android_show_how/6_curl.png -------------------------------------------------------------------------------- /assets/android_show_how/7_unzip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Angey40/BaiduPCS-Go/a2fe341cb83849e2fb2d64bf3576a72652787ddf/assets/android_show_how/7_unzip.png -------------------------------------------------------------------------------- /assets/android_show_how/8_ls_cd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Angey40/BaiduPCS-Go/a2fe341cb83849e2fb2d64bf3576a72652787ddf/assets/android_show_how/8_ls_cd.png -------------------------------------------------------------------------------- /assets/android_show_how/9_exec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Angey40/BaiduPCS-Go/a2fe341cb83849e2fb2d64bf3576a72652787ddf/assets/android_show_how/9_exec.png -------------------------------------------------------------------------------- /assets/test/test_4g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Angey40/BaiduPCS-Go/a2fe341cb83849e2fb2d64bf3576a72652787ddf/assets/test/test_4g.png -------------------------------------------------------------------------------- /baidupcs/baidupcs.go: -------------------------------------------------------------------------------- 1 | package baidupcs 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | var ( 9 | appid = 260149 10 | ) 11 | 12 | // PCSApi 百度 PCS API 详情 13 | type PCSApi struct { 14 | url url.URL 15 | bduss string 16 | 17 | writed bool 18 | } 19 | 20 | // NewPCS 提供 百度BDUSS, 返回 PCSApi 指针对象 21 | func NewPCS(bduss string) *PCSApi { 22 | return &PCSApi{ 23 | url: url.URL{ 24 | Scheme: "http", 25 | Host: "pcs.baidu.com", 26 | Path: "/rest/2.0/pcs/", 27 | RawQuery: fmt.Sprintf("app_id=%d", appid)}, 28 | bduss: bduss, 29 | writed: false, 30 | } 31 | } 32 | 33 | func (p *PCSApi) addItem(subPath, method string, param ...map[string]string) { 34 | if p.writed { 35 | panic("addItem: Already writed") 36 | } 37 | p.url.Path += subPath 38 | uv := p.url.Query() 39 | uv.Set("method", method) 40 | for k := range param { 41 | for k2 := range param[k] { 42 | uv.Set(k2, param[k][k2]) 43 | } 44 | } 45 | p.url.RawQuery = uv.Encode() 46 | p.writed = true 47 | } 48 | -------------------------------------------------------------------------------- /baidupcs/download.go: -------------------------------------------------------------------------------- 1 | package baidupcs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/iikira/BaiduPCS-Go/config" 6 | "github.com/iikira/BaiduPCS-Go/downloader" 7 | "github.com/iikira/BaiduPCS-Go/util" 8 | "net/http" 9 | "net/http/cookiejar" 10 | "os" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | // FileDownload 下载网盘内文件 16 | func (p PCSApi) FileDownload(path string, size int64) (err error) { 17 | // addItem 放在最后 18 | p.addItem("file", "download", map[string]string{ 19 | "path": path, 20 | }) 21 | 22 | h := downloader.NewHTTPClient() 23 | jar, _ := cookiejar.New(nil) 24 | jar.SetCookies(&p.url, []*http.Cookie{ 25 | &http.Cookie{ 26 | Name: "BDUSS", 27 | Value: p.bduss, 28 | }, 29 | }) 30 | h.SetCookiejar(jar) 31 | h.SetKeepAlive(true) 32 | h.SetTimeout(2 * time.Minute) 33 | 34 | fileDl, err := downloader.NewFileDl(h, p.url.String(), pcsconfig.GetSavePath(path), size) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | pa := make(chan struct{}) 40 | 41 | var exit = make(chan bool) 42 | 43 | fileDl.OnStart(func() { 44 | t1 := time.Now() 45 | for_1: 46 | for { 47 | status := fileDl.GetStatus() 48 | 49 | select { 50 | case <-exit: 51 | break for_1 52 | default: 53 | time.Sleep(time.Second * 1) 54 | fmt.Printf("\r%v/%v %v/s time: %s %v", 55 | pcsutil.ConvertFileSize(status.Downloaded, 2), 56 | pcsutil.ConvertFileSize(fileDl.Size, 2), 57 | pcsutil.ConvertFileSize(status.Speeds, 2), 58 | time.Since(t1)/1000000*1000000, 59 | "[DOWNLOADING]"+strings.Repeat(" ", 10), 60 | ) 61 | os.Stdout.Sync() 62 | } 63 | } 64 | }) 65 | 66 | fileDl.OnFinish(func() { 67 | exit <- true 68 | pa <- struct{}{} 69 | }) 70 | 71 | fileDl.Start() 72 | <-pa 73 | fmt.Printf("\n\n下载完成, 保存位置: %s\n\n", pcsconfig.GetSavePath(path)) 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /baidupcs/error.go: -------------------------------------------------------------------------------- 1 | package baidupcs 2 | -------------------------------------------------------------------------------- /baidupcs/file_directory.go: -------------------------------------------------------------------------------- 1 | package baidupcs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/bitly/go-simplejson" 6 | "github.com/iikira/BaiduPCS-Go/downloader" 7 | "github.com/iikira/BaiduPCS-Go/util" 8 | ) 9 | 10 | // FileDirectory 文件或目录的详细信息 11 | type FileDirectory struct { 12 | FsID int64 13 | Path string 14 | Filename string 15 | Ctime int64 16 | MD5 string 17 | Size int64 18 | Isdir bool 19 | Ifhassubdir bool 20 | } 21 | 22 | // FileDirectoryList FileDirectory 的 数组 23 | type FileDirectoryList []FileDirectory 24 | 25 | // FilesDirectoriesMeta 获取单个文件/目录的元信息 26 | // 27 | // 可用信息: 是否是目录isdir 是否含有子目录ifhassubdir 修改时间mtime 文件大小size 28 | func (p PCSApi) FilesDirectoriesMeta(path string) (data FileDirectory, err error) { 29 | if path == "" { 30 | path = "/" 31 | } 32 | 33 | p.addItem("file", "meta", map[string]string{ 34 | "path": path, 35 | }) 36 | 37 | h := downloader.NewHTTPClient() 38 | body, err := h.Fetch("GET", p.url.String(), nil, map[string]string{ 39 | "Cookie": "BDUSS=" + p.bduss, 40 | }) 41 | if err != nil { 42 | return 43 | } 44 | 45 | json, err := simplejson.NewJson(body) 46 | 47 | code, err := checkErr(json) 48 | if err != nil { 49 | err = fmt.Errorf("获取单个文件/目录的元信息遇到错误, 路径: %s, 错误代码: %d, 消息: %s", path, code, err) 50 | return 51 | } 52 | 53 | json = json.Get("list").GetIndex(0) 54 | 55 | data = FileDirectory{ 56 | FsID: json.Get("fs_id").MustInt64(), 57 | Path: json.Get("path").MustString(), 58 | Filename: json.Get("server_filename").MustString(), 59 | Ctime: json.Get("ctime").MustInt64(), 60 | MD5: json.Get("md5").MustString(), 61 | Size: json.Get("size").MustInt64(), 62 | Isdir: pcsutil.IntToBool(json.Get("isdir").MustInt()), 63 | Ifhassubdir: pcsutil.IntToBool(json.Get("ifhassubdir").MustInt()), 64 | } 65 | 66 | return 67 | } 68 | 69 | // FileList 获取目录下的文件列表 70 | func (p PCSApi) FileList(path string) (data FileDirectoryList, err error) { 71 | if path == "" { 72 | path = "/" 73 | } 74 | 75 | p.addItem("file", "list", map[string]string{ 76 | "path": path, 77 | "by": "name", 78 | "order": "asc", 79 | "limit": "0-2147483647", 80 | }) 81 | 82 | h := downloader.NewHTTPClient() 83 | body, err := h.Fetch("GET", p.url.String(), nil, map[string]string{ 84 | "Cookie": "BDUSS=" + p.bduss, 85 | }) 86 | if err != nil { 87 | return 88 | } 89 | 90 | json, err := simplejson.NewJson(body) 91 | if err != nil { 92 | return 93 | } 94 | 95 | code, err := checkErr(json) 96 | if err != nil { 97 | return nil, fmt.Errorf("获取目录下的文件列表遇到错误, 路径: %s, 错误代码: %d, 消息: %s", path, code, err) 98 | } 99 | 100 | json = json.Get("list") 101 | 102 | for i := 0; ; i++ { 103 | index := json.GetIndex(i) 104 | fsID := index.Get("fs_id").MustInt64() 105 | if fsID == 0 { 106 | break 107 | } 108 | data = append(data, FileDirectory{ 109 | FsID: fsID, 110 | Path: index.Get("path").MustString(), 111 | Filename: index.Get("server_filename").MustString(), 112 | Ctime: index.Get("ctime").MustInt64(), 113 | MD5: index.Get("md5").MustString(), 114 | Size: index.Get("size").MustInt64(), 115 | Isdir: pcsutil.IntToBool(index.Get("isdir").MustInt()), 116 | }) 117 | } 118 | return 119 | } 120 | 121 | func (f FileDirectory) String() string { 122 | if f.Isdir { 123 | return fmt.Sprintf("类型: 目录 \n目录名称: %s \n目录路径: %s \nfs_id: %d \n创建时间: %s \n是否含有子目录: %t\n", 124 | f.Filename, 125 | f.Path, 126 | f.FsID, 127 | pcsutil.FormatTime(f.Ctime), 128 | f.Ifhassubdir, 129 | ) 130 | } 131 | 132 | return fmt.Sprintf("类型: 文件 \n文件名: %s \n文件路径: %s \n文件大小: %d \nmd5: %s \nfs_id: %d \n创建时间: %s \n", 133 | f.Filename, 134 | f.Path, 135 | f.Size, 136 | f.MD5, 137 | f.FsID, 138 | pcsutil.FormatTime(f.Ctime), 139 | ) 140 | } 141 | 142 | // TotalSize 获取总文件大小 143 | func (f *FileDirectoryList) TotalSize() int64 { 144 | var size int64 145 | for k := range *f { 146 | size += (*f)[k].Size 147 | } 148 | return size 149 | } 150 | 151 | // Count 获取文件总数和目录总数 152 | func (f *FileDirectoryList) Count() (fileN, directoryN int64) { 153 | for k := range *f { 154 | if (*f)[k].Isdir { 155 | directoryN++ 156 | } else { 157 | fileN++ 158 | } 159 | } 160 | return 161 | } 162 | -------------------------------------------------------------------------------- /baidupcs/quota.go: -------------------------------------------------------------------------------- 1 | package baidupcs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/bitly/go-simplejson" 6 | "github.com/iikira/BaiduPCS-Go/downloader" 7 | ) 8 | 9 | // QuotaInfo 获取当前用户空间配额信息 10 | func (p PCSApi) QuotaInfo() (quota, used int64, err error) { 11 | p.addItem("quota", "info") 12 | 13 | h := downloader.NewHTTPClient() 14 | body, err := h.Fetch("GET", p.url.String(), nil, map[string]string{ 15 | "Cookie": "BDUSS=" + p.bduss, 16 | }) 17 | if err != nil { 18 | return 19 | } 20 | 21 | json, err := simplejson.NewJson(body) 22 | if err != nil { 23 | return 24 | } 25 | 26 | code, err := checkErr(json) 27 | if err != nil { 28 | return 0, 0, fmt.Errorf("获取当前用户空间配额信息, 错误代码: %d, 消息: %s", code, err) 29 | } 30 | 31 | quota = json.Get("quota").MustInt64() 32 | used = json.Get("used").MustInt64() 33 | 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /baidupcs/rm_mkdir.go: -------------------------------------------------------------------------------- 1 | package baidupcs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/bitly/go-simplejson" 7 | "github.com/iikira/BaiduPCS-Go/downloader" 8 | ) 9 | 10 | // Remove 批量删除文件/目录 11 | func (p PCSApi) Remove(paths ...string) (err error) { 12 | pathsData := struct { 13 | List []struct { 14 | Path string `json:"path"` 15 | } `json:"list"` 16 | }{} 17 | 18 | for k := range paths { 19 | pathsData.List = append(pathsData.List, struct { 20 | Path string `json:"path"` 21 | }{ 22 | Path: paths[k], 23 | }) 24 | } 25 | 26 | ej, err := json.Marshal(&pathsData) 27 | if err != nil { 28 | return 29 | } 30 | 31 | p.addItem("file", "delete", map[string]string{ 32 | "param": string(ej[:]), 33 | }) 34 | 35 | h := downloader.NewHTTPClient() 36 | body, err := h.Fetch("POST", p.url.String(), nil, map[string]string{ 37 | "Cookie": "BDUSS=" + p.bduss, 38 | }) 39 | if err != nil { 40 | return 41 | } 42 | 43 | json, err := simplejson.NewJson(body) 44 | if err != nil { 45 | return 46 | } 47 | 48 | code, err := checkErr(json) 49 | if err != nil { 50 | return fmt.Errorf("批量删除文件/目录 遇到错误, 错误代码: %d, 消息: %s", code, err) 51 | } 52 | 53 | return nil 54 | } 55 | 56 | // Mkdir 创建目录 57 | func (p PCSApi) Mkdir(path string) (err error) { 58 | p.addItem("file", "mkdir", map[string]string{ 59 | "path": path, 60 | }) 61 | 62 | h := downloader.NewHTTPClient() 63 | body, err := h.Fetch("POST", p.url.String(), nil, map[string]string{ 64 | "Cookie": "BDUSS=" + p.bduss, 65 | }) 66 | if err != nil { 67 | return 68 | } 69 | 70 | json, err := simplejson.NewJson(body) 71 | if err != nil { 72 | return 73 | } 74 | 75 | code, err := checkErr(json) 76 | if err != nil { 77 | return fmt.Errorf("创建目录 遇到错误, 错误代码: %d, 消息: %s", code, err) 78 | } 79 | return 80 | } 81 | -------------------------------------------------------------------------------- /baidupcs/util.go: -------------------------------------------------------------------------------- 1 | package baidupcs 2 | 3 | import ( 4 | "errors" 5 | "github.com/bitly/go-simplejson" 6 | ) 7 | 8 | func checkErr(json *simplejson.Json) (code int, msg error) { 9 | codeJSON, ok1 := json.CheckGet("error_code") 10 | msgJSON, ok2 := json.CheckGet("error_msg") 11 | if !ok1 && !ok2 { // 没有错误 12 | return 0, nil 13 | } 14 | 15 | errCode := codeJSON.MustInt() 16 | errMsg := msgJSON.MustString() 17 | switch errCode { 18 | case 31045: // user not exists 19 | errMsg = "操作失败, 可能BDUSS已过期, 请尝试运行 login 命令重新登录, 消息: " + errMsg 20 | } 21 | return errCode, errors.New(errMsg) 22 | } 23 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | name="BaiduPCS-Go" 2 | version="beta-v2" 3 | 4 | Build(){ 5 | echo "Building $1..." 6 | export GOOS=$2 GOARCH=$3 GO386=sse2 CGO_ENABLED=0 7 | if [ $2 = "windows" ];then 8 | go build -ldflags "-s -w" -o "out/$1/$name.exe" 9 | else 10 | go build -ldflags "-s -w" -o "out/$1/$name" 11 | fi 12 | mkdir "out/$1/download" 13 | cd out 14 | zip -q -r "$1.zip" "$1" 15 | cd .. 16 | echo Done! 17 | } 18 | 19 | ArmBuild(){ 20 | echo "Building $1..." 21 | export GOOS=$2 GOARCH=$3 GOARM=$4 CGO_ENABLED=1 22 | go build -ldflags '-s -w -linkmode=external -extldflags=-pie' -o "out/$1/$name" 23 | if [ $2 = "darwin" -a $3 = "arm64" ];then 24 | ldid -S "out/$1/$name" 25 | fi 26 | mkdir "out/$1/download" 27 | cd out 28 | zip -q -r "$1.zip" "$1" 29 | cd .. 30 | echo Done! 31 | } 32 | 33 | # android 34 | export NDK_INSTALL=$ANDROID_NDK_ROOT/bin 35 | # CC=$NDK_INSTALL/arm-linux-androideabi/bin/arm-linux-androideabi-gcc ArmBuild $name-$version"-android-16-armv5" android arm 5 36 | # CC=$NDK_INSTALL/arm-linux-androideabi/bin/arm-linux-androideabi-gcc ArmBuild $name-$version"-android-16-armv6" android arm 6 37 | CC=$NDK_INSTALL/arm-linux-androideabi/bin/arm-linux-androideabi-gcc ArmBuild $name-$version"-android-16-armv7" android arm 7 38 | CC=$NDK_INSTALL/aarch64-linux-android/bin/aarch64-linux-android-gcc ArmBuild $name-$version"-android-21-arm64" android arm64 7 39 | CC=$NDK_INSTALL/i686-linux-android/bin/i686-linux-android-gcc ArmBuild $name-$version"-android-16-386" android 386 7 40 | CC=$NDK_INSTALL/x86_64-linux-android/bin/x86_64-linux-android-gcc ArmBuild $name-$version"-android-21-amd64" android amd64 7 41 | 42 | # ios 43 | CC=/usr/local/go/misc/ios/clangwrap.sh ArmBuild $name-$version"-darwin-ios-5.0-armv7" darwin arm 7 44 | CC=/usr/local/go/misc/ios/clangwrap.sh ArmBuild $name-$version"-darwin-ios-5.0-arm64" darwin arm64 7 45 | 46 | # OS X / macOS 47 | Build $name-$version"-darwin-osx-amd64" darwin amd64 48 | # Build $name-$version"-darwin-osx-386" darwin 386 49 | 50 | # Windows 51 | Build $name-$version"-windows-x86" windows 386 52 | Build $name-$version"-windows-x64" windows amd64 53 | 54 | # Linux 55 | Build $name-$version"-linux-386" linux 386 56 | Build $name-$version"-linux-amd64" linux amd64 57 | Build $name-$version"-linux-arm" linux arm 58 | Build $name-$version"-linux-arm64" linux arm64 59 | # Build $name-$version"-linux-mips" linux mips 60 | # Build $name-$version"-linux-mips64" linux mips64 61 | # Build $name-$version"-linux-mipsel" linux mipsle 62 | # Build $name-$version"-linux-mips64el" linux mips64le 63 | # Build $name-$version"-linux-ppc64" linux ppc64 64 | # Build $name-$version"-linux-ppc64le" linux ppc64le 65 | # Build $name-$version"-linux-s390x" linux s390x 66 | 67 | # other 68 | # $name-$version 69 | # Build $name-$version"-solaris-amd64" solaris amd64 70 | Build $name-$version"-freebsd-386" freebsd 386 71 | # Build $name-$version"-freebsd-amd64" freebsd amd64 72 | # Build $name-$version"-freebsd-arm" freebsd arm 73 | # Build $name-$version"-netbsd-386" netbsd 386 74 | # Build $name-$version"-netbsd-amd64" netbsd amd64 75 | # Build $name-$version"-netbsd-arm" netbsd arm 76 | # Build $name-$version"-openbsd-386" openbsd 386 77 | # Build $name-$version"-openbsd-amd64" openbsd amd64 78 | # Build $name-$version"-openbsd-arm" openbsd arm 79 | # Build $name-$version"-plan9-386" plan9 386 80 | # Build $name-$version"-plan9-amd64" plan9 amd64 81 | # Build $name-$version"-plan9-arm" plan9 arm 82 | # Build $name-$version"-nacl-386" nacl 386 83 | # Build $name-$version"-nacl-amd64p32" nacl amd64p32 84 | # Build $name-$version"-nacl-arm" nacl arm 85 | # Build $name-$version"-dragonflybsd-amd64" dragonfly amd64 86 | -------------------------------------------------------------------------------- /command/cd.go: -------------------------------------------------------------------------------- 1 | package baidupcscmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/iikira/BaiduPCS-Go/config" 6 | ) 7 | 8 | // RunChangeDirectory 执行更改工作目录 9 | func RunChangeDirectory(path string) { 10 | path, err := toAbsPath(path) 11 | if err != nil { 12 | fmt.Println(err) 13 | return 14 | } 15 | 16 | data, err := info.FilesDirectoriesMeta(path) 17 | if err != nil { 18 | fmt.Println(err) 19 | return 20 | } 21 | 22 | if !data.Isdir { 23 | fmt.Printf("错误: %s 不是一个目录 (文件夹)\n", path) 24 | return 25 | } 26 | 27 | pcsconfig.ActiveBaiduUser.Workdir = path 28 | pcsconfig.Config.Save() 29 | 30 | fmt.Printf("改变工作目录: %s\n", path) 31 | } 32 | -------------------------------------------------------------------------------- /command/command.go: -------------------------------------------------------------------------------- 1 | package baidupcscmd 2 | 3 | import ( 4 | "github.com/iikira/BaiduPCS-Go/baidupcs" 5 | "github.com/iikira/BaiduPCS-Go/config" 6 | "os" 7 | ) 8 | 9 | var ( 10 | info = new(baidupcs.PCSApi) 11 | ) 12 | 13 | func init() { 14 | ReloadInfo() 15 | } 16 | 17 | // ReloadInfo 重载配置 18 | func ReloadInfo() { 19 | pcsconfig.Reload() 20 | info = baidupcs.NewPCS(pcsconfig.ActiveBaiduUser.BDUSS) 21 | } 22 | 23 | // ReloadIfInConsole 程序在 Console 模式下才回重载配置 24 | func ReloadIfInConsole() { 25 | if len(os.Args) == 1 { 26 | ReloadInfo() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /command/download.go: -------------------------------------------------------------------------------- 1 | package baidupcscmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/iikira/BaiduPCS-Go/config" 6 | "github.com/iikira/BaiduPCS-Go/downloader" 7 | "github.com/iikira/BaiduPCS-Go/util" 8 | "os" 9 | ) 10 | 11 | // RunDownload 执行下载网盘内文件 12 | func RunDownload(paths ...string) { 13 | downloader.SetCacheSize(2048) 14 | downloader.SetMaxParallel(pcsconfig.Config.MaxParallel) 15 | 16 | paths = getAllPaths(paths...) 17 | 18 | fmt.Println() 19 | for k := range paths { 20 | fmt.Printf("添加下载任务: %s\n", paths[k]) 21 | } 22 | fmt.Println() 23 | 24 | for k, path := range paths { 25 | downloadInfo, err := info.FilesDirectoriesMeta(path) 26 | if err != nil { 27 | fmt.Println(err) 28 | continue 29 | } 30 | 31 | fmt.Printf("[ %d / %d ] %s\n", k+1, len(paths), downloadInfo.String()) 32 | 33 | // 如果是一个目录, 递归下载该目录下的所有文件 34 | if downloadInfo.Isdir { 35 | fmt.Printf("即将下载目录: %s\n\n", path) 36 | 37 | fileN, directoryN, size := recurseFDCountTotalSize(path) 38 | statText := fmt.Sprintf("统计: 目录总数: %d, 文件总数: %d, 文件总大小: %s\n\n", 39 | directoryN, 40 | fileN, 41 | pcsutil.ConvertFileSize(size), 42 | ) 43 | 44 | fmt.Printf(statText) 45 | downloadDirectory(path) 46 | fmt.Printf("目录 %s 下载完成, %s", path, statText) 47 | continue 48 | } 49 | 50 | fmt.Printf("即将开始下载文件\n\n") 51 | 52 | err = info.FileDownload(path, downloadInfo.Size) 53 | if err != nil { 54 | fmt.Printf("下载文件时发生错误: %s (跳过...)\n\n", err) 55 | } 56 | } 57 | } 58 | 59 | func downloadDirectory(path string) { 60 | di, err := info.FileList(path) 61 | if err != nil { 62 | fmt.Println("发生错误,", err) 63 | } 64 | 65 | // 遇到空目录, 则创建目录 66 | if len(di) == 0 { 67 | os.MkdirAll(pcsconfig.GetSavePath(path), 0777) 68 | return 69 | } 70 | 71 | for k := range di { 72 | if di[k].Isdir { 73 | downloadDirectory(di[k].Path) 74 | continue 75 | } 76 | 77 | // 如果文件存在, 跳过 78 | if pcsconfig.CheckFileExist(di[k].Path) { 79 | fmt.Printf("文件已存在 (自动跳过): %s\n\n", pcsconfig.GetSavePath(di[k].Path)) 80 | continue 81 | } 82 | 83 | fmt.Println(di[k]) 84 | fmt.Printf("即将开始下载文件: %s\n\n", di[k].Filename) 85 | 86 | err = info.FileDownload(di[k].Path, di[k].Size) 87 | if err != nil { 88 | fmt.Println(err) 89 | } 90 | fmt.Println("------------------------------------------------------------") 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /command/ls.go: -------------------------------------------------------------------------------- 1 | package baidupcscmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/iikira/BaiduPCS-Go/util" 6 | "os" 7 | "text/template" 8 | ) 9 | 10 | // RunLs 执行列目录 11 | func RunLs(path string) { 12 | path, err := toAbsPath(path) 13 | if err != nil { 14 | fmt.Println(err) 15 | return 16 | } 17 | files, err := info.FileList(path) 18 | 19 | if err != nil { 20 | fmt.Println(err) 21 | return 22 | } 23 | 24 | if len(files) == 0 { 25 | RunGetMeta(path) 26 | return 27 | } 28 | 29 | for k := range files { 30 | if files[k].Isdir { 31 | files[k].Path += "/" 32 | } 33 | } 34 | 35 | tmpl, err := template.New("ls").Funcs( 36 | template.FuncMap{ 37 | "convertFileSize": func(size int64) string { 38 | res := pcsutil.ConvertFileSize(size) 39 | if res == "0" { 40 | return "- " 41 | } 42 | return res 43 | }, 44 | "timeFmt": pcsutil.FormatTime, 45 | "totalSize": func() string { 46 | return pcsutil.ConvertFileSize(files.TotalSize()) 47 | }, 48 | "fdCount": func() string { 49 | fN, dN := files.Count() 50 | return fmt.Sprintf("文件总数: %d,\t目录总数: %d", fN, dN) 51 | }, 52 | }, 53 | ).Parse( 54 | ` 55 | 文件大小 创建日期 文件(目录) 56 | ------------------------------------------------------------------------------{{range .}} 57 | {{convertFileSize .Size}} {{timeFmt .Ctime}} {{.Path}} {{end}} 58 | ------------------------------------------------------------------------------ 59 | 总大小: {{totalSize}} {{fdCount}} 60 | `) 61 | if err != nil { 62 | panic(err) 63 | } 64 | 65 | err = tmpl.Execute(os.Stdout, files) 66 | if err != nil { 67 | panic(err) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /command/meta.go: -------------------------------------------------------------------------------- 1 | package baidupcscmd 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // RunGetMeta 执行 获取单个文件/目录的元信息 8 | func RunGetMeta(path string) { 9 | p, err := toAbsPath(path) 10 | if err != nil { 11 | fmt.Println(err) 12 | return 13 | } 14 | 15 | data, err := info.FilesDirectoriesMeta(p) 16 | if err != nil { 17 | fmt.Println(err) 18 | return 19 | } 20 | fmt.Println() 21 | fmt.Println(data) 22 | } 23 | -------------------------------------------------------------------------------- /command/quota.go: -------------------------------------------------------------------------------- 1 | package baidupcscmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/iikira/BaiduPCS-Go/util" 6 | ) 7 | 8 | // RunGetQuota 执行 获取当前用户空间配额信息, 并输出 9 | func RunGetQuota() { 10 | quota, used, err := info.QuotaInfo() 11 | if err != nil { 12 | fmt.Println(err) 13 | return 14 | } 15 | fmt.Printf("总空间: %s, 已用空间: %s, 比率: %f%%\n", 16 | pcsutil.ConvertFileSize(quota), 17 | pcsutil.ConvertFileSize(used), 18 | 100*float64(used)/float64(quota), 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /command/rm_mkdir.go: -------------------------------------------------------------------------------- 1 | package baidupcscmd 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // RunRemove 执行 批量删除文件/目录 8 | func RunRemove(paths ...string) { 9 | paths = getAllPaths(paths...) 10 | 11 | pnt := func() { 12 | for k := range paths { 13 | fmt.Printf("%d: %s\n", k+1, paths[k]) 14 | } 15 | } 16 | 17 | err := info.Remove(paths...) 18 | if err != nil { 19 | fmt.Println(err) 20 | fmt.Println("操作失败, 以下文件/目录删除失败: ") 21 | pnt() 22 | return 23 | } 24 | 25 | fmt.Println("操作成功, 以下文件/目录已删除: ") 26 | pnt() 27 | } 28 | 29 | // RunMkdir 执行 创建目录 30 | func RunMkdir(path string) { 31 | path = getAbsPath(path) 32 | 33 | err := info.Mkdir(path) 34 | if err != nil { 35 | fmt.Println(err) 36 | return 37 | } 38 | 39 | fmt.Println("创建目录成功:", path) 40 | } 41 | -------------------------------------------------------------------------------- /command/util.go: -------------------------------------------------------------------------------- 1 | package baidupcscmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/iikira/BaiduPCS-Go/config" 6 | fpath "path" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | patternRE = regexp.MustCompile(`[\[\]\*\?]`) 13 | ) 14 | 15 | // getAbsPath 获取绝对路径, 忽略通配符 16 | func getAbsPath(path string) string { 17 | if !fpath.IsAbs(path) { 18 | path = fpath.Dir(pcsconfig.ActiveBaiduUser.Workdir + "/" + path + "/") 19 | } 20 | return path 21 | } 22 | 23 | func getAllPaths(paths ...string) (_paths []string) { 24 | for k := range paths { 25 | _paths = append(_paths, parsePath(paths[k])...) 26 | } 27 | return 28 | } 29 | 30 | func toAbsPath(path string) (string, error) { 31 | p := parsePath(path) 32 | if len(p) == 0 { 33 | return "", fmt.Errorf("文件路径匹配失败, 请检查通配符") 34 | } 35 | return p[0], nil 36 | } 37 | 38 | // parsePath 递归解析通配符 39 | func parsePath(path string) (paths []string) { 40 | path = getAbsPath(path) 41 | 42 | if !patternRE.MatchString(path) { 43 | paths = []string{path} 44 | return 45 | } 46 | 47 | if _, err := fpath.Match(path, ""); err != nil { 48 | return nil 49 | } 50 | 51 | names := strings.Split(path, "/") 52 | 53 | for k := range names { 54 | if names[k] == "" || !patternRE.MatchString(names[k]) { 55 | continue 56 | } 57 | 58 | pfiles, err := info.FileList(strings.Join(names[:k], "/")) 59 | if err != nil { 60 | fmt.Println(err) 61 | return nil 62 | } 63 | 64 | for k2 := range pfiles { 65 | ok, _ := fpath.Match(names[k], pfiles[k2].Filename) 66 | if ok { 67 | if k >= len(names)-1 { 68 | paths = append(paths, strings.Join(names[:k], "/")+"/"+pfiles[k2].Filename) 69 | } else if pfiles[k2].Isdir { 70 | paths = append(paths, parsePath(pfiles[k2].Path+"/"+strings.Join(names[k+1:], "/"))...) 71 | } 72 | } 73 | } 74 | break 75 | } 76 | 77 | return 78 | } 79 | 80 | func recurseFDCountTotalSize(path string) (fileN, directoryN, size int64) { 81 | di, err := info.FileList(path) 82 | if err != nil { 83 | fmt.Println(err) 84 | } 85 | 86 | for k := range di { 87 | if di[k].Isdir { 88 | f, d, s := recurseFDCountTotalSize(di[k].Path) 89 | fileN += f 90 | directoryN += d 91 | size += s 92 | } 93 | } 94 | f, d := di.Count() 95 | s := di.TotalSize() 96 | fileN += f 97 | directoryN += d 98 | size += s 99 | return 100 | } 101 | -------------------------------------------------------------------------------- /config/check_bduss.go: -------------------------------------------------------------------------------- 1 | package pcsconfig 2 | 3 | import ( 4 | "fmt" 5 | "github.com/bitly/go-simplejson" 6 | "github.com/iikira/BaiduPCS-Go/downloader" 7 | "github.com/iikira/BaiduPCS-Go/util" 8 | "strconv" 9 | ) 10 | 11 | type Baidu struct { 12 | UID uint64 `json:"uid"` 13 | Name string `json:"name"` 14 | BDUSS string `json:"bduss"` 15 | 16 | Workdir string `json:"workdir"` 17 | } 18 | 19 | // NewWithBDUSS 检测BDUSS有效性, 同时获取百度详细信息 20 | func NewWithBDUSS(bduss string) (*Baidu, error) { 21 | h := downloader.NewHTTPClient() 22 | timestamp := pcsutil.BeijingTimeOption("") 23 | post := map[string]string{ 24 | "bdusstoken": bduss + "|null", 25 | "channel_id": "", 26 | "channel_uid": "", 27 | "stErrorNums": "0", 28 | "subapp_type": "mini", 29 | "timestamp": timestamp + "922", 30 | } 31 | pcsutil.TiebaClientSignature(post) 32 | 33 | header := map[string]string{ 34 | "Content-Type": "application/x-www-form-urlencoded", 35 | "Cookie": "ka=open", 36 | "net": "1", 37 | "User-Agent": "bdtb for Android 6.9.2.1", 38 | "client_logid": timestamp + "416", 39 | "Connection": "Keep-Alive", 40 | } 41 | 42 | body, err := h.Fetch("POST", "http://tieba.baidu.com/c/s/login", post, header) // 获取百度ID的TBS,UID,BDUSS等 43 | if err != nil { 44 | return nil, fmt.Errorf("检测BDUSS有效性失败, %s", err) 45 | } 46 | 47 | json, err := simplejson.NewJson(body) 48 | if err != nil { 49 | return nil, fmt.Errorf("检测BDUSS有效性json解析出错: %s", err) 50 | } 51 | 52 | errCode := json.Get("error_code").MustString() 53 | errMsg := json.Get("error_msg").MustString() 54 | 55 | switch errCode { 56 | case "0": 57 | case "1": 58 | return nil, fmt.Errorf("检测BDUSS有效性错误, 百度BDUSS格式不正确或者已过期") 59 | default: 60 | return nil, fmt.Errorf("检测BDUSS有效性错误代码: %s, 消息: %s", errCode, errMsg) 61 | } 62 | 63 | uidStr := json.GetPath("user", "id").MustString() 64 | uid, _ := strconv.ParseUint(uidStr, 10, 64) 65 | 66 | username, err := GetUserNameByUID(uid) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | return &Baidu{ 72 | UID: uid, 73 | Name: username, 74 | BDUSS: bduss, 75 | Workdir: "/", 76 | }, nil 77 | } 78 | 79 | func GetUserNameByUID(uid uint64) (username string, err error) { 80 | rawQuery := "has_plist=0&need_post_count=1&rn=1&uid=" + fmt.Sprint(uid) 81 | urlStr := "http://c.tieba.baidu.com/c/u/user/profile?" + pcsutil.TiebaClientRawQuerySignature(rawQuery) 82 | body, err := downloader.HTTPGet(urlStr) 83 | if err != nil { 84 | return "", err 85 | } 86 | json, err := simplejson.NewJson(body) 87 | if err != nil { 88 | return "", err 89 | } 90 | userJSON := json.GetPath("user") 91 | username = userJSON.Get("name").MustString() 92 | return 93 | } 94 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package pcsconfig 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | ) 8 | 9 | var ( 10 | // Config 配置信息, 由外部调用 11 | Config = new(PCSConfig) 12 | 13 | // ActiveBaiduUser 当前百度帐号 14 | ActiveBaiduUser *Baidu 15 | 16 | configFileName = "pcs_config.json" 17 | 18 | // SaveDir 保存文件的目录 19 | SaveDir = "download/" 20 | ) 21 | 22 | // PCSConfig 配置详情 23 | type PCSConfig struct { 24 | BaiduActiveUID uint64 `json:"baidu_active_uid"` 25 | BaiduUserList []*Baidu `json:"baidu_user_list"` 26 | MaxParallel int `json:"max_parallel"` 27 | } 28 | 29 | // Init 初始化配置 30 | func Init() { 31 | // 检查配置 32 | cfg, err := loadConfig() 33 | if err != nil { 34 | fmt.Printf("错误: %s, 自动初始化配置文件\n", err) 35 | 36 | cfg = &PCSConfig{ 37 | BaiduActiveUID: 0, 38 | MaxParallel: 100, 39 | } 40 | 41 | err = cfg.Save() 42 | if err != nil { 43 | fmt.Println(err) 44 | } 45 | } 46 | Config = cfg 47 | 48 | if UpdateActiveBaiduUser() != nil { 49 | ActiveBaiduUser = new(Baidu) 50 | } 51 | } 52 | 53 | func loadConfig() (*PCSConfig, error) { 54 | data, err := ioutil.ReadFile(configFileName) 55 | if err != nil { 56 | return nil, err 57 | } 58 | conf := new(PCSConfig) 59 | err = json.Unmarshal(data, conf) 60 | if err != nil { 61 | return nil, err 62 | } 63 | return conf, nil 64 | } 65 | 66 | // Reload 从配置文件重载更新 Config 67 | func Reload() error { 68 | cfg, err := loadConfig() 69 | if err != nil { 70 | return err 71 | } 72 | Config = cfg 73 | 74 | // 更新 当前百度帐号 75 | return UpdateActiveBaiduUser() 76 | } 77 | 78 | // Save 保存配置信息到配置文件, 并重载配置 79 | func (c *PCSConfig) Save() error { 80 | data, err := json.MarshalIndent(c, "", "\t") 81 | if err != nil { 82 | return err 83 | } 84 | 85 | err = ioutil.WriteFile(configFileName, data, 0666) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | return Reload() 91 | } 92 | 93 | // UpdateActiveBaiduUser 更新 当前百度帐号 94 | func UpdateActiveBaiduUser() error { 95 | baidu, err := Config.GetBaiduUserByUID(Config.BaiduActiveUID) 96 | if err == nil { 97 | ActiveBaiduUser = baidu 98 | return nil 99 | } 100 | return err 101 | } 102 | -------------------------------------------------------------------------------- /config/deleter.go: -------------------------------------------------------------------------------- 1 | package pcsconfig 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func (c *PCSConfig) DeleteBaiduUserByUID(uid uint64) bool { 8 | for k := range c.BaiduUserList { 9 | if c.BaiduUserList[k].UID == uid { 10 | c.BaiduUserList = append(c.BaiduUserList[:k], c.BaiduUserList[k+1:]...) 11 | 12 | // 修改 正在使用的 百度帐号 13 | if c.BaiduActiveUID == uid { 14 | if len(c.BaiduUserList) != 0 { 15 | c.BaiduActiveUID = c.BaiduUserList[0].UID 16 | } else { 17 | c.BaiduActiveUID = 0 18 | } 19 | } 20 | 21 | err := c.Save() 22 | if err != nil { 23 | fmt.Println(err) 24 | return false 25 | } 26 | return true 27 | } 28 | } 29 | return false 30 | } 31 | -------------------------------------------------------------------------------- /config/get_checker.go: -------------------------------------------------------------------------------- 1 | package pcsconfig 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/iikira/BaiduPCS-Go/downloader" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func (c *PCSConfig) GetBaiduUserByUID(uid uint64) (*Baidu, error) { 12 | for k := range c.BaiduUserList { 13 | if uid == c.BaiduUserList[k].UID { 14 | return c.BaiduUserList[k], nil 15 | } 16 | } 17 | return nil, fmt.Errorf("未找到百度帐号") 18 | } 19 | 20 | func (c *PCSConfig) GetAllBaiduUser() string { 21 | var s bytes.Buffer 22 | s.WriteString("\nindex\t\tuid\t用户名\n") 23 | 24 | for k := range c.BaiduUserList { 25 | s.WriteString(fmt.Sprintf("%4d", k) + "\t" + fmt.Sprintf("%11d", c.BaiduUserList[k].UID) + "\t" + c.BaiduUserList[k].Name + "\n") 26 | } 27 | s.WriteString("\n") 28 | return s.String() 29 | } 30 | 31 | func (c *PCSConfig) CheckUIDExist(uid uint64) bool { 32 | for k := range c.BaiduUserList { 33 | if uid == c.BaiduUserList[k].UID { 34 | return true 35 | } 36 | } 37 | return false 38 | } 39 | 40 | // GetSavePath 根据提供的网盘文件路径 path, 返回本地储存路径 41 | func GetSavePath(path string) string { 42 | return filepath.Dir(fmt.Sprintf("%s/%d_%s%s/..", 43 | SaveDir, 44 | ActiveBaiduUser.UID, 45 | ActiveBaiduUser.Name, 46 | path, 47 | )) 48 | } 49 | 50 | // CheckFileExist 检查本地文件是否与网盘的文件重名 51 | func CheckFileExist(path string) bool { 52 | savePath := GetSavePath(path) 53 | if _, err := os.Stat(savePath); err == nil { 54 | if _, err = os.Stat(savePath + downloader.DownloadingFileSuffix); err != nil { 55 | return true 56 | } 57 | } 58 | return false 59 | } 60 | -------------------------------------------------------------------------------- /config/seter.go: -------------------------------------------------------------------------------- 1 | package pcsconfig 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // SetBDUSS 设置百度 bduss 并保存 8 | func (c *PCSConfig) SetBDUSS(bduss string) (username string, err error) { 9 | b, err := NewWithBDUSS(bduss) 10 | if err != nil { 11 | return "", err 12 | } 13 | if c.CheckUIDExist(b.UID) { 14 | return "", fmt.Errorf("登录失败, 用户 %s 已存在", b.Name) 15 | } 16 | c.BaiduUserList = append(c.BaiduUserList, b) 17 | c.BaiduActiveUID = b.UID 18 | return b.Name, c.Save() 19 | } 20 | -------------------------------------------------------------------------------- /downloader/downloader.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Bluek404 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | 补充: 此 inplememt 基于 https://github.com/Bluek404/downloader 17 | 针对百度网盘下载, 做出一些修改 18 | 19 | 增加功能: 线程控制 20 | 删去功能: 暂停下载, 恢复下载 21 | */ 22 | 23 | package downloader 24 | 25 | import ( 26 | "fmt" 27 | "net/http" 28 | "os" 29 | "path/filepath" 30 | "time" 31 | ) 32 | 33 | var parallel int 34 | 35 | // FileDl 下载详情 36 | type FileDl struct { 37 | URL string // 下载地址 38 | Size int64 // 文件大小 39 | File *os.File // 要写入的文件 40 | 41 | BlockList blockList // 用于记录未下载的文件块起始位置 42 | 43 | *HTTPClient // http client 44 | 45 | onStart func() 46 | onFinish func() 47 | onError func(int, error) 48 | 49 | status status // 下载状态 50 | } 51 | 52 | // NewFileDl 创建新的文件下载 53 | // 54 | // 如果 size <= 0 则自动获取文件大小 55 | func NewFileDl(h *HTTPClient, url, savePath string, size int64) (*FileDl, error) { 56 | 57 | // 获取文件信息 58 | request, err := http.NewRequest("HEAD", url, nil) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | resp, err := h.Client.Do(request) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | if savePath == "" { 69 | finds := FileNameRE.FindStringSubmatch( 70 | resp.Header.Get("Content-Disposition"), 71 | ) 72 | if len(finds) >= 2 { 73 | savePath = finds[1] 74 | } else { 75 | // 找不到文件名, 凑合吧 76 | savePath = filepath.Base(url) 77 | } 78 | } 79 | 80 | // 如果文件存在, 取消下载 81 | if _, err = os.Stat(savePath); err == nil { 82 | if _, err = os.Stat(savePath + DownloadingFileSuffix); err != nil { 83 | return nil, fmt.Errorf("文件已存在: %s", savePath) 84 | } 85 | } 86 | 87 | // 检测要保存下载内容的目录是否存在 88 | // 不存在则创建该目录 89 | if _, err = os.Stat(filepath.Dir(savePath)); err != nil { 90 | err = os.MkdirAll(filepath.Dir(savePath), 0777) 91 | if err != nil { 92 | return nil, err 93 | } 94 | } 95 | 96 | // 移除旧的断点续传文件 97 | if _, err = os.Stat(savePath); err != nil { 98 | if _, err = os.Stat(savePath + DownloadingFileSuffix); err == nil { 99 | os.Remove(savePath + DownloadingFileSuffix) 100 | } 101 | } 102 | 103 | // 检测要下载的文件是否存在 104 | // 如果存在, 则打开文件 105 | // 不存在则创建文件 106 | file, err := os.OpenFile(savePath, os.O_RDWR|os.O_CREATE, 0666) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | if size != resp.ContentLength { 112 | size = resp.ContentLength 113 | } 114 | 115 | resp.Body.Close() 116 | 117 | f := &FileDl{ 118 | URL: url, 119 | Size: size, 120 | File: file, 121 | HTTPClient: h, 122 | } 123 | 124 | return f, nil 125 | } 126 | 127 | // Start 开始下载 128 | func (f *FileDl) Start() { 129 | // 控制线程 130 | parallel = maxParallel 131 | 132 | // 如果文件不大, 或者线程数设置过高, 则调低线程数 133 | if int64(maxParallel) > f.Size/int64(10*cacheSize) { 134 | parallel = int(f.Size/int64(10*cacheSize)) + 1 135 | } 136 | 137 | if err := f.loadBreakPoint(); err != nil { 138 | if f.Size <= 0 { // 获取不到文件的大小, 关闭多线程下载 (暂时) 139 | f.BlockList = append(f.BlockList, Block{ 140 | Begin: 0, 141 | End: -1, 142 | }) 143 | } else { 144 | blockSize := f.Size / int64(parallel) 145 | var begin int64 146 | // 数据平均分配给各个线程 147 | for i := 0; i < parallel; i++ { 148 | var end = (int64(i) + 1) * blockSize 149 | f.BlockList = append(f.BlockList, Block{ 150 | Begin: begin, 151 | End: end, 152 | }) 153 | begin = end + 1 154 | } 155 | // 将余出数据分配给最后一个线程 156 | f.BlockList[parallel-1].End += f.Size - f.BlockList[parallel-1].End 157 | f.BlockList[parallel-1].Final = true 158 | } 159 | } 160 | 161 | go func() { 162 | f.touch(f.onStart) 163 | // 开始下载 164 | err := f.download() 165 | if err != nil { 166 | f.touchOnError(0, err) 167 | return 168 | } 169 | }() 170 | } 171 | 172 | func (f *FileDl) download() error { 173 | f.startGetSpeeds() // 启用速度监测 174 | 175 | for i := range f.BlockList { 176 | go func(id int) { 177 | f.downloadBlockFn(id) 178 | }(i) 179 | } 180 | <-f.blockMonitor() 181 | 182 | f.touch(f.onFinish) 183 | 184 | f.File.Close() 185 | 186 | return nil 187 | } 188 | 189 | func (f *FileDl) startGetSpeeds() { 190 | go func() { 191 | var old = f.status.Downloaded 192 | for { 193 | time.Sleep(time.Second * 1) 194 | f.status.Speeds = f.status.Downloaded - old 195 | old = f.status.Downloaded 196 | 197 | if f.status.Speeds > f.status.MaxSpeeds { 198 | f.status.MaxSpeeds = f.status.Speeds 199 | } 200 | } 201 | }() 202 | } 203 | 204 | // GetStatus 获取下载统计信息 205 | func (f FileDl) GetStatus() status { 206 | return f.status 207 | } 208 | 209 | // OnStart 任务开始时触发的事件 210 | func (f *FileDl) OnStart(fn func()) { 211 | f.onStart = fn 212 | } 213 | 214 | // OnFinish 任务完成时触发的事件 215 | func (f *FileDl) OnFinish(fn func()) { 216 | f.onFinish = fn 217 | } 218 | 219 | // OnError 任务出错时触发的事件 220 | // 221 | // errCode为错误码,errStr为错误描述 222 | func (f *FileDl) OnError(fn func(int, error)) { 223 | f.onError = fn 224 | } 225 | 226 | // 用于触发事件 227 | func (f FileDl) touch(fn func()) { 228 | if fn != nil { 229 | go fn() 230 | } 231 | } 232 | 233 | // 触发Error事件 234 | func (f FileDl) touchOnError(errCode int, err error) { 235 | if f.onError != nil { 236 | go f.onError(errCode, err) 237 | } 238 | } 239 | 240 | // status 状态 241 | type status struct { 242 | Downloaded int64 `json:"downloaded"` 243 | Speeds int64 244 | MaxSpeeds int64 245 | } 246 | -------------------------------------------------------------------------------- /downloader/downloader_block.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "sync" 10 | "sync/atomic" 11 | "time" 12 | ) 13 | 14 | var ( 15 | mu sync.Mutex 16 | ) 17 | 18 | // Block 下载区块 19 | type Block struct { 20 | Begin int64 `json:"begin"` 21 | End int64 `json:"end"` 22 | running int // 线程的载入量 23 | Final bool `json:"isfinal"` // 最后线程, 因为最后的下载线程, 需要另外做处理 24 | } 25 | 26 | type blockList []Block 27 | 28 | // isDone 判断线程是否完成下载任务 29 | func (b *Block) isDone() bool { 30 | // 举个例子来演示 如何判断线程完成任务 31 | // 假设 文件的大小 (Content-Length) 为 300, 线程数为4 32 | // 则 Block 划分为 33 | // 线程0: 0-75; 线程1: 76-150; 线程2: 151-225; 线程3: 225-300 34 | // 正常情况下, 文件下载完成, 每个线程对应的 Block 会变为 (注意最后一个线程 (线程3)) 35 | // 线程0: 76-75; 线程1: 151-150; 线程2: 226-225; 线程3: 300-300 36 | // 假设 线程0 出现异常, 调用 setDone 方法, 线程0 会变为 37 | // 线程0: 0-0 38 | // 即 Block 的 Begin 和 End 值都为 0 时, 返回 true 39 | // 40 | // 最后一个线程状态的判断方法, Begin 和 End 的值相等, 则返回 true 41 | // 其他, End 值 减去 Begin 值为 -1, 则返回 true 42 | // 43 | // 暂时先这么判断吧 44 | return b.End-b.Begin <= -1 || (b.Final == true && b.End-b.Begin <= 0) || (b.End == 0 && b.Begin == 0) 45 | } 46 | 47 | // setDone 设置线程为完成下载任务状态 (简单粗暴) 48 | func (b *Block) setDone() { 49 | // 只操作 End 部分 50 | // 避免操作 Begin 部分, 否则可能写文件时, 会出现异常 51 | if b.Begin == 0 { 52 | b.End = 0 53 | return 54 | } 55 | b.End = b.Begin - 1 56 | } 57 | 58 | // isComplete 判断线程是否空闲, 59 | // 即 线程已完成下载任务 且 线程的载入量 (running 值) 为 0 60 | func (b *Block) isComplete() bool { 61 | return b.isDone() && b.running == 0 62 | } 63 | 64 | // avaliableThread 筛选空闲的线程, 65 | // 返回值, 没有空闲的线程, bool 返回 false 66 | // 找到空闲的线程, int 返回该线程的索引 index 67 | func (bl *blockList) avaliableThread() (int, bool) { 68 | index := -1 69 | for k := range *bl { 70 | if (*bl)[k].isComplete() { 71 | index = k 72 | break 73 | } 74 | } 75 | return index, index != -1 76 | } 77 | 78 | // isAllDone 检查所有的线程, 是否都完成了下载任务 79 | func (bl *blockList) isAllDone() bool { 80 | for k := range *bl { 81 | if (*bl)[k].isDone() { 82 | continue 83 | } 84 | return false 85 | } 86 | return true 87 | } 88 | 89 | // downloadBlockFn 线程控制器 90 | func (f *FileDl) downloadBlockFn(id int) { 91 | f.BlockList[id].running++ 92 | for_2: // code 为 1 时, 不重试 93 | // 其他的 code, 无限重试 94 | for { 95 | code, err := f.downloadBlock(id) 96 | 97 | // 成功, 退出循环 98 | if code == 0 || err == nil { 99 | break 100 | } 101 | 102 | // 未成功(有错误), 继续 103 | switch code { 104 | case 1: //不重试 105 | break for_2 // break for循环 106 | case 2: 107 | // 连接太多, 可能会 connect refuse 108 | time.Sleep(3 * time.Second) 109 | case 10: // 无限重试 110 | default: // 休息 3 秒, 再无限重试 111 | time.Sleep(3 * time.Second) 112 | } 113 | 114 | // 重新下载 115 | f.touchOnError(code, err) 116 | } 117 | 118 | f.BlockList[id].running-- 119 | } 120 | 121 | // downloadBlock 文件块下载 122 | // 根据 id 对于的 Block, 创建下载任务 123 | func (f *FileDl) downloadBlock(id int) (code int, err error) { 124 | if f.BlockList[id].isDone() { 125 | return 1, errors.New("already done") 126 | } 127 | 128 | request, err := http.NewRequest("GET", f.URL, nil) 129 | if err != nil { 130 | return 1, err 131 | } 132 | 133 | if f.BlockList[id].End != -1 { 134 | // 设置 Range 请求头, 给各线程分配内容 135 | request.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", f.BlockList[id].Begin, f.BlockList[id].End)) 136 | } 137 | 138 | resp, err := f.Client.Do(request) // 开始 http 请求 139 | if err != nil { 140 | return 2, err 141 | } 142 | 143 | switch resp.StatusCode { 144 | case 200, 206: 145 | // do nothing, continue 146 | case 416: //Requested Range Not Satisfiable 147 | // 可能是线程在等待响应时, 已被其他线程重载 148 | return 1, errors.New("thread already reload") 149 | case 406: // Not Acceptable 150 | case 429, 509: // Too Many Requests 151 | for f.status.Speeds >= f.status.MaxSpeeds/5 { 152 | // 下载速度若不减慢, 循环就不会退出 153 | time.Sleep(1 * time.Second) 154 | } 155 | return 3, errors.New(resp.Status) 156 | default: 157 | fmt.Println("不知道的错误", id, resp.StatusCode, resp.Status) // 调试 158 | return 2, errors.New(resp.Status) 159 | } 160 | 161 | defer resp.Body.Close() 162 | 163 | var buf = make([]byte, cacheSize) 164 | 165 | for { 166 | begin := f.BlockList[id].Begin // 用于下文比较 167 | n, err := resp.Body.Read(buf) 168 | 169 | bufSize := int64(n) 170 | if f.BlockList[id].End != -1 { 171 | // 检查下载的大小是否超出需要下载的大小 172 | // 这里End+1是因为http的Range的end是包括在需要下载的数据内的 173 | // 比如 0-1 的长度其实是2,所以这里end需要+1 174 | needSize := f.BlockList[id].End + 1 - f.BlockList[id].Begin 175 | 176 | // 已完成 (未雨绸缪) 177 | if needSize < 0 { 178 | return 1, errors.New("already complete") 179 | } 180 | if bufSize > needSize { 181 | // 数据大小不正常 182 | // 一般是该线程已被重载 183 | 184 | // 也可能是因为网络环境不好导致 185 | // 比如用中国电信下载国外文件 186 | 187 | // 设置数据大小来去掉多余数据 188 | // 并结束这个线程的下载 189 | bufSize = needSize 190 | n = int(needSize) 191 | err = io.EOF 192 | } 193 | } 194 | 195 | // 将缓冲数据写入硬盘 196 | f.File.WriteAt(buf[:n], begin) 197 | 198 | // 两次 begin 不相等, 可能已有新的空闲线程参与 199 | // 旧线程应该被结束 200 | if begin != f.BlockList[id].Begin { 201 | return 1, errors.New("thread already reload") 202 | } 203 | 204 | // 更新已下载大小 205 | atomic.AddInt64(&f.status.Downloaded, bufSize) 206 | atomic.AddInt64(&f.BlockList[id].Begin, bufSize) 207 | 208 | if err != nil { 209 | // 下载数据可能出现异常, 重新下载 210 | if f.BlockList[id].End != -1 && !f.BlockList[id].isDone() { 211 | return 11, fmt.Errorf("download failed, %s, reset", err) 212 | } 213 | switch { 214 | case err == io.EOF: 215 | // 数据已经下载完毕 216 | return 0, nil 217 | default: 218 | // 其他错误, 返回 219 | return 5, err 220 | } 221 | } 222 | } 223 | } 224 | 225 | // blockMonitor 延迟监控各线程状态, 226 | // 管理空闲 (已完成下载任务) 的线程, 227 | // 清除长时间无响应, 和下载速度为 0 的线程 228 | func (f *FileDl) blockMonitor() <-chan struct{} { 229 | c := make(chan struct{}) 230 | go func() { 231 | for { 232 | // 下载完毕, 线程全部完成下载任务, 发送结束信号 233 | if f.BlockList.isAllDone() { 234 | c <- struct{}{} 235 | os.Remove(f.File.Name() + DownloadingFileSuffix) // 删除断点信息 236 | 237 | if f.Size <= 0 { 238 | fileInfo, err := f.File.Stat() 239 | if err == nil && fileInfo.Size() > f.status.Downloaded { 240 | f.File.Truncate(f.status.Downloaded) 241 | } 242 | } else { 243 | f.File.Truncate(f.Size) 244 | } 245 | 246 | break 247 | } 248 | 249 | f.recordBreakPoint() 250 | 251 | for k := range f.BlockList { 252 | go func(k int) { 253 | // 过滤已完成下载任务的线程 254 | if f.BlockList[k].isDone() { 255 | return 256 | } 257 | 258 | // 清除长时间无响应, 和下载速度为 0 的线程 259 | go func(k int) { 260 | // 设 old 速度监测点, 2 秒后检查速度有无变化 261 | old := f.BlockList[k].Begin 262 | time.Sleep(2 * time.Second) 263 | // 过滤 速度有变化, 或 2 秒内完成了下载任务 的线程 264 | if old != f.BlockList[k].Begin || f.BlockList[k].isDone() { 265 | return 266 | } 267 | 268 | // 筛选出 长时间无响应, 和下载速度为 0 的线程 269 | // 然后尝试清除该线程, 选出其他 空闲的线程, 重新添加下载任务 270 | mu.Lock() // 加锁, 防止出现重复添加线程的状况 (实验阶段) 271 | 272 | // 筛选空闲的线程 273 | index, ok := f.BlockList.avaliableThread() 274 | if !ok { // 没有空的 275 | mu.Unlock() // 解锁 276 | return 277 | } 278 | 279 | // 复制 旧线程 的信息到 空闲的新线程 280 | f.BlockList[index].End = f.BlockList[k].End 281 | f.BlockList[index].Begin = f.BlockList[k].Begin 282 | f.BlockList[index].Final = f.BlockList[k].Final 283 | 284 | f.BlockList[k].setDone() // 清除旧线程 285 | 286 | mu.Unlock() // 解锁 287 | f.downloadBlockFn(index) // 添加任务 288 | }(k) 289 | 290 | // 动态分配新线程 291 | go func(k int) { 292 | mu.Lock() 293 | 294 | // 筛选空闲的线程 295 | index, ok := f.BlockList.avaliableThread() 296 | if !ok { // 没有空的 297 | mu.Unlock() // 解锁 298 | return 299 | } 300 | 301 | middle := (f.BlockList[k].Begin + f.BlockList[k].End) / 2 302 | if f.BlockList[k].End-middle <= 102400 { // 如果线程剩余的下载量太少, 不分配空闲线程 303 | mu.Unlock() 304 | return 305 | } 306 | 307 | // 设置偏移量 offset, 尽量减少流量的浪费 308 | offset := cacheSize - ((middle - f.BlockList[k].Begin) % cacheSize) - 2 309 | f.BlockList[index].Begin = middle + offset + 1 310 | f.BlockList[index].End = f.BlockList[k].End 311 | f.BlockList[index].Final = f.BlockList[k].Final 312 | f.BlockList[k].End = middle + offset 313 | 314 | mu.Unlock() 315 | // fmt.Println(k, f.BlockList[k], "=>", index, f.BlockList[index]) 316 | f.downloadBlockFn(index) 317 | }(k) 318 | 319 | }(k) 320 | } 321 | time.Sleep(1 * time.Second) // 监测频率 1 秒 322 | } 323 | }() 324 | return c 325 | } 326 | -------------------------------------------------------------------------------- /downloader/downloader_config.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | var ( 8 | maxParallel = 5 9 | cacheSize int64 = 2048 10 | 11 | // FileNameRE 正则表达式: 匹配文件名 12 | FileNameRE = regexp.MustCompile("filename=\"(.*?)\"") 13 | ) 14 | 15 | // SetMaxParallel 设置最大线程 16 | func SetMaxParallel(t int) { 17 | if t <= 0 { 18 | panic("downloader.SetMaxParallel: zero or negative parallel") 19 | } 20 | maxParallel = t 21 | } 22 | 23 | // SetCacheSize 设置缓冲大小 24 | func SetCacheSize(size int64) { 25 | if size < 1024 { 26 | cacheSize = 1024 27 | return 28 | } 29 | cacheSize = size 30 | } 31 | -------------------------------------------------------------------------------- /downloader/example.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // DoDownload 简单网络下载器, 使用默认下载线程, 11 | // 通过调用 SetMaxThread 来修改默认下载线程 12 | func DoDownload(url string, fileName string, sizeofFile int64) { 13 | h := NewHTTPClient() 14 | fileDl, err := NewFileDl(h, url, fileName, sizeofFile) 15 | if err != nil { 16 | return 17 | } 18 | 19 | done := make(chan struct{}) 20 | 21 | var exit = make(chan bool) 22 | fileDl.OnStart(func() { 23 | fmt.Println("download started") 24 | format := "\r%v/%v [%s] %v byte/s %v" 25 | 26 | for_1: 27 | for { 28 | status := fileDl.GetStatus() 29 | var i = float64(status.Downloaded) / float64(fileDl.Size) * 50 30 | h := strings.Repeat("=", int(i)) + strings.Repeat(" ", 50-int(i)) 31 | 32 | select { 33 | case <-exit: 34 | fmt.Printf(format, status.Downloaded, fileDl.Size, h, 0, "[FINISH]") 35 | fmt.Println("\ndownload finished") 36 | break for_1 37 | default: 38 | time.Sleep(time.Second * 1) 39 | fmt.Printf(format, status.Downloaded, fileDl.Size, h, status.Speeds, "[DOWNLOADING]") 40 | os.Stdout.Sync() 41 | } 42 | } 43 | }) 44 | 45 | fileDl.OnFinish(func() { 46 | exit <- true 47 | done <- struct{}{} 48 | }) 49 | 50 | fileDl.OnError(func(errCode int, e error) { 51 | err = e 52 | }) 53 | 54 | fileDl.Start() 55 | <-done 56 | } 57 | -------------------------------------------------------------------------------- /downloader/fetch.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | ) 11 | 12 | // HTTPGet 简单实现 http 访问 GET 请求 13 | func HTTPGet(urlStr string) (body []byte, err error) { 14 | resp, err := http.Get(urlStr) 15 | if err != nil { 16 | return nil, err 17 | } 18 | defer resp.Body.Close() 19 | return ioutil.ReadAll(resp.Body) 20 | } 21 | 22 | // Fetch 实现 http/https 访问 和 GET/POST 请求, 23 | // 根据给定的 method (GET, POST, HEAD, PUT 等等), urlStr (网址), 24 | // post (post 数据), header (header 请求头数据), 进行网站访问。 25 | // 返回值分别为 网站主体, 错误 26 | func (h *HTTPClient) Fetch(method string, urlStr string, post interface{}, header map[string]string) (body []byte, err error) { 27 | var ( 28 | req *http.Request 29 | obody io.Reader 30 | ) 31 | 32 | if post != nil { 33 | switch value := post.(type) { 34 | case map[string]string: 35 | query := url.Values{} 36 | for k := range value { 37 | query.Set(k, value[k]) 38 | } 39 | obody = strings.NewReader(query.Encode()) 40 | case string: 41 | obody = strings.NewReader(value) 42 | case []byte: 43 | obody = bytes.NewReader(value[:]) 44 | } 45 | } 46 | req, err = http.NewRequest(method, urlStr, obody) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | if header != nil { 52 | for key := range header { 53 | req.Header.Add(key, header[key]) 54 | } 55 | } 56 | 57 | resp, err := h.Client.Do(req) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | body, err = ioutil.ReadAll(resp.Body) 63 | resp.Body.Close() 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /downloader/http_client.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/http" 6 | "net/http/cookiejar" 7 | "time" 8 | ) 9 | 10 | // HTTPClient http client 11 | type HTTPClient struct { 12 | Client http.Client 13 | } 14 | 15 | // NewHTTPClient 返回 HTTPClient 的指针, 16 | // 预设了一些配置 17 | func NewHTTPClient() *HTTPClient { 18 | return &HTTPClient{ 19 | Client: http.Client{ 20 | Transport: &http.Transport{ 21 | TLSClientConfig: &tls.Config{ 22 | InsecureSkipVerify: true, 23 | }, 24 | TLSHandshakeTimeout: 10 * time.Second, 25 | DisableKeepAlives: false, 26 | DisableCompression: false, 27 | ResponseHeaderTimeout: 10 * time.Second, 28 | ExpectContinueTimeout: 10 * time.Second, 29 | }, 30 | }, 31 | } 32 | } 33 | 34 | // SetCookiejar 设置 cookie 35 | func (h *HTTPClient) SetCookiejar(c *cookiejar.Jar) { 36 | h.Client.Jar = c 37 | } 38 | 39 | // ClearCookiejar 清空 cookie 40 | func (h *HTTPClient) ClearCookiejar() { 41 | h.Client.Jar, _ = cookiejar.New(nil) 42 | } 43 | 44 | // SetHTTPSecure 是否启用 https 安全检查 45 | func (h *HTTPClient) SetHTTPSecure(b bool) { 46 | h.Client.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = !b 47 | } 48 | 49 | // SetKeepAlive 设置 Keep-Alive 50 | func (h *HTTPClient) SetKeepAlive(b bool) { 51 | h.Client.Transport.(*http.Transport).DisableKeepAlives = !b 52 | } 53 | 54 | //SetGzip 是否启用Gzip 55 | func (h *HTTPClient) SetGzip(b bool) { 56 | h.Client.Transport.(*http.Transport).DisableCompression = !b 57 | } 58 | 59 | //SetResponseHeaderTimeout 设置目标服务器响应超时时间 60 | func (h *HTTPClient) SetResponseHeaderTimeout(t time.Duration) { 61 | h.Client.Transport.(*http.Transport).ResponseHeaderTimeout = t 62 | } 63 | 64 | // SetTimeout 设置 http 请求超时时间 默认30s 65 | func (h *HTTPClient) SetTimeout(t time.Duration) { 66 | h.Client.Timeout = t 67 | } 68 | -------------------------------------------------------------------------------- /downloader/resume_breakpoint.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | ) 7 | 8 | var ( 9 | // DownloadingFileSuffix 断点续传临时文件后缀 10 | DownloadingFileSuffix = ".baidupcs_go_downloading" 11 | ) 12 | 13 | type downloadStatus struct { 14 | Downloaded int64 `json:"downloaded"` 15 | BlockList blockList `json:"block_list"` 16 | } 17 | 18 | // recordBreakPoint 保存下载断点到文件, 用于断点续传 19 | func (f *FileDl) recordBreakPoint() error { 20 | byt, err := json.Marshal(downloadStatus{ 21 | Downloaded: f.status.Downloaded, 22 | BlockList: f.BlockList, 23 | }) 24 | if err != nil { 25 | return err 26 | } 27 | return ioutil.WriteFile(f.File.Name()+DownloadingFileSuffix, byt, 0644) 28 | } 29 | 30 | // loadBreakPoint 尝试从文件载入下载断点 31 | func (f *FileDl) loadBreakPoint() error { 32 | byt, err := ioutil.ReadFile(f.File.Name() + DownloadingFileSuffix) 33 | if err != nil { 34 | return err 35 | } 36 | downloadStatus := new(downloadStatus) 37 | err = json.Unmarshal(byt, downloadStatus) 38 | if err != nil { 39 | return err 40 | } 41 | f.status.Downloaded = downloadStatus.Downloaded 42 | f.BlockList = downloadStatus.BlockList 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gobs/args" 6 | "github.com/iikira/BaiduPCS-Go/command" 7 | "github.com/iikira/BaiduPCS-Go/config" 8 | "github.com/kardianos/osext" 9 | "github.com/peterh/liner" 10 | "github.com/urfave/cli" 11 | "io" 12 | "log" 13 | "os" 14 | "path/filepath" 15 | "runtime" 16 | "sort" 17 | "strconv" 18 | ) 19 | 20 | var ( 21 | historyFile = "pcs_command_history.txt" 22 | 23 | reloadFn = func(c *cli.Context) error { 24 | baidupcscmd.ReloadIfInConsole() 25 | return nil 26 | } 27 | ) 28 | 29 | func init() { 30 | // change work directory 31 | folderPath, err := osext.ExecutableFolder() 32 | if err != nil { 33 | folderPath, err = filepath.Abs(filepath.Dir(os.Args[0])) 34 | if err != nil { 35 | folderPath = filepath.Dir(os.Args[0]) 36 | } 37 | } 38 | os.Chdir(folderPath) 39 | 40 | pcsconfig.Init() 41 | } 42 | 43 | func main() { 44 | app := cli.NewApp() 45 | app.Name = "BaiduPCS-Go" 46 | app.Version = "beta-v2" 47 | app.Author = "iikira/BaiduPCS-Go: https://github.com/iikira/BaiduPCS-Go" 48 | app.Usage = "百度网盘工具箱 for " + runtime.GOOS + "/" + runtime.GOARCH 49 | app.Description = `BaiduPCS-Go 使用 Go语言编写, 为操作百度网盘, 提供实用功能. 50 | 具体功能, 参见 COMMANDS 列表 51 | 52 | 特色: 53 | 网盘内列出文件和目录, 支持通配符匹配路径; 54 | 下载网盘内文件, 支持网盘内目录 (文件夹) 下载, 支持多个文件或目录下载, 支持断点续传和高并发高速下载. 55 | 56 | 程序目前处于测试版, 后续会添加更多的实用功能.` 57 | app.Action = func(c *cli.Context) { 58 | if c.NArg() == 0 { 59 | cli.ShowAppHelp(c) 60 | 61 | line := newLiner() 62 | defer closeLiner(line) 63 | 64 | for { 65 | if commandLine, err := line.Prompt("BaiduPCS-Go > "); err == nil { 66 | line.AppendHistory(commandLine) 67 | 68 | cmdArgs := args.GetArgs(commandLine) 69 | if len(cmdArgs) == 0 { 70 | continue 71 | } 72 | 73 | s := []string{os.Args[0]} 74 | s = append(s, cmdArgs...) 75 | 76 | closeLiner(line) 77 | 78 | c.App.Run(s) 79 | 80 | line = newLiner() 81 | 82 | } else if err == liner.ErrPromptAborted || err == io.EOF { 83 | break 84 | } else { 85 | log.Print("Error reading line: ", err) 86 | continue 87 | } 88 | } 89 | } else { 90 | fmt.Printf("未找到命令: %s\n运行命令 %s help 获取帮助\n", c.Args().Get(0), app.Name) 91 | } 92 | } 93 | 94 | app.Commands = []cli.Command{ 95 | { 96 | Name: "login", 97 | Usage: "使用百度BDUSS登录百度账号", 98 | Description: fmt.Sprintf("\n 示例: \n\n %s\n\n %s\n\n %s\n\n %s\n\n %s\n", 99 | filepath.Base(os.Args[0])+" login --bduss=123456789", 100 | filepath.Base(os.Args[0])+" login", 101 | "百度BDUSS获取方法: ", 102 | "参考这篇 Wiki: https://github.com/iikira/BaiduPCS-Go/wiki/关于-获取百度-BDUSS", 103 | "或者百度搜索: 获取百度BDUSS", 104 | ), 105 | Category: "百度帐号操作", 106 | Before: reloadFn, 107 | After: reloadFn, 108 | Action: func(c *cli.Context) error { 109 | bduss := "" 110 | if c.IsSet("bduss") { 111 | bduss = c.String("bduss") 112 | } else if c.NArg() == 0 { 113 | cli.ShowCommandHelp(c, c.Command.Name) 114 | line := liner.NewLiner() 115 | line.SetCtrlCAborts(true) 116 | defer line.Close() 117 | bduss, _ = line.Prompt("请输入百度BDUSS值, 回车键提交 > ") 118 | } else { 119 | cli.ShowCommandHelp(c, c.Command.Name) 120 | return nil 121 | } 122 | 123 | username, err := pcsconfig.Config.SetBDUSS(bduss) 124 | if err != nil { 125 | fmt.Println(err) 126 | return nil 127 | } 128 | 129 | fmt.Println("百度帐号登录成功:", username) 130 | return nil 131 | }, 132 | Flags: []cli.Flag{ 133 | cli.StringFlag{ 134 | Name: "bduss", 135 | Usage: "百度BDUSS", 136 | }, 137 | }, 138 | }, 139 | { 140 | Name: "chuser", 141 | Usage: "切换已登录的百度帐号", 142 | Description: fmt.Sprintf("%s\n 示例:\n\n %s\n %s\n", 143 | "如果运行该条命令没有提供参数, 程序将会列出所有的百度帐号, 供选择切换", 144 | filepath.Base(os.Args[0])+" chuser --uid=123456789", 145 | filepath.Base(os.Args[0])+" chuser", 146 | ), 147 | Category: "百度帐号操作", 148 | Before: reloadFn, 149 | After: reloadFn, 150 | Action: func(c *cli.Context) error { 151 | if len(pcsconfig.Config.BaiduUserList) == 0 { 152 | fmt.Println("未设置任何百度帐号, 不能切换") 153 | return nil 154 | } 155 | 156 | var uid uint64 157 | if c.IsSet("uid") { 158 | if pcsconfig.Config.CheckUIDExist(c.Uint64("uid")) { 159 | uid = c.Uint64("uid") 160 | } else { 161 | fmt.Println("切换用户失败, uid 不存在") 162 | } 163 | } else if c.NArg() == 0 { 164 | cli.HandleAction(app.Command("loglist").Action, c) 165 | 166 | line := liner.NewLiner() 167 | line.SetCtrlCAborts(true) 168 | defer line.Close() 169 | nLine, _ := line.Prompt("请输入要切换帐号的 index 值 > ") 170 | 171 | if n, err := strconv.Atoi(nLine); err == nil && n >= 0 && n < len(pcsconfig.Config.BaiduUserList) { 172 | uid = pcsconfig.Config.BaiduUserList[n].UID 173 | } else { 174 | fmt.Println("切换用户失败, 请检查 index 值是否正确") 175 | } 176 | } else { 177 | cli.ShowCommandHelp(c, c.Command.Name) 178 | } 179 | 180 | if uid == 0 { 181 | return nil 182 | } 183 | 184 | pcsconfig.Config.BaiduActiveUID = uid 185 | if err := pcsconfig.Config.Save(); err != nil { 186 | fmt.Println(err) 187 | return nil 188 | } 189 | 190 | fmt.Printf("切换用户成功, %v\n", pcsconfig.ActiveBaiduUser.Name) 191 | return nil 192 | 193 | }, 194 | Flags: []cli.Flag{ 195 | cli.StringFlag{ 196 | Name: "uid", 197 | Usage: "百度帐号 uid 值", 198 | }, 199 | }, 200 | }, 201 | { 202 | Name: "logout", 203 | Usage: "退出已登录的百度帐号", 204 | Description: fmt.Sprintf("%s\n 示例:\n\n %s\n %s\n", 205 | "如果运行该条命令没有提供参数, 程序将会列出所有的百度帐号, 供选择退出", 206 | filepath.Base(os.Args[0])+" logout --uid=123456789", 207 | filepath.Base(os.Args[0])+" logout", 208 | ), 209 | Category: "百度帐号操作", 210 | Before: reloadFn, 211 | After: reloadFn, 212 | Action: func(c *cli.Context) error { 213 | if len(pcsconfig.Config.BaiduUserList) == 0 { 214 | fmt.Println("未设置任何百度帐号, 不能退出") 215 | return nil 216 | } 217 | 218 | var uid uint64 219 | if c.IsSet("uid") { 220 | if pcsconfig.Config.CheckUIDExist(c.Uint64("uid")) { 221 | uid = c.Uint64("uid") 222 | } else { 223 | fmt.Println("退出用户失败, uid 不存在") 224 | } 225 | } else if c.NArg() == 0 { 226 | cli.HandleAction(app.Command("loglist").Action, c) 227 | 228 | line := liner.NewLiner() 229 | line.SetCtrlCAborts(true) 230 | defer line.Close() 231 | nLine, _ := line.Prompt("请输入要退出帐号的 index 值 > ") 232 | 233 | if n, err := strconv.Atoi(nLine); err == nil && n >= 0 && n < len(pcsconfig.Config.BaiduUserList) { 234 | uid = pcsconfig.Config.BaiduUserList[n].UID 235 | } else { 236 | fmt.Println("退出用户失败, 请检查 index 值是否正确") 237 | } 238 | } else { 239 | cli.ShowCommandHelp(c, c.Command.Name) 240 | } 241 | 242 | if uid == 0 { 243 | return nil 244 | } 245 | 246 | // 删除之前先获取被删除的数据, 用于下文输出日志 247 | baidu, err := pcsconfig.Config.GetBaiduUserByUID(uid) 248 | if err != nil { 249 | fmt.Println(err) 250 | return nil 251 | } 252 | 253 | if !pcsconfig.Config.DeleteBaiduUserByUID(uid) { 254 | fmt.Printf("退出用户失败, %s\n", baidu.Name) 255 | } 256 | 257 | fmt.Printf("退出用户成功, %v\n", baidu.Name) 258 | return nil 259 | }, 260 | Flags: []cli.Flag{ 261 | cli.StringFlag{ 262 | Name: "uid", 263 | Usage: "百度帐号 uid 值", 264 | }, 265 | }, 266 | }, 267 | { 268 | Name: "loglist", 269 | Usage: "获取当前帐号, 和所有已登录的百度帐号", 270 | Category: "百度帐号操作", 271 | Before: reloadFn, 272 | Action: func(c *cli.Context) error { 273 | fmt.Printf("\n当前帐号 uid: %d, 用户名: %s\n", pcsconfig.ActiveBaiduUser.UID, pcsconfig.ActiveBaiduUser.Name) 274 | fmt.Println(pcsconfig.Config.GetAllBaiduUser()) 275 | return nil 276 | }, 277 | }, 278 | { 279 | Name: "quota", 280 | Usage: "获取配额, 即获取网盘总空间, 和已使用空间", 281 | Category: "网盘操作", 282 | Before: reloadFn, 283 | Action: func(c *cli.Context) error { 284 | baidupcscmd.RunGetQuota() 285 | return nil 286 | }, 287 | }, 288 | { 289 | Name: "cd", 290 | Usage: "切换工作目录", 291 | UsageText: fmt.Sprintf("%s cd <目录 绝对路径或相对路径>", filepath.Base(os.Args[0])), 292 | Category: "网盘操作", 293 | Before: reloadFn, 294 | After: reloadFn, 295 | Action: func(c *cli.Context) error { 296 | if c.NArg() == 0 { 297 | cli.ShowCommandHelp(c, c.Command.Name) 298 | return nil 299 | } 300 | baidupcscmd.RunChangeDirectory(c.Args().Get(0)) 301 | return nil 302 | }, 303 | }, 304 | { 305 | Name: "ls", 306 | Aliases: []string{"l", "ll"}, 307 | Usage: "列出当前工作目录内的文件和目录 或 指定目录内的文件和目录", 308 | UsageText: fmt.Sprintf("%s ls <目录 绝对路径或相对路径>", filepath.Base(os.Args[0])), 309 | Category: "网盘操作", 310 | Before: reloadFn, 311 | Action: func(c *cli.Context) error { 312 | baidupcscmd.RunLs(c.Args().Get(0)) 313 | return nil 314 | }, 315 | }, 316 | { 317 | Name: "pwd", 318 | Usage: "输出当前所在目录 (工作目录)", 319 | UsageText: fmt.Sprintf("%s pwd", filepath.Base(os.Args[0])), 320 | Category: "网盘操作", 321 | Before: reloadFn, 322 | Action: func(c *cli.Context) error { 323 | fmt.Println(pcsconfig.ActiveBaiduUser.Workdir) 324 | return nil 325 | }, 326 | }, 327 | { 328 | Name: "meta", 329 | Usage: "获取单个文件/目录的元信息 (详细信息)", 330 | UsageText: fmt.Sprintf("%s meta <文件/目录 绝对路径或相对路径>", filepath.Base(os.Args[0])), 331 | Category: "网盘操作", 332 | Before: reloadFn, 333 | Action: func(c *cli.Context) error { 334 | baidupcscmd.RunGetMeta(c.Args().Get(0)) 335 | return nil 336 | }, 337 | }, 338 | { 339 | Name: "rm", 340 | Usage: "删除 单个/多个 文件/目录", 341 | UsageText: fmt.Sprintf("%s rm <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...", filepath.Base(os.Args[0])), 342 | Description: fmt.Sprintf("\n %s\n", 343 | "注意: 删除多个文件和目录时, 请确保每一个文件和目录都存在, 否则删除操作会失败.", 344 | ), 345 | Category: "网盘操作", 346 | Before: reloadFn, 347 | Action: func(c *cli.Context) error { 348 | if c.NArg() == 0 { 349 | cli.ShowCommandHelp(c, c.Command.Name) 350 | return nil 351 | } 352 | 353 | var dargs []string 354 | for i := 0; c.Args().Get(i) != ""; i++ { 355 | dargs = append(dargs, c.Args().Get(i)) 356 | } 357 | baidupcscmd.RunRemove(dargs...) 358 | return nil 359 | }, 360 | }, 361 | { 362 | Name: "mkdir", 363 | Usage: "创建目录", 364 | UsageText: fmt.Sprintf("%s mkdir <目录 绝对路径或相对路径> ...", filepath.Base(os.Args[0])), 365 | Category: "网盘操作", 366 | Before: reloadFn, 367 | Action: func(c *cli.Context) error { 368 | if c.NArg() == 0 { 369 | cli.ShowCommandHelp(c, c.Command.Name) 370 | return nil 371 | } 372 | 373 | baidupcscmd.RunMkdir(c.Args().Get(0)) 374 | return nil 375 | }, 376 | }, 377 | { 378 | Name: "download", 379 | Aliases: []string{"d"}, 380 | Usage: "下载文件或目录", 381 | UsageText: fmt.Sprintf("%s download <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...", filepath.Base(os.Args[0])), 382 | Description: "下载的文件将会保存到, 程序所在目录的 download/ 目录 (文件夹).\n 已支持目录下载.\n 已支持多个文件或目录下载.\n 自动跳过下载重名的文件! \n", 383 | Category: "网盘操作", 384 | Before: reloadFn, 385 | Action: func(c *cli.Context) error { 386 | if c.NArg() == 0 { 387 | cli.ShowCommandHelp(c, c.Command.Name) 388 | return nil 389 | } 390 | 391 | var dargs []string 392 | for i := 0; c.Args().Get(i) != ""; i++ { 393 | dargs = append(dargs, c.Args().Get(i)) 394 | } 395 | baidupcscmd.RunDownload(dargs...) 396 | return nil 397 | }, 398 | }, 399 | { 400 | Name: "set", 401 | Usage: "设置配置", 402 | UsageText: fmt.Sprintf("%s set OptionName Value", filepath.Base(os.Args[0])), 403 | Description: ` 404 | 可设置的值: 405 | 406 | OptionName Value 407 | ------------------ 408 | max_parallel 下载最大线程 (并发量) - 建议值 ( 100 ~ 500 ) 409 | 410 | 例子: 411 | 412 | set max_parallel 250 413 | `, 414 | Category: "配置", 415 | Before: reloadFn, 416 | After: reloadFn, 417 | Action: func(c *cli.Context) error { 418 | if c.NArg() < 2 { 419 | cli.ShowCommandHelp(c, c.Command.Name) 420 | return nil 421 | } 422 | 423 | switch c.Args().Get(0) { 424 | case "max_parallel": 425 | parallel, err := strconv.Atoi(c.Args().Get(1)) 426 | if err != nil { 427 | return cli.NewExitError(fmt.Errorf("max_parallel 设置值不合法, 错误: %s", err), 1) 428 | } 429 | 430 | pcsconfig.Config.MaxParallel = parallel 431 | err = pcsconfig.Config.Save() 432 | if err != nil { 433 | fmt.Println("设置失败, 错误:", err) 434 | return nil 435 | } 436 | fmt.Printf("设置成功, %s -> %v\n", c.Args().Get(0), c.Args().Get(1)) 437 | 438 | default: 439 | fmt.Printf("未知设定值: %s\n\n", c.Args().Get(0)) 440 | cli.ShowCommandHelp(c, "set") 441 | } 442 | return nil 443 | }, 444 | }, 445 | { 446 | Name: "quit", 447 | Aliases: []string{"exit"}, 448 | Usage: "退出程序", 449 | Action: func(c *cli.Context) error { 450 | return cli.NewExitError("", 0) 451 | }, 452 | Hidden: true, 453 | HideHelp: true, 454 | }, 455 | } 456 | 457 | sort.Sort(cli.FlagsByName(app.Flags)) 458 | sort.Sort(cli.CommandsByName(app.Commands)) 459 | 460 | app.Run(os.Args) 461 | } 462 | 463 | func newLiner() *liner.State { 464 | line := liner.NewLiner() 465 | 466 | line.SetCtrlCAborts(true) 467 | 468 | if f, err := os.Open(historyFile); err == nil { 469 | line.ReadHistory(f) 470 | f.Close() 471 | } 472 | 473 | return line 474 | } 475 | 476 | func closeLiner(line *liner.State) { 477 | if f, err := os.Create(historyFile); err != nil { 478 | log.Print("Error writing history file: ", err) 479 | } else { 480 | line.WriteHistory(f) 481 | f.Close() 482 | } 483 | line.Close() 484 | } 485 | -------------------------------------------------------------------------------- /util/convert.go: -------------------------------------------------------------------------------- 1 | package pcsutil 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const ( 9 | b = (int64)(1 << (10 * iota)) 10 | kb 11 | mb 12 | gb 13 | tb 14 | pb 15 | ) 16 | 17 | // ConvertFileSize 文件大小格式化输出 18 | func ConvertFileSize(size int64, precision ...int) string { 19 | p := 6 20 | if len(precision) == 1 { 21 | p = precision[0] 22 | } 23 | pint := fmt.Sprint(p) 24 | if size <= 0 { 25 | return "0" 26 | } 27 | if size < kb { 28 | return fmt.Sprintf("%."+pint+"fB", float64(size)/float64(b)) 29 | } 30 | if size < mb { 31 | return fmt.Sprintf("%."+pint+"fKB", float64(size)/float64(kb)) 32 | } 33 | if size < gb { 34 | return fmt.Sprintf("%."+pint+"fMB", float64(size)/float64(mb)) 35 | } 36 | if size < tb { 37 | return fmt.Sprintf("%."+pint+"fGB", float64(size)/float64(gb)) 38 | } 39 | if size < pb { 40 | return fmt.Sprintf("%."+pint+"fTB", float64(size)/float64(tb)) 41 | } 42 | return fmt.Sprintf("%."+pint+"fPB", float64(size)/float64(pb)) 43 | } 44 | 45 | // IntToBool int 类型转换为 bool 46 | func IntToBool(i int) bool { 47 | if i == 0 { 48 | return false 49 | } 50 | return true 51 | } 52 | 53 | // FormatTime 讲 Unix 时间戳, 转换为字符串 54 | func FormatTime(t int64) string { 55 | return time.Unix(t, 0).Format("2006-01-02 03:04:05") 56 | } 57 | -------------------------------------------------------------------------------- /util/error.go: -------------------------------------------------------------------------------- 1 | package pcsutil 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | // PrintErrIfExist 简易错误处理, 如果 err 存在, 就只向屏幕输出 err 。 9 | func PrintErrIfExist(err error) { 10 | if err != nil { 11 | log.Println(err) 12 | } 13 | } 14 | 15 | // PrintErrAndExit 简易错误处理, 如果 err 存在, 向屏幕输出 err 并退出, annotate 是加在 err 之前的注释信息。 16 | func PrintErrAndExit(annotate string, err error) { 17 | if err != nil { 18 | log.Println(annotate, err) 19 | os.Exit(1) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /util/log_colorable_prefix.go: -------------------------------------------------------------------------------- 1 | package pcsutil 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fatih/color" 6 | "log" 7 | ) 8 | 9 | var ( 10 | // ErrorColor 设置输出错误的颜色 11 | ErrorColor = color.New(color.FgRed).SprintFunc() 12 | ) 13 | 14 | // 自定义log writer 15 | type logWriter struct{} 16 | 17 | func (logWriter) Write(bytes []byte) (int, error) { 18 | return fmt.Fprint(color.Output, "["+BeijingTimeOption("Refer")+"] "+string(bytes)) 19 | } 20 | 21 | // SetLogPrefix 设置日志输出的时间前缀 22 | func SetLogPrefix() { 23 | log.SetFlags(0) 24 | log.SetOutput(new(logWriter)) 25 | } 26 | -------------------------------------------------------------------------------- /util/regexp_pre.go: -------------------------------------------------------------------------------- 1 | package pcsutil 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | var ( 8 | // HTTPSRE https regexp 9 | HTTPSRE = regexp.MustCompile("^https") 10 | // ChinaPhoneRE https regexp 11 | ChinaPhoneRE = regexp.MustCompile("^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0,5-9]))\\d{8}$") 12 | ) 13 | -------------------------------------------------------------------------------- /util/tieba_client_signature.go: -------------------------------------------------------------------------------- 1 | package pcsutil 2 | 3 | import ( 4 | "bytes" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | // TiebaClientSignature 根据给定贴吧客户端的 post (post数据指针) 进行签名, 以通过百度服务器验证。返回值为: sign 签名字符串 10 | func TiebaClientSignature(post map[string]string) { 11 | if post == nil { 12 | return 13 | } 14 | // 预设 15 | post["_client_type"] = "2" 16 | post["_client_version"] = "6.9.2.1" 17 | post["_phone_imei"] = "860983036542682" 18 | post["from"] = "mini_ad_wandoujia" 19 | post["model"] = "HUAWEI NXT-AL10" 20 | post["cuid"] = "61464018582906C485355A89D105ECFB|286245630389068" 21 | var keys []string 22 | for key := range post { 23 | keys = append(keys, key) 24 | } 25 | sort.Sort(sort.StringSlice(keys)) 26 | 27 | var bb bytes.Buffer 28 | for _, key := range keys { 29 | bb.WriteString(key + "=" + post[key]) 30 | } 31 | bb.WriteString("tiebaclient!!!") 32 | post["sign"] = Md5Encrypt(bb.Bytes()[:]) 33 | } 34 | 35 | // TiebaClientRawQuerySignature 给 rawQuery 进行贴吧客户端签名 36 | func TiebaClientRawQuerySignature(rawQuery string) (sign string) { 37 | return rawQuery + "&sign=" + Md5Encrypt(strings.Replace(rawQuery, "&", "", -1)+"tiebaclient!!!") 38 | } 39 | -------------------------------------------------------------------------------- /util/time.go: -------------------------------------------------------------------------------- 1 | package pcsutil 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | var ( 9 | // CSTLocation CST Location 10 | CSTLocation = time.FixedZone("CST", 8*3600) 11 | ) 12 | 13 | /* 14 | BeijingTimeOption 根据给定的 get 返回时间格式. 15 | 16 | get: 时间格式 17 | 18 | "Refer": 2017-7-21 12:02:32.000 19 | "printLog": 2017-7-21_12:02:32 20 | "day": 21 21 | "ymd": 2017-7-21 22 | "hour": 12 23 | 默认时间戳: 1500609752 24 | */ 25 | func BeijingTimeOption(get string) string { 26 | //获取北京(东八区)时间 27 | CSTLoc := time.FixedZone("CST", 8*3600) // 东8区 28 | now := time.Now().In(CSTLoc) 29 | year, mon, day := now.Date() 30 | hour, min, sec := now.Clock() 31 | millisecond := now.Nanosecond() / 1e6 32 | switch get { 33 | case "Refer": 34 | return fmt.Sprintf("%d-%d-%d %02d:%02d:%02d.%03d", year, mon, day, hour, min, sec, millisecond) 35 | case "printLog": 36 | return fmt.Sprintf("%d-%d-%d_%02dh%02dm%02ds", year, mon, day, hour, min, sec) 37 | case "day": 38 | return fmt.Sprint(day) 39 | case "ymd": 40 | return fmt.Sprintf("%d-%d-%d", year, mon, day) 41 | case "hour": 42 | return fmt.Sprint(hour) 43 | default: 44 | return fmt.Sprint(time.Now().Unix()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /util/unsafe_strconv_test.go: -------------------------------------------------------------------------------- 1 | // go test -test.bench=".*" 2 | package pcsutil 3 | 4 | import ( 5 | "testing" 6 | ) 7 | 8 | var str = "asddsadfaalkdjsksajdfkashjkdfhashfliuhsadifhasifhaishdfiashdihaisdfhiuassfasdff" 9 | 10 | func BenchmarkToBytes(b *testing.B) { 11 | for i := 0; i <= b.N; i++ { 12 | _ = ToBytes(str) 13 | } 14 | } 15 | 16 | func BenchmarkBytes(b *testing.B) { 17 | for i := 0; i <= b.N; i++ { 18 | _ = []byte(str) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package pcsutil 2 | 3 | import ( 4 | "compress/gzip" 5 | "crypto/md5" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net/http/cookiejar" 11 | "net/url" 12 | "os" 13 | "strings" 14 | "unsafe" 15 | ) 16 | 17 | var ( 18 | // PipeInput 命令中是否为管道输入 19 | PipeInput bool 20 | ) 21 | 22 | func init() { 23 | fileInfo, err := os.Stdin.Stat() 24 | if err != nil { 25 | return 26 | } 27 | PipeInput = (fileInfo.Mode() & os.ModeNamedPipe) == os.ModeNamedPipe 28 | } 29 | 30 | // ToString 将 []byte 转换为 string 31 | func ToString(p []byte) string { 32 | return *(*string)(unsafe.Pointer(&p)) 33 | } 34 | 35 | // ToBytes 将 string 转换为 []byte 36 | func ToBytes(str string) []byte { 37 | return *(*[]byte)(unsafe.Pointer(&str)) 38 | } 39 | 40 | // GetURLCookieString 返回cookie字串 41 | func GetURLCookieString(urlString string, jar *cookiejar.Jar) string { 42 | url, _ := url.Parse(urlString) 43 | cookies := jar.Cookies(url) 44 | cookieString := "" 45 | for _, v := range cookies { 46 | cookieString += v.String() + "; " 47 | } 48 | cookieString = strings.TrimRight(cookieString, "; ") 49 | return cookieString 50 | } 51 | 52 | // Md5Encrypt 对 str 进行md5加密, 返回值为 str 加密后的密文 53 | func Md5Encrypt(str interface{}) string { 54 | md5Ctx := md5.New() 55 | switch value := str.(type) { 56 | case string: 57 | md5Ctx.Write([]byte(str.(string))) 58 | case *string: 59 | md5Ctx.Write([]byte(*str.(*string))) 60 | case []byte: 61 | md5Ctx.Write(str.([]byte)) 62 | case *[]byte: 63 | md5Ctx.Write(*str.(*[]byte)) 64 | default: 65 | fmt.Println("MD5Encrypt: undefined type:", value) 66 | return "" 67 | } 68 | return fmt.Sprintf("%X", md5Ctx.Sum(nil)) 69 | } 70 | 71 | // DecompressGZIP 对 io.Reader 数据, 进行 gzip 解压 72 | func DecompressGZIP(r io.Reader) ([]byte, error) { 73 | gzipReader, err := gzip.NewReader(r) 74 | if err != nil { 75 | return nil, err 76 | } 77 | gzipReader.Close() 78 | return ioutil.ReadAll(gzipReader) 79 | } 80 | 81 | // FlagProvided 检测命令行是否提供名为 name 的 flag, 支持多个name(names) 82 | func FlagProvided(names ...string) bool { 83 | if len(names) == 0 { 84 | return false 85 | } 86 | var targetFlag *flag.Flag 87 | for _, name := range names { 88 | targetFlag = flag.Lookup(name) 89 | if targetFlag == nil { 90 | return false 91 | } 92 | if targetFlag.DefValue == targetFlag.Value.String() { 93 | return false 94 | } 95 | } 96 | return true 97 | } 98 | -------------------------------------------------------------------------------- /util/wait_group.go: -------------------------------------------------------------------------------- 1 | package pcsutil 2 | 3 | import "sync" 4 | 5 | // WaitGroup 在 sync.WaitGroup 的基础上, 新增线程控制功能 6 | type WaitGroup struct { 7 | wg sync.WaitGroup 8 | p chan struct{} 9 | } 10 | 11 | // NewWaitGroup returns a pointer to a new `WaitGroup` object. 12 | // parallel 为最大并发数, 0 代表无限制 13 | func NewWaitGroup(parallel int) (w *WaitGroup) { 14 | w = &WaitGroup{} 15 | if parallel <= 0 { 16 | return 17 | } 18 | w.p = make(chan struct{}, parallel) 19 | return 20 | } 21 | 22 | // AddDelta 在 sync.WaitGroup 的基础上, 新增线程控制功能 23 | func (w *WaitGroup) AddDelta() { 24 | w.wg.Add(1) 25 | if w.p == nil { 26 | return 27 | } 28 | w.p <- struct{}{} 29 | } 30 | 31 | // Done 在 sync.WaitGroup 的基础上, 新增线程控制功能 32 | func (w *WaitGroup) Done() { 33 | w.wg.Done() 34 | if w.p == nil { 35 | return 36 | } 37 | <-w.p 38 | } 39 | 40 | // Wait 参照 sync.WaitGroup 的 Wait 方法 41 | func (w *WaitGroup) Wait() { 42 | w.wg.Wait() 43 | } 44 | 45 | // Parallel 返回当前正在进行的任务数量 46 | func (w *WaitGroup) Parallel() int { 47 | return len(w.p) 48 | } 49 | -------------------------------------------------------------------------------- /util/wait_group_test.go: -------------------------------------------------------------------------------- 1 | package pcsutil 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestWg(t *testing.T) { 10 | wg := NewWaitGroup(2) 11 | for i := 0; i < 60; i++ { 12 | wg.AddDelta() 13 | go func() { 14 | fmt.Println(i, wg.Parallel()) 15 | time.Sleep(1e9) 16 | wg.Done() 17 | }() 18 | } 19 | wg.Wait() 20 | } 21 | --------------------------------------------------------------------------------