Update README.md

This commit is contained in:
qjfoidnh
2020-11-07 02:28:22 +08:00
commit 70fe168f0f
589 changed files with 99853 additions and 0 deletions

14
BaiduPCS-Go.exe.manifest Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="com.iikira.baidupcsgo" type="win32"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

22
Info.plist Normal file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>BaiduPCS-Go</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
<key>CFBundleExecutable</key>
<string>BaiduPCS-Go</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>CFBundleIdentifier</key>
<string>com.iikira.baidupcsgo</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>CFBundleDisplayName</key>
<string>BaiduPCS-Go</string>
</dict>
</plist>

202
LICENSE Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright iikira.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

954
README.md Normal file
View File

@@ -0,0 +1,954 @@
# BaiduPCS-Go-Transfer 百度网盘客户端
仿 Linux shell 文件处理命令的百度网盘命令行客户端.
This project was largely inspired by [GangZhuo/BaiduPCS](https://github.com/GangZhuo/BaiduPCS) and iikira/BaiduPCS-Go
## 注意
此版本基于iikira原版BaiduPCS-Go最新版本(v3.6.2)修改, 添加了转存功能和部分配置建议, 替换了文档中一些失效的链接和页面并更新少量说明.
<!-- toc -->
## 目录
- [特色](#特色)
- [编译/交叉编译 说明](#编译交叉编译-说明)
- [下载/运行 说明](#下载运行-说明)
* [Windows](#windows)
* [Linux / macOS](#linux--macos)
* [Android / iOS](#android--ios)
- [命令列表及说明](#命令列表及说明)
* [注意 ! ! !](#注意---)
* [检测程序更新](#检测程序更新)
* [登录百度帐号](#登录百度帐号)
* [列出帐号列表](#列出帐号列表)
* [获取当前帐号](#获取当前帐号)
* [切换百度帐号](#切换百度帐号)
* [退出百度帐号](#退出百度帐号)
* [获取网盘配额](#获取网盘配额)
* [切换工作目录](#切换工作目录)
* [输出工作目录](#输出工作目录)
* [列出目录](#列出目录)
* [列出目录树形图](#列出目录树形图)
* [获取文件/目录的元信息](#获取文件目录的元信息)
* [搜索文件](#搜索文件)
* [下载文件/目录](#下载文件目录)
* [上传文件/目录](#上传文件目录)
* [获取下载直链](#获取下载直链)
* [手动秒传文件](#手动秒传文件)
* [修复文件MD5](#修复文件MD5)
* [获取本地文件的秒传信息](#获取本地文件的秒传信息)
* [导出文件/目录](#导出文件目录)
* [创建目录](#创建目录)
* [删除文件/目录](#删除文件目录)
* [拷贝文件/目录](#拷贝文件目录)
* [移动/重命名文件/目录](#移动重命名文件目录)
* [转存文件/目录](#转存文件目录)
* [分享文件/目录](#分享文件目录)
+ [设置分享文件/目录](#设置分享文件目录)
+ [列出已分享文件/目录](#列出已分享文件目录)
+ [取消分享文件/目录](#取消分享文件目录)
* [离线下载](#离线下载)
+ [添加离线下载任务](#添加离线下载任务)
+ [精确查询离线下载任务](#精确查询离线下载任务)
+ [查询离线下载任务列表](#查询离线下载任务列表)
+ [取消离线下载任务](#取消离线下载任务)
+ [删除离线下载任务](#删除离线下载任务)
* [回收站](#回收站)
+ [列出回收站文件列表](#列出回收站文件列表)
+ [还原回收站文件或目录](#还原回收站文件或目录)
+ [删除回收站文件或目录/清空回收站](#删除回收站文件或目录清空回收站)
* [显示和修改程序配置项](#显示和修改程序配置项)
* [测试通配符](#测试通配符)
* [工具箱](#工具箱)
- [初级使用教程](#初级使用教程)
* [1. 查看程序使用说明](#1-查看程序使用说明)
* [2. 登录百度帐号 (必做)](#2-登录百度帐号-必做)
* [3. 切换网盘工作目录](#3-切换网盘工作目录)
* [4. 网盘内列出文件和目录](#4-网盘内列出文件和目录)
* [5. 下载文件](#5-下载文件)
* [6. 设置下载最大并发量](#6-设置下载最大并发量)
* [7. 退出程序](#7-退出程序)
- [已知问题](#已知问题)
- [TODO](#todo)
- [交流反馈](#交流反馈)
<!-- tocstop -->
# 特色
多平台支持, 支持 Windows, macOS, linux, 移动设备等.
百度帐号多用户支持;
通配符匹配网盘路径和 Tab 自动补齐命令和路径, [通配符_百度百科](https://baike.baidu.com/item/通配符);
[下载](#下载文件目录)网盘内文件, 支持多个文件或目录下载, 支持断点续传和单文件并行下载;
[上传](#上传文件目录)本地文件, 支持上传大文件(>2GB), 支持多个文件或目录上传;
[转存](#转存文件目录)其他用户分享的文件, 支持公开、带密码的分享链接, 支持常见的几种秒传链接;
[离线下载](#离线下载), 支持http/https/ftp/电驴/磁力链协议.
# 编译/交叉编译 说明
设置好 GOOS 和 GOARCH 环境变量,
运行 go tool dist list 查看所有支持的 GOOS/GOARCH
## Linux/Darwin 例子: 编译 Windows 下的 64 位程序
```
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build
```
## Windows 例子: 编译 Linux 下的 32 位程序
```
set GOOS=linux
set GOARCH=386
set CGO_ENABLED=0
go build
```
# 下载/运行 说明
Go语言程序, 常用几种平台的已编译程序可直接在[蓝奏云](https://ww.lanzous.com/b01berebe)下载使用. 密码:4pix
如果程序运行时输出乱码, 请检查下终端的编码方式是否为 `UTF-8`.
使用本程序之前, 建议学习一些 linux 基础知识 和 基础命令.
如果未带任何参数运行程序, 程序将会进入仿Linux shell系统用户界面的cli交互模式, 可直接运行相关命令.
cli交互模式下, 光标所在行的前缀应为 `BaiduPCS-Go >`, 如果登录了百度帐号则格式为 `BaiduPCS-Go:<工作目录> <百度ID>$ `
程序会提供相关命令的使用说明.
## Windows
程序应在 命令提示符 (Command Prompt) 或 PowerShell 中运行, 在 mintty (例如: GitBash) 可能会有显示问题.
也可直接双击程序运行, 具体使用方法请参见 [命令列表及说明](#命令列表及说明) 和 [初级使用教程](#初级使用教程).
## Linux / macOS
程序应在 终端 (Terminal) 运行.
具体使用方法请参见 [命令列表及说明](#命令列表及说明) 和 [初级使用教程](#初级使用教程).
## Android / iOS
> Android / iOS 移动设备操作比较麻烦, 不建议在移动设备上使用本程序.
安卓, 建议使用 [Termux](https://termux.com) 或 [NeoTerm](https://github.com/NeoTerm/NeoTerm) 或 终端模拟器, 以提供终端环境.
示例: [Android 运行本项目程序参考示例](https://web.archive.org/web/20190820154934/https://github.com/iikira/BaiduPCS-Go/wiki/Android-%E8%BF%90%E8%A1%8C%E6%9C%AC%E9%A1%B9%E7%9B%AE%E7%A8%8B%E5%BA%8F%E5%8F%82%E8%80%83%E7%A4%BA%E4%BE%8B), 有兴趣的可以参考一下.
苹果iOS, 需要越狱, 在 Cydia 搜索下载并安装 MobileTerminal, 或者其他提供终端环境的软件.
示例: [iOS 运行本项目程序参考示例](https://web.archive.org/web/20190820155025/https://github.com/iikira/BaiduPCS-Go/wiki/iOS-%E8%BF%90%E8%A1%8C%E6%9C%AC%E9%A1%B9%E7%9B%AE%E7%A8%8B%E5%BA%8F%E5%8F%82%E8%80%83%E7%A4%BA%E4%BE%8B), 有兴趣的可以参考一下.
具体使用方法请参见 [命令列表及说明](#命令列表及说明) 和 [初级使用教程](#初级使用教程).
# 命令列表及说明
## 注意 ! ! !
命令的前缀 `BaiduPCS-Go` 为指向程序运行的全路径名 (ARGv 的第一个参数)
直接运行程序时, 未带任何其他参数, 则程序进入cli交互模式, 运行以下命令时, 要把命令的前缀 `BaiduPCS-Go` 去掉!
cli交互模式已支持按tab键自动补全命令和路径.
## 检测程序更新
```
BaiduPCS-Go update
```
## 登录百度帐号
### 常规登录百度帐号
支持在线验证绑定的手机号或邮箱,
```
BaiduPCS-Go login
```
### 使用百度 BDUSS 来登录百度帐号
[关于 获取百度 BDUSS](https://blog.csdn.net/ykiwmy/article/details/103730962)
```
BaiduPCS-Go login -bduss=<BDUSS>
```
### 使用百度 Cookies 来登录百度账号(唯一可使用转存功能的登录方式)
[关于 获取百度 Cookies](https://jingyan.baidu.com/article/5553fa829a6a9e65a23934b0.html)
教程中为百度经验的Cookies获取, 这里换成百度网盘首页即可.
```
BaiduPCS-Go login -cookies=<Cookies>
```
#### 例子
```
BaiduPCS-Go login -bduss=1234567
```
```
BaiduPCS-Go login
请输入百度用户名(手机号/邮箱/用户名), 回车键提交 > 1234567
```
```
BaiduPCS-Go login -cookies="BAIDUID=50949C0890YG9735EA6Q3870AFE38:FG=1; BIDUPSID=112335C0ACCAFFJW675EA69A870AFE38; PSTM=1981928511; BDORZ=D6745EBF6F3SW24E515D22A1598; PANWEB=1; BDUSS=ASAYUGFHSTFKGBGSU; STOKEN=gfsdge9gisfgspig34254d7879eee5756b10sgeyrw5vyw342td510ffc9414d32251; SCRC=cwrywec5evyetra26bvvehefvfg6a8; BDCLND=C%4sfgGysrZ%2BML6; PANPSC=wreyewygdfhdggedhsdfg4353"
```
## 列出帐号列表
```
BaiduPCS-Go loglist
```
列出所有已登录的百度帐号
## 获取当前帐号
```
BaiduPCS-Go who
```
## 切换百度帐号
切换已登录的百度帐号
```
BaiduPCS-Go su <uid>
```
```
BaiduPCS-Go su
请输入要切换帐号的 # 值 >
```
## 退出百度帐号
退出当前登录的百度帐号
```
BaiduPCS-Go logout
```
程序会进一步确认退出帐号, 防止误操作.
## 获取网盘配额
```
BaiduPCS-Go quota
```
获取网盘的总储存空间, 和已使用的储存空间
## 切换工作目录
```
BaiduPCS-Go cd <目录>
```
### 切换工作目录后自动列出工作目录下的文件和目录
```
BaiduPCS-Go cd -l <目录>
```
#### 例子
```
# 切换 /我的资源 工作目录
BaiduPCS-Go cd /我的资源
# 切换 上级目录
BaiduPCS-Go cd ..
# 切换 根目录
BaiduPCS-Go cd /
# 切换 /我的资源 工作目录, 并自动列出 /我的资源 下的文件和目录
BaiduPCS-Go cd -l 我的资源
# 使用通配符
BaiduPCS-Go cd /我的*
```
## 输出工作目录
```
BaiduPCS-Go pwd
```
## 列出目录
列出当前工作目录的文件和目录或指定目录
```
BaiduPCS-Go ls
```
```
BaiduPCS-Go ls <目录>
```
### 可选参数
```
-asc: 升序排序
-desc: 降序排序
-time: 根据时间排序
-name: 根据文件名排序
-size: 根据大小排序
```
#### 例子
```
# 列出 我的资源 内的文件和目录
BaiduPCS-Go ls 我的资源
# 绝对路径
BaiduPCS-Go ls /我的资源
# 降序排序
BaiduPCS-Go ls -desc 我的资源
# 按文件大小降序排序
BaiduPCS-Go ls -size -desc 我的资源
# 使用通配符
BaiduPCS-Go ls /我的*
```
## 列出目录树形图
列出当前工作目录的文件和目录或指定目录的树形图
```
BaiduPCS-Go tree <目录>
# 默认获取工作目录元信息
BaiduPCS-Go tree
```
## 获取文件/目录的元信息
```
BaiduPCS-Go meta <文件/目录1> <文件/目录2> <文件/目录3> ...
# 默认获取工作目录元信息
BaiduPCS-Go meta
```
#### 例子
```
BaiduPCS-Go meta 我的资源
BaiduPCS-Go meta /
```
## 搜索文件
按文件名搜索文件(不支持查找目录)。
默认在当前工作目录搜索.
```
BaiduPCS-Go search [-path=<需要检索的目录>] [-r] <关键字>
```
#### 例子
```
# 搜索根目录的文件
BaiduPCS-Go search -path=/ 关键字
# 搜索当前工作目录的文件
BaiduPCS-Go search 关键字
# 递归搜索当前工作目录的文件
BaiduPCS-Go search -r 关键字
```
## 下载文件/目录
```
BaiduPCS-Go download <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
BaiduPCS-Go d <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
```
### 可选参数
```
--test 测试下载, 此操作不会保存文件到本地
--ow overwrite, 覆盖已存在的文件
--status 输出所有线程的工作状态
--save 将下载的文件直接保存到当前工作目录
--saveto value 将下载的文件直接保存到指定的目录
-x 为文件加上执行权限, (windows系统无效)
--mode value 下载模式, 可选值: pcs, stream, locate, 默认为 locate, 相关说明见上面的帮助 (default: "locate")
-p value 指定下载线程数 (default: 0)
-l value 指定同时进行下载文件的数量 (default: 0)
--retry value 下载失败最大重试次数 (default: 3)
--nocheck 下载文件完成后不校验文件
```
下载的文件默认保存到 **程序所在目录** 的 download/ 目录, 支持设置指定目录, 重名的文件会自动跳过!
下载的文件默认保存到, **程序所在目录**的 **download/** 目录.
通过 `BaiduPCS-Go config set -savedir <savedir>`, 自定义保存的目录.
支持多个文件或目录下载.
支持下载完成后自动校验文件, 但并不是所有的文件都支持校验!
自动跳过下载重名的文件!
#### 下载模式说明
* pcs: 通过百度网盘的 PCS API 下载(已废弃)
* stream: 通过百度网盘的 PCS API, 以流式文件的方式下载, 效果同 pcs(已废弃)
* locate: 默认的下载模式。从百度网盘 Android 客户端, 获取下载链接的方式来下载
#### 例子
```
# 设置保存目录, 保存到 D:\Downloads
# 注意区别反斜杠 "\" 和 斜杠 "/" !!!
BaiduPCS-Go config set -savedir D:/Downloads
# 下载 /我的资源/1.mp4
BaiduPCS-Go d /我的资源/1.mp4
# 下载 /我的资源 整个目录!!
BaiduPCS-Go d /我的资源
# 下载网盘内的全部文件!!
BaiduPCS-Go d /
BaiduPCS-Go d *
```
## 上传文件/目录
```
BaiduPCS-Go upload <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>
BaiduPCS-Go u <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>
```
* 上传默认采用分片上传的方式, 上传的文件将会保存到, <目标目录>.
* 遇到同名文件将会自动覆盖!!
* 当上传的文件名和网盘的目录名称相同时, 不会覆盖目录, 防止丢失数据.
#### 注意:
* 分片上传之后, 服务器可能会记录到错误的文件md5, 可使用 fixmd5 命令尝试修复文件的MD5值, 修复md5不一定能成功, 但文件的完整性是没问题的.
fixmd5 命令使用方法:
```
BaiduPCS-Go fixmd5 -h
```
* 禁用分片上传可以保证服务器记录到正确的md5.
* 禁用分片上传时只能使用单线程上传, 指定的单个文件上传最大线程数将会无效.
#### 例子:
```
# 将本地的 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录
# 注意区别反斜杠 "\" 和 斜杠 "/" !!!
BaiduPCS-Go upload C:/Users/Administrator/Desktop/1.mp4 /视频
# 将本地的 C:\Users\Administrator\Desktop\1.mp4 和 C:\Users\Administrator\Desktop\2.mp4 上传到网盘 /视频 目录
BaiduPCS-Go upload C:/Users/Administrator/Desktop/1.mp4 C:/Users/Administrator/Desktop/2.mp4 /视频
# 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录
BaiduPCS-Go upload C:/Users/Administrator/Desktop /视频
```
## 获取下载直链
```
BaiduPCS-Go locate <文件1> <文件2> ...
```
#### 注意
若该功能无法正常使用, 提示`user is not authorized, hitcode:xxx`, 尝试更换 User-Agent 为 `netdisk;2.2.51.6;netdisk;10.0.63;PC;android-android`:
```
BaiduPCS-Go config set -user_agent "netdisk;2.2.51.6;netdisk;10.0.63;PC;android-android"
```
## 手动秒传文件
```
BaiduPCS-Go rapidupload -length=<文件的大小> -md5=<文件的md5值> -slicemd5=<文件前256KB切片的md5值(可选)> -crc32=<文件的crc32值(可选)> <保存的网盘路径, 需包含文件名>
BaiduPCS-Go ru -length=<文件的大小> -md5=<文件的md5值> -slicemd5=<文件前256KB切片的md5值(可选)> -crc32=<文件的crc32值(可选)> <保存的网盘路径, 需包含文件名>
```
注意: 使用此功能秒传文件, 前提是知道文件的大小, md5, 前256KB切片的 md5 (可选), crc32 (可选), 且百度网盘中存在一模一样的文件.
上传的文件将会保存到网盘的目标目录.
遇到同名文件将会自动覆盖!
可能无法秒传 20GB 以上的文件!!
#### 例子:
```
# 如果秒传成功, 则保存到网盘路径 /test
BaiduPCS-Go rapidupload -length=56276137 -md5=fbe082d80e90f90f0fb1f94adbbcfa7f -slicemd5=38c6a75b0ec4499271d4ea38a667ab61 -crc32=314332359 /test
```
## 修复文件MD5
```
BaiduPCS-Go fixmd5 <文件1> <文件2> <文件3> ...
```
尝试修复文件的MD5值, 以便于校验文件的完整性和导出文件.
使用分片上传文件, 当文件分片数大于1时, 百度网盘服务端最终计算所得的md5值和本地的不一致, 这可能是百度网盘的bug.
不过把上传的文件下载到本地后对比md5值是匹配的, 也就是文件在传输中没有发生损坏.
对于MD5值可能有误的文件, 程序会在获取文件的元信息时, 给出MD5值 "可能不正确" 的提示, 表示此文件可以尝试进行MD5值修复.
修复文件MD5不一定能成功, 原因可能是服务器未刷新, 可过几天后再尝试.
修复文件MD5的原理为秒传文件, 即修复文件MD5成功后, 文件的**创建日期, 修改日期, fs_id, 版本历史等信息**将会被覆盖, 修复的MD5值将覆盖原先的MD5值, 但不影响文件的完整性.
注意: 无法修复 **20GB** 以上文件的 md5!!
#### 例子:
```
# 修复 /我的资源/1.mp4 的 MD5 值
BaiduPCS-Go fixmd5 /我的资源/1.mp4
```
## 获取本地文件的秒传信息
```
BaiduPCS-Go sumfile <本地文件的路径>
BaiduPCS-Go sf <本地文件的路径>
```
获取本地文件的大小, md5, 前256KB切片的 md5, crc32, 可用于秒传文件.
#### 例子:
```
# 获取 C:\Users\Administrator\Desktop\1.mp4 的秒传信息
BaiduPCS-Go sumfile C:/Users/Administrator/Desktop/1.mp4
```
## 导出文件/目录
```
BaiduPCS-Go export <文件/目录1> <文件/目录2> ...
BaiduPCS-Go ep <文件/目录1> <文件/目录2> ...
```
导出网盘内的文件或目录, 原理为秒传文件, 此操作会生成导出文件或目录的命令.
#### 注意
**无法导出 20GB 以上的文件!!**
**无法导出文件的版本历史等数据!!**
**以通用秒传格式导出会丢失文件路径信息!!**
并不是所有的文件都能导出成功, 程序会列出无法导出的文件列表
#### 例子:
```
# 导出当前工作目录:
BaiduPCS-Go export
# 导出所有文件和目录, 并设置新的根目录为 /root
BaiduPCS-Go export -root=/root /
# 导出 /我的资源
BaiduPCS-Go export /我的资源
# 导出 /我的资源 格式为通用秒传链接格式
BaiduPCS-Go export /我的资源 --link
```
## 创建目录
```
BaiduPCS-Go mkdir <目录>
```
#### 例子
```
BaiduPCS-Go mkdir 123
```
## 删除文件/目录
```
BaiduPCS-Go rm <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
```
注意: 删除多个文件和目录时, 请确保每一个文件和目录都存在, 否则删除操作会失败.
被删除的文件或目录可在网盘文件回收站找回.
#### 例子
```
# 删除 /我的资源/1.mp4
BaiduPCS-Go rm /我的资源/1.mp4
# 删除 /我的资源/1.mp4 和 /我的资源/2.mp4
BaiduPCS-Go rm /我的资源/1.mp4 /我的资源/2.mp4
# 删除 /我的资源 内的所有文件和目录, 但不删除该目录
BaiduPCS-Go rm /我的资源/*
# 删除 /我的资源 整个目录 !!
BaiduPCS-Go rm /我的资源
```
## 拷贝文件/目录
```
BaiduPCS-Go cp <文件/目录> <目标 文件/目录>
BaiduPCS-Go cp <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>
```
注意: 拷贝多个文件和目录时, 请确保每一个文件和目录都存在, 否则拷贝操作会失败.
#### 例子
```
# 将 /我的资源/1.mp4 复制到 根目录 /
BaiduPCS-Go cp /我的资源/1.mp4 /
# 将 /我的资源/1.mp4 和 /我的资源/2.mp4 复制到 根目录 /
BaiduPCS-Go cp /我的资源/1.mp4 /我的资源/2.mp4 /
```
## 移动/重命名文件/目录
```
# 移动:
BaiduPCS-Go mv <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>
# 重命名:
BaiduPCS-Go mv <文件/目录> <重命名的文件/目录>
```
注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败.
#### 例子
```
# 将 /我的资源/1.mp4 移动到 根目录 /
BaiduPCS-Go mv /我的资源/1.mp4 /
# 将 /我的资源/1.mp4 重命名为 /我的资源/3.mp4
BaiduPCS-Go mv /我的资源/1.mp4 /我的资源/3.mp4
```
## 转存文件/目录
```
# 转存分享链接里的文件到当前目录:
BaiduPCS-Go transfer <分享链接> <提取码>
# 转存通用秒传链接里的文件到当前目录:
BaiduPCS-Go transfer <秒传链接>
```
注意: 公开分享链接不需输入提取码, 支持多个文件/目录; 只支持包含单个文件的秒传链接.
转存文件保存到当前工作目录下, 不支持指定. 欲使用此功能登录时需要使用cookies方式.
#### 例子
```
# 将 https://pan.baidu.com/s/12L_ZZVNxz5f_2CccoyyVrW (提取码edv4) 转存到当前目录
BaiduPCS-Go transfer https://pan.baidu.com/s/12L_ZZVNxz5f_2CccoyyVrW edv4
# 将 E7E7B8613854379642F70230B179F37A#FA690D0AB7C8BC6A62WD1B6B3FC5248F#128859362#test.7z 转存到当前目录
BaiduPCS-Go transfer E7E7B8613854379642F70230B179F37A#FA690D0AB7C8BC6A62WD1B6B3FC5248F#128859362#test.7z
```
## 分享文件/目录
```
BaiduPCS-Go share
```
### 设置分享文件/目录
```
BaiduPCS-Go share set <文件/目录1> <文件/目录2> ...
BaiduPCS-Go share s <文件/目录1> <文件/目录2> ...
```
### 列出已分享文件/目录
```
BaiduPCS-Go share list
BaiduPCS-Go share l
```
### 取消分享文件/目录
```
BaiduPCS-Go share cancel <shareid_1> <shareid_2> ...
BaiduPCS-Go share c <shareid_1> <shareid_2> ...
```
目前只支持通过分享id (shareid) 来取消分享.
## 离线下载
```
BaiduPCS-Go offlinedl
BaiduPCS-Go clouddl
BaiduPCS-Go od
```
离线下载支持http/https/ftp/电驴/磁力链协议
离线下载同时进行的任务数量有限, 超出限制的部分将无法添加.
### 添加离线下载任务
```
BaiduPCS-Go offlinedl add -path=<离线下载文件保存的路径> 资源地址1 地址2 ...
```
添加任务成功之后, 返回离线下载的任务ID.
### 精确查询离线下载任务
```
BaiduPCS-Go offlinedl query 任务ID1 任务ID2 ...
```
### 查询离线下载任务列表
```
BaiduPCS-Go offlinedl list
```
### 取消离线下载任务
```
BaiduPCS-Go offlinedl cancel 任务ID1 任务ID2 ...
```
### 删除离线下载任务
```
BaiduPCS-Go offlinedl delete 任务ID1 任务ID2 ...
# 清空离线下载任务记录, 程序不会进行二次确认, 谨慎操作!!!
BaiduPCS-Go offlinedl delete -all
```
#### 例子
```
# 将百度和腾讯主页, 离线下载到根目录 /
BaiduPCS-Go offlinedl add -path=/ http://baidu.com http://qq.com
# 添加磁力链接任务
BaiduPCS-Go offlinedl add magnet:?xt=urn:btih:xxx
# 查询任务ID为 12345 的离线下载任务状态
BaiduPCS-Go offlinedl query 12345
# 取消任务ID为 12345 的离线下载任务
BaiduPCS-Go offlinedl cancel 12345
```
## 回收站
```
BaiduPCS-Go recycle
```
回收站操作.
### 列出回收站文件列表
```
BaiduPCS-Go recycle list
```
#### 可选参数
```
--page value 回收站文件列表页数 (default: 1)
```
### 还原回收站文件或目录
```
BaiduPCS-Go recycle restore <fs_id 1> <fs_id 2> <fs_id 3> ...
```
根据文件/目录的 fs_id, 还原回收站指定的文件或目录.
### 删除回收站文件或目录/清空回收站
```
BaiduPCS-Go recycle delete [-all] <fs_id 1> <fs_id 2> <fs_id 3> ...
```
根据文件/目录的 fs_id 或 -all 参数, 删除回收站指定的文件或目录或清空回收站.
#### 例子
```
# 从回收站还原两个文件, 其中的两个文件的 fs_id 分别为 1013792297798440 和 643596340463870
BaiduPCS-Go recycle restore 1013792297798440 643596340463870
# 从回收站删除两个文件, 其中的两个文件的 fs_id 分别为 1013792297798440 和 643596340463870
BaiduPCS-Go recycle delete 1013792297798440 643596340463870
# 清空回收站, 程序不会进行二次确认, 谨慎操作!!!
BaiduPCS-Go recycle delete -all
```
## 显示程序环境变量
```
BaiduPCS-Go env
```
BAIDUPCS_GO_CONFIG_DIR: 配置文件路径,
BAIDUPCS_GO_VERBOSE: 是否启用调试.
## 显示和修改程序配置项
```
# 显示配置
BaiduPCS-Go config
# 设置配置
BaiduPCS-Go config set
```
注意: v3.5 以后, 程序对配置文件储存路径的寻找做了调整, 配置文件所在的目录可以是程序本身所在目录, 也可以是家目录.
配置文件所在的目录为家目录的情况:
Windows: `%APPDATA%\BaiduPCS-Go`
其他操作系统: `$HOME/.config/BaiduPCS-Go`
可通过设置环境变量 `BAIDUPCS_GO_CONFIG_DIR`, 指定配置文件存放的目录.
谨慎修改 `appid`, `user_agent`, `pcs_ua`, `pan_ua` 的值, 否则访问网盘服务器时, 可能会出现错误.
`cache_size` 的值支持可选设置单位了, 单位不区分大小写, `b``B` 均表示字节的意思, 如 `64KB`, `1MB`, `32kb`, `65536b`, `65536`.
`max_upload_parallel`, `max_download_load` 的值支持可选设置单位了, 单位为每秒的传输速率, 后缀`/s` 可省略, 如 `2MB/s`, `2MB`, `2m`, `2mb` 均为一个意思.
#### 例子
```
# 显示所有可以设置的值
BaiduPCS-Go config -h
BaiduPCS-Go config set -h
# 设置下载文件的储存目录
BaiduPCS-Go config set -savedir D:/Downloads
# 设置下载最大并发量为 150
BaiduPCS-Go config set -max_parallel 150
# 组合设置
BaiduPCS-Go config set -max_parallel 150 -savedir D:/Downloads
```
## 测试通配符
```
BaiduPCS-Go match <通配符表达式>
```
测试通配符匹配路径, 操作成功则输出所有匹配到的路径.
#### 例子
```
# 匹配 /我的资源 目录下所有mp4格式的文件
BaiduPCS-Go match /我的资源/*.mp4
```
## 工具箱
```
BaiduPCS-Go tool
```
目前工具箱支持加解密文件等.
# 初级使用教程
新手建议: **双击运行程序**, 进入仿 Linux shell 的 cli 交互模式;
cli交互模式下, 光标所在行的前缀应为 `BaiduPCS-Go >`, 如果登录了百度帐号则格式为 `BaiduPCS-Go:<工作目录> <百度ID>$ `
以下例子的命令, 均为 cli交互模式下的命令
运行命令的正确操作: **输入命令, 按一下回车键 (键盘上的 Enter 键)**, 程序会接收到命令并输出结果
## 1. 查看程序使用说明
cli交互模式下, 运行命令 `help`
## 2. 登录百度帐号 (必做)
cli交互模式下, 运行命令 `login -h` (注意空格) 查看帮助
cli交互模式下, 运行命令 `login` 程序将会提示你输入百度用户名(手机号/邮箱/用户名)和密码, 必要时还可以在线验证绑定的手机号或邮箱
## 3. 切换网盘工作目录
cli交互模式下, 运行命令 `cd /我的资源` 将工作目录切换为 `/我的资源` (前提: 该目录存在于网盘)
目录支持通配符匹配, 所以你也可以这样: 运行命令 `cd /我的*``cd /我的??` 将工作目录切换为 `/我的资源`, 简化输入.
将工作目录切换为 `/我的资源` 成功后, 运行命令 `cd ..` 切换上级目录, 即将工作目录切换为 `/`
为什么要这样设计呢, 举个例子,
假设 你要下载 `/我的资源` 内名为 `1.mp4``2.mp4` 两个文件, 而未切换工作目录, 你需要依次运行以下命令:
```
d /我的资源/1.mp4
d /我的资源/2.mp4
```
而切换网盘工作目录之后, 依次运行以下命令:
```
cd /我的资源
d 1.mp4
d 2.mp4
```
这样就达到了简化输入的目的
## 4. 网盘内列出文件和目录
cli交互模式下, 运行命令 `ls -h` (注意空格) 查看帮助
cli交互模式下, 运行命令 `ls` 来列出当前所在目录的文件和目录
cli交互模式下, 运行命令 `ls /我的资源` 来列出 `/我的资源` 内的文件和目录
cli交互模式下, 运行命令 `ls ..` 来列出当前所在目录的上级目录的文件和目录
## 5. 下载文件
说明: 下载的文件默认保存到 download/ 目录 (文件夹)
cli交互模式下, 运行命令 `d -h` (注意空格) 查看帮助
cli交互模式下, 运行命令 `d /我的资源/1.mp4` 来下载位于 `/我的资源/1.mp4` 的文件 `1.mp4` , 该操作等效于运行以下命令:
```
cd /我的资源
d 1.mp4
```
现在已经支持目录 (文件夹) 下载, 所以, 运行以下命令, 会下载 `/我的资源` 内的所有文件 (违规文件除外):
```
d /我的资源
```
## 6. 设置下载最大并发量
cli交互模式下, 运行命令 `config set -h` (注意空格) 查看设置帮助以及可供设置的值
cli交互模式下, 运行命令 `config set -max_parallel 2` 将下载最大并发量设置为 2
注意:下载最大并发量的值不易设置过高, 可能会导致百度帐号被限制下载
## 7. 退出程序
运行命令 `quit``exit` 或 组合键 `Ctrl+C` 或 组合键 `Ctrl+D`
# 已知问题
* 分片上传文件时, 当文件分片数大于1, 网盘端最终计算所得的md5值和本地的不一致, 这可能是百度网盘的bug, 测试把上传的文件下载到本地后对比md5值是匹配的. 可通过秒传的原理来修复md5值.
* 下载文件时可能会check MD5 不通过, 但文件其实并未出错, 使用--no-check下载即可.
* 下载线程, 即max_parallel参数设置过大时可能导致账号进入黑名单状态, 建议普通用户设置为1, 超级会员可尝试调大, 但不建议超过5.
# TODO
# 交流反馈
提交Issue: [Issues](https://github.com/qjfoidnh/BaiduPCS-Go-Transfer/issues)

BIN
assets/BaiduPCS-Go.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
assets/caution.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

368
baidupcs/baidupcs.go Normal file
View File

@@ -0,0 +1,368 @@
// Package baidupcs BaiduPCS RESTful API 工具包
package baidupcs
import (
"errors"
"net/http"
"net/http/cookiejar"
"net/url"
"strconv"
"github.com/iikira/BaiduPCS-Go/baidupcs/expires/cachemap"
"github.com/iikira/BaiduPCS-Go/baidupcs/internal/panhome"
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"github.com/iikira/BaiduPCS-Go/pcsverbose"
"github.com/iikira/BaiduPCS-Go/requester"
)
const (
// OperationGetUK 获取UK
OperationGetUK = "获取UK"
// OperationQuotaInfo 获取当前用户空间配额信息
OperationQuotaInfo = "获取当前用户空间配额信息"
// OperationFilesDirectoriesMeta 获取文件/目录的元信息
OperationFilesDirectoriesMeta = "获取文件/目录的元信息"
// OperationFilesDirectoriesList 获取目录下的文件列表
OperationFilesDirectoriesList = "获取目录下的文件列表"
// OperationSearch 搜索
OperationSearch = "搜索"
// OperationRemove 删除文件/目录
OperationRemove = "删除文件/目录"
// OperationMkdir 创建目录
OperationMkdir = "创建目录"
// OperationRename 重命名文件/目录
OperationRename = "重命名文件/目录"
// OperationCopy 拷贝文件/目录
OperationCopy = "拷贝文件/目录"
// OperationMove 移动文件/目录
OperationMove = "移动文件/目录"
// OperationRapidUpload 秒传文件
OperationRapidUpload = "秒传文件"
// OperationUpload 上传单个文件
OperationUpload = "上传单个文件"
// OperationUploadTmpFile 分片上传—文件分片及上传
OperationUploadTmpFile = "分片上传—文件分片及上传"
// OperationUploadCreateSuperFile 分片上传—合并分片文件
OperationUploadCreateSuperFile = "分片上传—合并分片文件"
// OperationUploadPrecreate 分片上传—Precreate
OperationUploadPrecreate = "分片上传—Precreate"
// OperationUploadSuperfile2 分片上传—Superfile2
OperationUploadSuperfile2 = "分片上传—Superfile2"
// OperationDownloadFile 下载单个文件
OperationDownloadFile = "下载单个文件"
// OperationDownloadStreamFile 下载流式文件
OperationDownloadStreamFile = "下载流式文件"
// OperationLocateDownload 获取下载链接
OperationLocateDownload = "获取下载链接"
// OperationLocatePanAPIDownload 从百度网盘首页获取下载链接
OperationLocatePanAPIDownload = "获取下载链接2"
// OperationCloudDlAddTask 添加离线下载任务
OperationCloudDlAddTask = "添加离线下载任务"
// OperationCloudDlQueryTask 精确查询离线下载任务
OperationCloudDlQueryTask = "精确查询离线下载任务"
// OperationCloudDlListTask 查询离线下载任务列表
OperationCloudDlListTask = "查询离线下载任务列表"
// OperationCloudDlCancelTask 取消离线下载任务
OperationCloudDlCancelTask = "取消离线下载任务"
// OperationCloudDlDeleteTask 删除离线下载任务
OperationCloudDlDeleteTask = "删除离线下载任务"
// OperationCloudDlClearTask 清空离线下载任务记录
OperationCloudDlClearTask = "清空离线下载任务记录"
// OperationShareSet 创建分享链接
OperationShareSet = "创建分享链接"
// OperationShareCancel 取消分享
OperationShareCancel = "取消分享"
// OperationShareList 列出分享列表
OperationShareList = "列出分享列表"
// OperationShareSURLInfo 获取分享详细信息
OperationShareSURLInfo = "获取分享详细信息"
// OperationShareFileSavetoLocal 用分享链接转存到网盘
OperationShareFileSavetoLocal = "用分享链接转存到网盘"
// OperationRapidLinkSavetoLocal 用秒传链接转存到网盘
OperationRapidLinkSavetoLocal = "用秒传链接转存到网盘"
// OperationRecycleList 列出回收站文件列表
OperationRecycleList = "列出回收站文件列表"
// OperationRecycleRestore 还原回收站文件或目录
OperationRecycleRestore = "还原回收站文件或目录"
// OperationRecycleDelete 删除回收站文件或目录
OperationRecycleDelete = "删除回收站文件或目录"
// OperationRecycleClear 清空回收站
OperationRecycleClear = "清空回收站"
// OperationExportFileInfo 导出文件信息
OperationExportFileInfo = "导出文件信息"
// OperationGetRapidUploadInfo 获取文件秒传信息
OperationGetRapidUploadInfo = "获取文件秒传信息"
// OperationFixMD5 修复文件md5
OperationFixMD5 = "修复文件md5"
// OperrationMatchPathByShellPattern 通配符匹配文件路径
OperrationMatchPathByShellPattern = "通配符匹配文件路径"
// PCSBaiduCom pcs api地址
PCSBaiduCom = "pcs.baidu.com"
// PanBaiduCom 网盘首页api地址
PanBaiduCom = "pan.baidu.com"
// YunBaiduCom 网盘首页api地址2
YunBaiduCom = "yun.baidu.com"
// PanAppID 百度网盘appid
PanAppID = "250528"
// NetdiskUA 网盘客户端ua
NetdiskUA = "netdisk;2.2.51.6;netdisk;10.0.63;PC;android-android"
// DotBaiduCom .baidu.com
DotBaiduCom = ".baidu.com"
// PathSeparator 路径分隔符
PathSeparator = "/"
)
var (
baiduPCSVerbose = pcsverbose.New("BAIDUPCS")
baiduComURL = &url.URL{
Scheme: "http",
Host: "baidu.com",
}
baiduPcsComURL = &url.URL{
Scheme: "http",
Host: "baidupcs.com",
}
)
type (
// BaiduPCS 百度 PCS API 详情
BaiduPCS struct {
appID int // app_id
isHTTPS bool // 是否启用https
uid uint64 // 百度uid
client *requester.HTTPClient // http 客户端
pcsUA string
panUA string
isSetPanUA bool
ph *panhome.PanHome
cacheOpMap cachemap.CacheOpMap
}
userInfoJSON struct {
*pcserror.PanErrorInfo
Records []struct {
Uk int64 `json:"uk"`
} `json:"records"`
}
)
// NewPCS 提供app_id, 百度BDUSS, 返回 BaiduPCS 对象
func NewPCS(appID int, bduss string) *BaiduPCS {
client := requester.NewHTTPClient()
client.ResetCookiejar()
client.Jar.SetCookies(baiduComURL, []*http.Cookie{
&http.Cookie{
Name: "BDUSS",
Value: bduss,
Domain: DotBaiduCom,
},
})
return &BaiduPCS{
appID: appID,
client: client,
}
}
// NewPCSWithClient 提供app_id, 自定义客户端, 返回 BaiduPCS 对象
func NewPCSWithClient(appID int, client *requester.HTTPClient) *BaiduPCS {
pcs := &BaiduPCS{
appID: appID,
client: client,
}
return pcs
}
// NewPCSWithCookieStr 提供app_id, cookie 字符串, 返回 BaiduPCS 对象
func NewPCSWithCookieStr(appID int, cookieStr string) *BaiduPCS {
pcs := &BaiduPCS{
appID: appID,
client: requester.NewHTTPClient(),
}
cookies := requester.ParseCookieStr(cookieStr)
for _, cookie := range cookies {
cookie.Domain = DotBaiduCom
}
jar, _ := cookiejar.New(nil)
jar.SetCookies(baiduComURL, cookies)
pcs.client.SetCookiejar(jar)
return pcs
}
func (pcs *BaiduPCS) lazyInit() {
if pcs.client == nil {
pcs.client = requester.NewHTTPClient()
}
if pcs.ph == nil {
pcs.ph = panhome.NewPanHome(pcs.client)
}
if !pcs.isSetPanUA {
pcs.panUA = NetdiskUA
}
}
// GetClient 获取当前的http client
func (pcs *BaiduPCS) GetClient() *requester.HTTPClient {
pcs.lazyInit()
return pcs.client
}
// GetBDUSS 获取BDUSS
func (pcs *BaiduPCS) GetBDUSS() (bduss string) {
if pcs.client == nil || pcs.client.Jar == nil {
return ""
}
cookies := pcs.client.Jar.Cookies(baiduComURL)
for _, cookie := range cookies {
if cookie.Name == "BDUSS" {
return cookie.Value
}
}
return ""
}
// SetAPPID 设置app_id
func (pcs *BaiduPCS) SetAPPID(appID int) {
pcs.appID = appID
}
// SetUID 设置百度UID
// 只有locatedownload才需要设置此项
func (pcs *BaiduPCS) SetUID(uid uint64) {
pcs.uid = uid
}
// SetStoken 设置stoken
func (pcs *BaiduPCS) SetStoken(stoken string) {
pcs.lazyInit()
if pcs.client.Jar == nil {
pcs.client.ResetCookiejar()
}
pcs.client.Jar.SetCookies(baiduComURL, []*http.Cookie{
&http.Cookie{
Name: "STOKEN",
Value: stoken,
Domain: DotBaiduCom,
},
})
}
// SetPCSUserAgent 设置 PCS User-Agent
func (pcs *BaiduPCS) SetPCSUserAgent(ua string) {
pcs.pcsUA = ua
}
// SetPanUserAgent 设置 Pan User-Agent
func (pcs *BaiduPCS) SetPanUserAgent(ua string) {
pcs.panUA = ua
pcs.isSetPanUA = true
}
// SetHTTPS 是否启用https连接
func (pcs *BaiduPCS) SetHTTPS(https bool) {
pcs.isHTTPS = https
}
// URL 返回 url
func (pcs *BaiduPCS) URL() *url.URL {
return &url.URL{
Scheme: GetHTTPScheme(pcs.isHTTPS),
Host: PCSBaiduCom,
}
}
func (pcs *BaiduPCS) getPanUAHeader() (header map[string]string) {
return map[string]string{
"User-Agent": pcs.panUA,
}
}
func (pcs *BaiduPCS) generatePCSURL(subPath, method string, param ...map[string]string) *url.URL {
pcsURL := pcs.URL()
pcsURL.Path = "/rest/2.0/pcs/" + subPath
uv := pcsURL.Query()
uv.Set("app_id", strconv.Itoa(pcs.appID))
uv.Set("method", method)
for k := range param {
for k2 := range param[k] {
uv.Set(k2, param[k][k2])
}
}
pcsURL.RawQuery = uv.Encode()
return pcsURL
}
func (pcs *BaiduPCS) generatePCSURL2(subPath, method string, param ...map[string]string) *url.URL {
pcsURL2 := &url.URL{
Scheme: GetHTTPScheme(pcs.isHTTPS),
Host: PanBaiduCom,
Path: "/rest/2.0/" + subPath,
}
uv := pcsURL2.Query()
uv.Set("app_id", PanAppID)
uv.Set("method", method)
for k := range param {
for k2 := range param[k] {
uv.Set(k2, param[k][k2])
}
}
pcsURL2.RawQuery = uv.Encode()
return pcsURL2
}
func (pcs *BaiduPCS) generatePanURL(subPath string, param map[string]string) *url.URL {
panURL := url.URL{
Scheme: GetHTTPScheme(pcs.isHTTPS),
Host: PanBaiduCom,
Path: "/api/" + subPath,
}
if param != nil {
uv := url.Values{}
for k := range param {
uv.Set(k, param[k])
}
panURL.RawQuery = uv.Encode()
}
return &panURL
}
// UK 获取用户 UK
func (pcs *BaiduPCS) UK() (uk int64, pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareUK()
if pcsError != nil {
return
}
defer dataReadCloser.Close()
errInfo := pcserror.NewPanErrorInfo(OperationGetUK)
jsonData := userInfoJSON{
PanErrorInfo: errInfo,
}
pcsError = pcserror.HandleJSONParse(OperationGetUK, dataReadCloser, &jsonData)
if pcsError != nil {
return
}
if len(jsonData.Records) != 1 {
errInfo.ErrType = pcserror.ErrTypeOthers
errInfo.Err = errors.New("Unknown remote data")
return 0, errInfo
}
return jsonData.Records[0].Uk, nil
}

35
baidupcs/cache.go Normal file
View File

@@ -0,0 +1,35 @@
package baidupcs
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs/expires"
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"time"
)
// deleteCache 删除含有 dirs 的缓存
func (pcs *BaiduPCS) deleteCache(dirs []string) {
cache := pcs.cacheOpMap.LazyInitCachePoolOp(OperationFilesDirectoriesList)
for _, v := range dirs {
key := v + "_" + defaultOrderOptionsStr
_, ok := cache.Load(key)
if ok {
cache.Delete(key)
}
}
}
// CacheFilesDirectoriesList 缓存获取
func (pcs *BaiduPCS) CacheFilesDirectoriesList(path string, options *OrderOptions) (fdl FileDirectoryList, pcsError pcserror.Error) {
data := pcs.cacheOpMap.CacheOperation(OperationFilesDirectoriesList, path+"_"+fmt.Sprint(options), func() expires.DataExpires {
fdl, pcsError = pcs.FilesDirectoriesList(path, options)
if pcsError != nil {
return nil
}
return expires.NewDataExpires(fdl, 1*time.Minute)
})
if pcsError != nil {
return
}
return data.Data().(FileDirectoryList), nil
}

337
baidupcs/cloud_dl.go Normal file
View File

@@ -0,0 +1,337 @@
package baidupcs
import (
"errors"
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"github.com/iikira/BaiduPCS-Go/pcstable"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"github.com/iikira/BaiduPCS-Go/pcsutil/pcstime"
"io"
"path"
"strconv"
"strings"
)
type (
// CloudDlFileInfo 离线下载的文件信息
CloudDlFileInfo struct {
FileName string `json:"file_name"`
FileSize int64 `json:"file_size"`
}
// CloudDlTaskInfo 离线下载的任务信息
CloudDlTaskInfo struct {
TaskID int64
Status int // 0下载成功, 1下载进行中, 2系统错误, 3资源不存在, 4下载超时, 5资源存在但下载失败, 6存储空间不足, 7任务取消
StatusText string
FileSize int64 // 文件大小
FinishedSize int64 // 文件大小
CreateTime int64 // 创建时间
StartTime int64 // 开始时间
FinishTime int64 // 结束时间
SavePath string // 保存的路径
SourceURL string // 资源地址
TaskName string // 任务名称, 一般为文件名
OdType int
FileList []*CloudDlFileInfo
Result int // 0查询成功结果有效1要查询的task_id不存在
}
// CloudDlTaskList 离线下载的任务信息列表
CloudDlTaskList []*CloudDlTaskInfo
// cloudDlTaskInfo 用于解析远程返回的JSON
cloudDlTaskInfo struct {
Status string `json:"status"`
FileSize string `json:"file_size"`
FinishedSize string `json:"finished_size"`
CreateTime string `json:"create_time"`
StartTime string `json:"start_time"`
FinishTime string `json:"finish_time"`
SavePath string `json:"save_path"`
SourceURL string `json:"source_url"`
TaskName string `json:"task_name"`
OdType string `json:"od_type"`
FileList []*struct {
FileName string `json:"file_name"`
FileSize string `json:"file_size"`
} `json:"file_list"`
Result int `json:"result"`
}
taskIDJSON struct {
TaskID string `json:"task_id"`
}
taskIDInt64JSON struct {
TaskID int64 `json:"task_id"`
}
cloudDlAddTaskJSON struct {
*pcserror.PCSErrInfo
taskIDInt64JSON
}
cloudDlQueryTaskJSON struct {
TaskInfo map[string]*cloudDlTaskInfo `json:"task_info"`
*pcserror.PCSErrInfo
}
cloudDlListTaskJSON struct {
TaskInfo []*taskIDJSON `json:"task_info"`
*pcserror.PCSErrInfo
}
cloudDlClearJSON struct {
Total int `json:"total"`
*pcserror.PCSErrInfo
}
)
func (ci *cloudDlTaskInfo) convert() *CloudDlTaskInfo {
ci2 := &CloudDlTaskInfo{
Status: converter.MustInt(ci.Status),
FileSize: converter.MustInt64(ci.FileSize),
FinishedSize: converter.MustInt64(ci.FinishedSize),
CreateTime: converter.MustInt64(ci.CreateTime),
StartTime: converter.MustInt64(ci.StartTime),
FinishTime: converter.MustInt64(ci.FinishTime),
SavePath: ci.SavePath,
SourceURL: ci.SourceURL,
TaskName: ci.TaskName,
OdType: converter.MustInt(ci.OdType),
Result: ci.Result,
}
ci2.FileList = make([]*CloudDlFileInfo, 0, len(ci.FileList))
for _, v := range ci.FileList {
if v == nil {
continue
}
ci2.FileList = append(ci2.FileList, &CloudDlFileInfo{
FileName: v.FileName,
FileSize: converter.MustInt64(v.FileSize),
})
}
return ci2
}
// CloudDlAddTask 添加离线下载任务
func (pcs *BaiduPCS) CloudDlAddTask(sourceURL, savePath string) (taskID int64, pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareCloudDlAddTask(sourceURL, savePath)
if pcsError != nil {
return
}
defer dataReadCloser.Close()
errInfo := pcserror.NewPCSErrorInfo(OperationCloudDlAddTask)
taskInfo := cloudDlAddTaskJSON{
PCSErrInfo: errInfo,
}
pcsError = pcserror.HandleJSONParse(OperationCloudDlAddTask, dataReadCloser, &taskInfo)
if pcsError != nil {
return
}
return taskInfo.TaskID, nil
}
func (pcs *BaiduPCS) cloudDlQueryTask(op string, taskIDs []int64) (cl CloudDlTaskList, pcsError pcserror.Error) {
errInfo := pcserror.NewPCSErrorInfo(op)
if len(taskIDs) == 0 {
errInfo.ErrType = pcserror.ErrTypeOthers
errInfo.Err = errors.New("no input any task_ids")
return nil, errInfo
}
// TODO: 支持100条以上的task_id查询
if len(taskIDs) > 100 {
taskIDs = taskIDs[:100]
}
taskStrIDs := make([]string, len(taskIDs))
for k := range taskStrIDs {
taskStrIDs[k] = strconv.FormatInt(taskIDs[k], 10)
}
dataReadCloser, pcsError := pcs.PrepareCloudDlQueryTask(strings.Join(taskStrIDs, ","))
if pcsError != nil {
return
}
defer dataReadCloser.Close()
taskInfo := cloudDlQueryTaskJSON{
PCSErrInfo: errInfo,
}
pcsError = pcserror.HandleJSONParse(OperationCloudDlQueryTask, dataReadCloser, &taskInfo)
if pcsError != nil {
return
}
var v2 *CloudDlTaskInfo
cl = make(CloudDlTaskList, 0, len(taskStrIDs))
for k := range taskStrIDs {
var err error
v := taskInfo.TaskInfo[taskStrIDs[k]]
if v == nil {
continue
}
v2 = v.convert()
v2.TaskID, err = strconv.ParseInt(taskStrIDs[k], 10, 64)
if err != nil {
continue
}
v2.ParseText()
cl = append(cl, v2)
}
return cl, nil
}
// CloudDlQueryTask 精确查询离线下载任务
func (pcs *BaiduPCS) CloudDlQueryTask(taskIDs []int64) (cl CloudDlTaskList, pcsError pcserror.Error) {
return pcs.cloudDlQueryTask(OperationCloudDlQueryTask, taskIDs)
}
// CloudDlListTask 查询离线下载任务列表
func (pcs *BaiduPCS) CloudDlListTask() (cl CloudDlTaskList, pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareCloudDlListTask()
if pcsError != nil {
return
}
defer dataReadCloser.Close()
errInfo := pcserror.NewPCSErrorInfo(OperationCloudDlListTask)
taskInfo := cloudDlListTaskJSON{
PCSErrInfo: errInfo,
}
pcsError = pcserror.HandleJSONParse(OperationCloudDlListTask, dataReadCloser, &taskInfo)
if pcsError != nil {
return
}
// 没有任务
if len(taskInfo.TaskInfo) <= 0 {
return CloudDlTaskList{}, nil
}
var (
taskID int64
taskIDs = make([]int64, 0, len(taskInfo.TaskInfo))
)
for _, v := range taskInfo.TaskInfo {
if v == nil {
continue
}
var err error
if taskID, err = strconv.ParseInt(v.TaskID, 10, 64); err == nil {
taskIDs = append(taskIDs, taskID)
}
}
cl, pcsError = pcs.cloudDlQueryTask(OperationCloudDlListTask, taskIDs)
if pcsError != nil {
return nil, pcsError
}
return cl, nil
}
func (pcs *BaiduPCS) cloudDlManipTask(op string, taskID int64) (pcsError pcserror.Error) {
var dataReadCloser io.ReadCloser
switch op {
case OperationCloudDlCancelTask:
dataReadCloser, pcsError = pcs.PrepareCloudDlCancelTask(taskID)
case OperationCloudDlDeleteTask:
dataReadCloser, pcsError = pcs.PrepareCloudDlDeleteTask(taskID)
default:
panic("unknown op, " + op)
}
if pcsError != nil {
return
}
defer dataReadCloser.Close()
errInfo := pcserror.DecodePCSJSONError(op, dataReadCloser)
return errInfo
}
// CloudDlCancelTask 取消离线下载任务
func (pcs *BaiduPCS) CloudDlCancelTask(taskID int64) (pcsError pcserror.Error) {
return pcs.cloudDlManipTask(OperationCloudDlCancelTask, taskID)
}
// CloudDlDeleteTask 删除离线下载任务
func (pcs *BaiduPCS) CloudDlDeleteTask(taskID int64) (pcsError pcserror.Error) {
return pcs.cloudDlManipTask(OperationCloudDlDeleteTask, taskID)
}
// CloudDlClearTask 清空离线下载任务记录
func (pcs *BaiduPCS) CloudDlClearTask() (total int, pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareCloudDlClearTask()
if pcsError != nil {
return
}
defer dataReadCloser.Close()
errInfo := pcserror.NewPCSErrorInfo(OperationCloudDlClearTask)
clearInfo := cloudDlClearJSON{
PCSErrInfo: errInfo,
}
pcsError = pcserror.HandleJSONParse(OperationCloudDlClearTask, dataReadCloser, &clearInfo)
if pcsError != nil {
return
}
return clearInfo.Total, nil
}
// ParseText 解析状态码
func (ci *CloudDlTaskInfo) ParseText() {
switch ci.Status {
case 0:
ci.StatusText = "下载成功"
case 1:
ci.StatusText = "下载进行中"
case 2:
ci.StatusText = "系统错误"
case 3:
ci.StatusText = "资源不存在"
case 4:
ci.StatusText = "下载超时"
case 5:
ci.StatusText = "资源存在但下载失败"
case 6:
ci.StatusText = "存储空间不足"
case 7:
ci.StatusText = "任务取消"
default:
ci.StatusText = "未知状态码: " + strconv.Itoa(ci.Status)
}
}
func (cl CloudDlTaskList) String() string {
builder := &strings.Builder{}
tb := pcstable.NewTable(builder)
tb.SetHeader([]string{"#", "任务ID", "任务名称", "文件大小", "创建日期", "保存路径", "资源地址", "状态"})
for k, v := range cl {
tb.Append([]string{strconv.Itoa(k), strconv.FormatInt(v.TaskID, 10), v.TaskName, converter.ConvertFileSize(v.FileSize), pcstime.FormatTime(v.CreateTime), path.Clean(v.SavePath), v.SourceURL, v.StatusText})
}
tb.Render()
return builder.String()
}

42
baidupcs/cp_mv_rename.go Normal file
View File

@@ -0,0 +1,42 @@
package baidupcs
import (
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"unsafe"
)
// Rename 重命名文件/目录
func (pcs *BaiduPCS) Rename(from, to string) (pcsError pcserror.Error) {
return pcs.cpmvOp(OperationRename, &CpMvJSON{
From: from,
To: to,
})
}
// Copy 批量拷贝文件/目录
func (pcs *BaiduPCS) Copy(cpmvJSON ...*CpMvJSON) (pcsError pcserror.Error) {
return pcs.cpmvOp(OperationCopy, cpmvJSON...)
}
// Move 批量移动文件/目录
func (pcs *BaiduPCS) Move(cpmvJSON ...*CpMvJSON) (pcsError pcserror.Error) {
return pcs.cpmvOp(OperationMove, cpmvJSON...)
}
func (pcs *BaiduPCS) cpmvOp(op string, cpmvJSON ...*CpMvJSON) (pcsError pcserror.Error) {
dataReadCloser, err := pcs.prepareCpMvOp(op, cpmvJSON...)
if err != nil {
return
}
defer dataReadCloser.Close()
errInfo := pcserror.DecodePCSJSONError(op, dataReadCloser)
if errInfo != nil {
return errInfo
}
// 更新缓存
pcs.deleteCache((*CpMvJSONList)(unsafe.Pointer(&cpmvJSON)).AllRelatedDir())
return nil
}

169
baidupcs/download.go Normal file
View File

@@ -0,0 +1,169 @@
package baidupcs
import (
"errors"
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"net/http"
"net/url"
)
var (
// ErrLocateDownloadURLNotFound 未找到下载链接
ErrLocateDownloadURLNotFound = errors.New("locatedownload url not found")
// MaxDownloadRangeSize 文件片段最大值
MaxDownloadRangeSize = 55 * converter.MB
)
type (
// DownloadFunc 下载文件处理函数
DownloadFunc func(downloadURL string, jar http.CookieJar) error
// URLInfo 下载链接详情
URLInfo struct {
URLs []struct {
URL string `json:"url"`
} `json:"urls"`
}
// LocateDownloadInfoV1 locatedownload api v1
LocateDownloadInfoV1 struct {
Server []string `json:"server"`
PathJSON
}
locateDownloadJSON struct {
*pcserror.PCSErrInfo
URLInfo
}
// APIDownloadDlinkInfo 下载信息
APIDownloadDlinkInfo struct {
Dlink string `json:"dlink"`
FsID string `json:"fs_id"`
}
// APIDownloadDlinkInfoList 下载信息列表
APIDownloadDlinkInfoList []*APIDownloadDlinkInfo
panAPIDownloadJSON struct {
*pcserror.PanErrorInfo
DlinkList APIDownloadDlinkInfoList `json:"dlink"`
}
)
// URLStrings 返回下载链接数组
func (ui *URLInfo) URLStrings(https bool) (urls []*url.URL) {
urls = make([]*url.URL, 0, len(ui.URLs))
for k := range ui.URLs {
thisURL, err := url.Parse(ui.URLs[k].URL)
if err != nil {
continue
}
thisURL.Scheme = GetHTTPScheme(https)
urls = append(urls, thisURL)
}
return urls
}
// SingleURL 返回单条下载链接
func (ui *URLInfo) SingleURL(https bool) *url.URL {
if len(ui.URLs) < 1 {
return nil
}
u, err := url.Parse(ui.URLs[0].URL)
if err != nil {
return nil
}
u.Scheme = GetHTTPScheme(https)
return u
}
// LastURL 返回最后一条下载链接
func (ui *URLInfo) LastURL(https bool) *url.URL {
if len(ui.URLs) < 1 {
return nil
}
u, err := url.Parse(ui.URLs[len(ui.URLs)-1].URL)
if err != nil {
return nil
}
u.Scheme = GetHTTPScheme(https)
return u
}
// DownloadFile 下载单个文件
func (pcs *BaiduPCS) DownloadFile(path string, downloadFunc DownloadFunc) (err error) {
pcs.lazyInit()
pcsURL := pcs.generatePCSURL("file", "download", map[string]string{
"path": path,
})
baiduPCSVerbose.Infof("%s URL: %s\n", OperationDownloadFile, pcsURL)
return downloadFunc(pcsURL.String(), pcs.client.Jar)
}
// DownloadStreamFile 下载流式文件
func (pcs *BaiduPCS) DownloadStreamFile(path string, downloadFunc DownloadFunc) (err error) {
pcs.lazyInit()
pcsURL := pcs.generatePCSURL("stream", "download", map[string]string{
"path": path,
})
baiduPCSVerbose.Infof("%s URL: %s\n", OperationDownloadStreamFile, pcsURL)
return downloadFunc(pcsURL.String(), pcs.client.Jar)
}
// LocateDownloadWithUserAgent 获取下载链接
func (pcs *BaiduPCS) LocateDownload(pcspath string) (info *URLInfo, pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareLocateDownload(pcspath)
if dataReadCloser != nil {
defer dataReadCloser.Close()
}
if pcsError != nil {
return nil, pcsError
}
errInfo := pcserror.NewPCSErrorInfo(OperationLocateDownload)
jsonData := locateDownloadJSON{
PCSErrInfo: errInfo,
}
pcsError = pcserror.HandleJSONParse(OperationLocateDownload, dataReadCloser, &jsonData)
if pcsError != nil {
return
}
return &jsonData.URLInfo, nil
}
// LocatePanAPIDownload 从百度网盘首页获取下载链接
func (pcs *BaiduPCS) LocatePanAPIDownload(fidList ...int64) (dlinkInfoList APIDownloadDlinkInfoList, pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareLocatePanAPIDownload(fidList...)
if dataReadCloser != nil {
defer dataReadCloser.Close()
}
if pcsError != nil {
return nil, pcsError
}
jsonData := panAPIDownloadJSON{
PanErrorInfo: pcserror.NewPanErrorInfo(OperationLocatePanAPIDownload),
}
pcsError = pcserror.HandleJSONParse(OperationLocatePanAPIDownload, dataReadCloser, &jsonData)
if pcsError != nil {
if pcsError.GetErrType() == pcserror.ErrTypeRemoteError {
switch pcsError.GetRemoteErrCode() {
case 112: // 页面已过期
fallthrough
case 113: // 签名错误
pcs.ph.SetSignExpires() // 重置
}
}
return
}
return jsonData.DlinkList, nil
}

View File

@@ -0,0 +1,43 @@
package cachemap
import (
"github.com/iikira/BaiduPCS-Go/baidupcs/expires"
"sync"
)
var (
GlobalCacheOpMap = CacheOpMap{}
)
type (
CacheOpMap struct {
cachePool sync.Map
}
)
func (cm *CacheOpMap) LazyInitCachePoolOp(op string) CacheUnit {
cacheItf, _ := cm.cachePool.LoadOrStore(op, &cacheUnit{})
return cacheItf.(CacheUnit)
}
func (cm *CacheOpMap) RemoveCachePoolOp(op string) {
cm.cachePool.Delete(op)
}
// ClearInvalidate 清除已过期的数据(一般用不到)
func (cm *CacheOpMap) ClearInvalidate() {
cm.cachePool.Range(func(_, cacheItf interface{}) bool {
cache := cacheItf.(CacheUnit)
cache.Range(func(key interface{}, exp expires.DataExpires) bool {
if exp.IsExpires() {
cache.Delete(key)
}
return true
})
return true
})
}
// PrintAll 输出所有缓冲项目
func (cm *CacheOpMap) PrintAll() {
}

View File

@@ -0,0 +1,80 @@
package cachemap_test
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs/expires"
"github.com/iikira/BaiduPCS-Go/baidupcs/expires/cachemap"
"sync"
"sync/atomic"
"testing"
"time"
)
func TestCacheMapDataExpires(t *testing.T) {
cm := cachemap.CacheOpMap{}
cache := cm.LazyInitCachePoolOp("op")
cache.Store("key_1", expires.NewDataExpires("value_1", 1*time.Second))
time.Sleep(2 * time.Second)
data, ok := cache.Load("key_1")
if ok {
fmt.Printf("data: %s\n", data.Data())
// 超时仍能读取到数据, 失败
t.FailNow()
}
}
func TestCacheOperation(t *testing.T) {
cm := cachemap.CacheOpMap{}
data := cm.CacheOperation("op", "key_1", func() expires.DataExpires {
return expires.NewDataExpires("value_1", 1*time.Second)
})
fmt.Printf("data: %s\n", data.Data())
newData := cm.CacheOperation("op", "key_1", func() expires.DataExpires {
return expires.NewDataExpires("value_3", 1*time.Second)
})
if data != newData {
t.FailNow()
}
fmt.Printf("data: %s\n", data.Data())
}
func TestCacheOperation_LockKey(t *testing.T) {
cm := cachemap.CacheOpMap{}
wg := sync.WaitGroup{}
wg.Add(5000)
var (
execTimes1 int32 = 0 // 执行次数1
execTimes2 int32 = 0 // 执行次数2
)
for i := 0; i < 5000; i++ {
go func(i int) {
defer wg.Done()
cm.CacheOperation("op", "key_1", func() expires.DataExpires {
time.Sleep(50 * time.Microsecond) // 一些耗时的操作
atomic.AddInt32(&execTimes1, 1)
return expires.NewDataExpires(fmt.Sprintf("value_1: %d", i), 10*time.Second)
})
cm.CacheOperation("op", "key_2", func() expires.DataExpires {
time.Sleep(50 * time.Microsecond) // 一些耗时的操作
atomic.AddInt32(&execTimes2, 1)
return expires.NewDataExpires(fmt.Sprintf("value_2: %d", i), 10*time.Second)
})
}(i)
}
wg.Wait()
// 执行次数应为1
if execTimes1 != 1 {
fmt.Printf("execTimes1: %d\n", execTimes1)
t.FailNow()
}
if execTimes2 != 1 {
fmt.Printf("execTimes2: %d\n", execTimes2)
t.FailNow()
}
}

View File

@@ -0,0 +1,81 @@
package cachemap
import (
"github.com/iikira/BaiduPCS-Go/baidupcs/expires"
"sync"
)
type (
CacheUnit interface {
Delete(key interface{})
Load(key interface{}) (value expires.DataExpires, ok bool)
LoadOrStore(key interface{}, value expires.DataExpires) (actual expires.DataExpires, loaded bool)
Range(f func(key interface{}, value expires.DataExpires) bool)
Store(key interface{}, value expires.DataExpires)
LockKey(key interface{})
UnlockKey(key interface{})
}
cacheUnit struct {
unit sync.Map
keyMap sync.Map
}
)
func (cu *cacheUnit) Delete(key interface{}) {
cu.unit.Delete(key)
cu.keyMap.Delete(key)
}
func (cu *cacheUnit) Load(key interface{}) (value expires.DataExpires, ok bool) {
val, ok := cu.unit.Load(key)
if !ok {
return nil, ok
}
exp := val.(expires.DataExpires)
if exp.IsExpires() {
cu.unit.Delete(key)
return nil, false
}
return exp, ok
}
func (cu *cacheUnit) Range(f func(key interface{}, value expires.DataExpires) bool) {
cu.unit.Range(func(k, val interface{}) bool {
exp := val.(expires.DataExpires)
if exp.IsExpires() {
cu.unit.Delete(k)
return true
}
return f(k, val.(expires.DataExpires))
})
}
func (cu *cacheUnit) LoadOrStore(key interface{}, value expires.DataExpires) (actual expires.DataExpires, loaded bool) {
ac, loaded := cu.unit.LoadOrStore(key, value)
exp := ac.(expires.DataExpires)
if exp.IsExpires() {
cu.unit.Delete(key)
return nil, false
}
return exp, loaded
}
func (cu *cacheUnit) Store(key interface{}, value expires.DataExpires) {
if value.IsExpires() {
return
}
cu.unit.Store(key, value)
}
func (cu *cacheUnit) LockKey(key interface{}) {
muItf, _ := cu.keyMap.LoadOrStore(key, &sync.Mutex{})
mu := muItf.(*sync.Mutex)
mu.Lock()
}
func (cu *cacheUnit) UnlockKey(key interface{}) {
muItf, _ := cu.keyMap.LoadOrStore(key, &sync.Mutex{})
mu := muItf.(*sync.Mutex)
mu.Unlock()
}

View File

@@ -0,0 +1,54 @@
package cachemap
import (
"github.com/iikira/BaiduPCS-Go/baidupcs/expires"
)
type (
OpFunc func() expires.DataExpires
OpFuncWithError func() (expires.DataExpires, error)
)
func (cm *CacheOpMap) CacheOperation(op string, key interface{}, opFunc OpFunc) (data expires.DataExpires) {
var (
cache = cm.LazyInitCachePoolOp(op)
ok bool
)
cache.LockKey(key)
defer cache.UnlockKey(key)
data, ok = cache.Load(key)
if !ok {
data = opFunc()
if data != nil {
cache.Store(key, data)
}
return
}
return
}
func (cm *CacheOpMap) CacheOperationWithError(op string, key interface{}, opFunc OpFuncWithError) (data expires.DataExpires, err error) {
var (
cache = cm.LazyInitCachePoolOp(op)
ok bool
)
cache.LockKey(key)
defer cache.UnlockKey(key)
data, ok = cache.Load(key)
if !ok {
data, err = opFunc()
if err != nil {
return
}
if data == nil {
// 数据为空时也不存
return
}
cache.Store(key, data)
}
return
}

View File

@@ -0,0 +1,28 @@
package expires
import (
"time"
)
type (
DataExpires interface {
Data() interface{}
Expires
}
dataExpires struct {
data interface{}
Expires
}
)
func NewDataExpires(data interface{}, dur time.Duration) DataExpires {
return &dataExpires{
data: data,
Expires: NewExpires(dur),
}
}
func (de *dataExpires) Data() interface{} {
return de.data
}

View File

@@ -0,0 +1,60 @@
package expires
import (
"fmt"
"time"
_ "unsafe" // for go:linkname
)
type (
Expires interface {
IsExpires() bool
GetExpires() time.Time
SetExpires(e bool)
fmt.Stringer
}
expires struct {
expiresAt time.Time
abort bool
}
)
//go:linkname stripMono time.(*Time).stripMono
func stripMono(t *time.Time)
// StripMono strip monotonic clocks
func StripMono(t *time.Time) {
stripMono(t)
}
func NewExpires(dur time.Duration) Expires {
t := time.Now().Add(dur)
StripMono(&t)
return &expires{
expiresAt: t,
}
}
func NewExpiresAt(at time.Time) Expires {
StripMono(&at)
return &expires{
expiresAt: at,
}
}
func (ep *expires) GetExpires() time.Time {
return ep.expiresAt
}
func (ep *expires) SetExpires(e bool) {
ep.abort = e
}
func (ep *expires) IsExpires() bool {
return ep.abort || time.Now().After(ep.expiresAt)
}
func (ep *expires) String() string {
return fmt.Sprintf("expires at: %s, abort: %t", ep.expiresAt, ep.abort)
}

View File

362
baidupcs/extends.go Normal file
View File

@@ -0,0 +1,362 @@
package baidupcs
import (
"crypto/md5"
"encoding/hex"
"errors"
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"github.com/iikira/BaiduPCS-Go/pcsutil/cachepool"
"github.com/iikira/BaiduPCS-Go/pcsutil/escaper"
"github.com/iikira/BaiduPCS-Go/requester/downloader"
"io"
"mime"
"net/http"
"net/url"
"path"
"strconv"
"strings"
)
const (
// ShellPatternCharacters 通配符字符串
ShellPatternCharacters = "*?[]"
)
var (
// ErrFixMD5Isdir 目录不需要修复md5
ErrFixMD5Isdir = errors.New("directory not support fix md5")
// ErrFixMD5Failed 修复MD5失败, 可能服务器未刷新
ErrFixMD5Failed = errors.New("fix md5 failed")
// ErrFixMD5FileInfoNil 文件信息对象为空
ErrFixMD5FileInfoNil = errors.New("file info is nil")
// ErrMatchPathByShellPatternNotAbsPath 不是绝对路径
ErrMatchPathByShellPatternNotAbsPath = errors.New("not absolute path")
ErrContentRangeNotFound = errors.New("Content-Range not found")
ErrGetRapidUploadInfoLengthNotFound = errors.New("Content-Length not found")
ErrGetRapidUploadInfoMD5NotFound = errors.New("Content-MD5 not found")
ErrGetRapidUploadInfoCrc32NotFound = errors.New("x-bs-meta-crc32 not found")
ErrGetRapidUploadInfoFilenameNotEqual = errors.New("文件名不匹配")
ErrGetRapidUploadInfoLengthNotEqual = errors.New("Content-Length 不匹配")
ErrGetRapidUploadInfoMD5NotEqual = errors.New("Content-MD5 不匹配")
ErrGetRapidUploadInfoCrc32NotEqual = errors.New("x-bs-meta-crc32 不匹配")
ErrGetRapidUploadInfoSliceMD5NotEqual = errors.New("slice-md5 不匹配")
ErrFileTooLarge = errors.New("文件大于20GB, 无法秒传")
)
func (pcs *BaiduPCS) getLocateDownloadLink(pcspath string) (link string, pcsError pcserror.Error) {
info, pcsError := pcs.LocateDownload(pcspath)
if pcsError != nil {
return
}
u := info.SingleURL(pcs.isHTTPS)
if u == nil {
return "", &pcserror.PCSErrInfo{
Operation: OperationLocateDownload,
ErrType: pcserror.ErrTypeOthers,
Err: ErrLocateDownloadURLNotFound,
}
}
return u.String(), nil
}
// ExportByFileInfo 通过文件信息对象, 导出文件信息
func (pcs *BaiduPCS) ExportByFileInfo(finfo *FileDirectory) (rinfo *RapidUploadInfo, pcsError pcserror.Error) {
errInfo := pcserror.NewPCSErrorInfo(OperationExportFileInfo)
errInfo.ErrType = pcserror.ErrTypeOthers
if finfo.Size > MaxRapidUploadSize {
errInfo.Err = ErrFileTooLarge
return nil, errInfo
}
rinfo, pcsError = pcs.GetRapidUploadInfoByFileInfo(finfo)
if pcsError != nil {
return nil, pcsError
}
if rinfo.Filename != finfo.Filename {
baiduPCSVerbose.Infof("%s filename not equal, local: %s, remote link: %s\n", OperationExportFileInfo, finfo.Filename, rinfo.Filename)
rinfo.Filename = finfo.Filename
}
return rinfo, nil
}
// GetRapidUploadInfoByFileInfo 通过文件信息对象, 获取秒传信息
func (pcs *BaiduPCS) GetRapidUploadInfoByFileInfo(finfo *FileDirectory) (rinfo *RapidUploadInfo, pcsError pcserror.Error) {
if finfo.Size <= SliceMD5Size && len(finfo.BlockList) == 1 && finfo.BlockList[0] == finfo.MD5 {
// 可直接秒传
return &RapidUploadInfo{
Filename: finfo.Filename,
ContentLength: finfo.Size,
ContentMD5: finfo.MD5,
SliceMD5: finfo.MD5,
ContentCrc32: "0",
}, nil
}
link, pcsError := pcs.getLocateDownloadLink(finfo.Path)
if pcsError != nil {
return nil, pcsError
}
// 只有ContentLength可以比较
// finfo记录的ContentMD5不一定是正确的
// finfo记录的Filename不一定与获取到的一致
return pcs.GetRapidUploadInfoByLink(link, &RapidUploadInfo{
ContentLength: finfo.Size,
})
}
// GetRapidUploadInfoByLink 通过下载链接, 获取文件秒传信息
func (pcs *BaiduPCS) GetRapidUploadInfoByLink(link string, compareRInfo *RapidUploadInfo) (rinfo *RapidUploadInfo, pcsError pcserror.Error) {
errInfo := pcserror.NewPCSErrorInfo(OperationGetRapidUploadInfo)
errInfo.ErrType = pcserror.ErrTypeOthers
var (
header = pcs.getPanUAHeader()
isSetRange = compareRInfo != nil && compareRInfo.ContentLength > SliceMD5Size // 是否设置Range
)
if isSetRange {
header["Range"] = "bytes=0-" + strconv.FormatInt(SliceMD5Size-1, 10)
}
resp, err := pcs.client.Req(http.MethodGet, link, nil, header)
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
errInfo.SetNetError(err)
return nil, errInfo
}
// 检测响应状态码
if resp.StatusCode/100 != 2 {
errInfo.SetNetError(errors.New(resp.Status))
return nil, errInfo
}
// 检测是否存在MD5
md5Str := resp.Header.Get("Content-MD5")
if md5Str == "" { // 未找到md5值, 可能是服务器未刷新
errInfo.Err = ErrGetRapidUploadInfoMD5NotFound
return nil, errInfo
}
if compareRInfo != nil && compareRInfo.ContentMD5 != "" && compareRInfo.ContentMD5 != md5Str {
errInfo.Err = ErrGetRapidUploadInfoMD5NotEqual
return nil, errInfo
}
// 获取文件名
_, params, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition"))
if err != nil {
errInfo.Err = err
return nil, errInfo
}
filename, err := url.QueryUnescape(params["filename"])
if err != nil {
errInfo.Err = err
return nil, errInfo
}
if compareRInfo != nil && compareRInfo.Filename != "" && compareRInfo.Filename != filename {
errInfo.Err = ErrGetRapidUploadInfoFilenameNotEqual
return nil, errInfo
}
var (
contentLength int64
)
if isSetRange {
// 检测Content-Range
contentRange := resp.Header.Get("Content-Range")
if contentRange == "" {
errInfo.Err = ErrContentRangeNotFound
return nil, errInfo
}
contentLength = downloader.ParseContentRange(contentRange)
} else {
contentLength = resp.ContentLength
}
// 检测Content-Length
switch contentLength {
case -1:
errInfo.Err = ErrGetRapidUploadInfoLengthNotFound
return nil, errInfo
case 0:
return &RapidUploadInfo{
Filename: filename,
ContentLength: contentLength,
ContentMD5: EmptyContentMD5,
SliceMD5: EmptyContentMD5,
ContentCrc32: "0",
}, nil
default:
if compareRInfo != nil && compareRInfo.ContentLength > 0 && compareRInfo.ContentLength != contentLength {
errInfo.Err = ErrGetRapidUploadInfoLengthNotEqual
return nil, errInfo
}
}
// 检测是否存在crc32 值, 一般都会存在的
crc32Str := resp.Header.Get("x-bs-meta-crc32")
if crc32Str == "" || crc32Str == "0" {
errInfo.Err = ErrGetRapidUploadInfoCrc32NotFound
return nil, errInfo
}
if compareRInfo != nil && compareRInfo.ContentCrc32 != "" && compareRInfo.ContentCrc32 != crc32Str {
errInfo.Err = ErrGetRapidUploadInfoCrc32NotEqual
return nil, errInfo
}
// 获取slice-md5
// 忽略比较slice-md5
if contentLength <= SliceMD5Size {
return &RapidUploadInfo{
Filename: filename,
ContentLength: contentLength,
ContentMD5: md5Str,
SliceMD5: md5Str,
ContentCrc32: crc32Str,
}, nil
}
buf := cachepool.RawMallocByteSlice(int(SliceMD5Size))
_, err = io.ReadFull(resp.Body, buf)
if err != nil {
errInfo.SetNetError(err)
return nil, errInfo
}
// 计算slice-md5
m := md5.New()
_, err = m.Write(buf)
if err != nil {
panic(err)
}
sliceMD5Str := hex.EncodeToString(m.Sum(nil))
// 检测slice-md5, 不必要的
if compareRInfo != nil && compareRInfo.SliceMD5 != "" && compareRInfo.SliceMD5 != sliceMD5Str {
errInfo.Err = ErrGetRapidUploadInfoSliceMD5NotEqual
return nil, errInfo
}
return &RapidUploadInfo{
Filename: filename,
ContentLength: contentLength,
ContentMD5: md5Str,
SliceMD5: sliceMD5Str,
ContentCrc32: crc32Str,
}, nil
}
// FixMD5ByFileInfo 尝试修复文件的md5, 通过文件信息对象
func (pcs *BaiduPCS) FixMD5ByFileInfo(finfo *FileDirectory) (pcsError pcserror.Error) {
errInfo := pcserror.NewPCSErrorInfo(OperationFixMD5)
errInfo.ErrType = pcserror.ErrTypeOthers
if finfo == nil {
errInfo.Err = ErrFixMD5FileInfoNil
return errInfo
}
if finfo.Size > MaxRapidUploadSize { // 文件大于20GB
errInfo.Err = ErrFileTooLarge
return errInfo
}
// 忽略目录
if finfo.Isdir {
errInfo.Err = ErrFixMD5Isdir
return errInfo
}
if len(finfo.BlockList) == 1 && finfo.BlockList[0] == finfo.MD5 {
// 不需要修复
return nil
}
link, pcsError := pcs.getLocateDownloadLink(finfo.Path)
if pcsError != nil {
return pcsError
}
var (
cmpInfo = &RapidUploadInfo{
Filename: finfo.Filename,
ContentLength: finfo.Size,
}
)
rinfo, pcsError := pcs.GetRapidUploadInfoByLink(link, cmpInfo)
if pcsError != nil {
switch pcsError.GetError() {
case ErrGetRapidUploadInfoMD5NotFound, ErrGetRapidUploadInfoCrc32NotFound:
errInfo.Err = ErrFixMD5Failed
default:
errInfo.Err = pcsError
}
return errInfo
}
// 开始修复
return pcs.RapidUploadNoCheckDir(finfo.Path, rinfo.ContentMD5, rinfo.SliceMD5, rinfo.ContentCrc32, rinfo.ContentLength)
}
// FixMD5 尝试修复文件的md5
func (pcs *BaiduPCS) FixMD5(pcspath string) (pcsError pcserror.Error) {
finfo, pcsError := pcs.FilesDirectoriesMeta(pcspath)
if pcsError != nil {
return
}
return pcs.FixMD5ByFileInfo(finfo)
}
func (pcs *BaiduPCS) recurseMatchPathByShellPattern(index int, patternSlice *[]string, ps *[]string, pcspaths *[]string) {
if index == len(*patternSlice) {
*pcspaths = append(*pcspaths, strings.Join(*ps, PathSeparator))
return
}
if !strings.ContainsAny((*patternSlice)[index], ShellPatternCharacters) {
(*ps)[index] = (*patternSlice)[index]
pcs.recurseMatchPathByShellPattern(index+1, patternSlice, ps, pcspaths)
return
}
fds, pcsError := pcs.FilesDirectoriesList(strings.Join((*ps)[:index], PathSeparator), DefaultOrderOptions)
if pcsError != nil {
panic(pcsError) // 抛出异常
}
for k := range fds {
if matched, _ := path.Match((*patternSlice)[index], fds[k].Filename); matched {
(*ps)[index] = fds[k].Filename
pcs.recurseMatchPathByShellPattern(index+1, patternSlice, ps, pcspaths)
}
}
return
}
// MatchPathByShellPattern 通配符匹配文件路径, pattern 为绝对路径
func (pcs *BaiduPCS) MatchPathByShellPattern(pattern string) (pcspaths []string, pcsError pcserror.Error) {
errInfo := pcserror.NewPCSErrorInfo(OperrationMatchPathByShellPattern)
errInfo.ErrType = pcserror.ErrTypeOthers
patternSlice := strings.Split(escaper.Escape(path.Clean(pattern), []rune{'['}), PathSeparator) // 转义中括号
if patternSlice[0] != "" {
errInfo.Err = ErrMatchPathByShellPatternNotAbsPath
return nil, errInfo
}
ps := make([]string, len(patternSlice))
defer func() { // 捕获异常
if err := recover(); err != nil {
pcspaths = nil
pcsError = err.(pcserror.Error)
}
}()
pcs.recurseMatchPathByShellPattern(1, &patternSlice, &ps, &pcspaths)
return pcspaths, nil
}

370
baidupcs/file_directory.go Normal file
View File

@@ -0,0 +1,370 @@
package baidupcs
import (
"errors"
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"github.com/iikira/BaiduPCS-Go/pcstable"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"github.com/iikira/BaiduPCS-Go/pcsutil/pcstime"
"github.com/olekukonko/tablewriter"
"strconv"
"strings"
"unsafe"
)
type (
// OrderBy 排序字段
OrderBy string
// Order 升序降序
Order string
)
const (
// OrderByName 根据文件名排序
OrderByName OrderBy = "name"
// OrderByTime 根据时间排序
OrderByTime OrderBy = "time"
// OrderBySize 根据大小排序, 注意目录无大小
OrderBySize OrderBy = "size"
// OrderAsc 升序
OrderAsc Order = "asc"
// OrderDesc 降序
OrderDesc Order = "desc"
)
type (
// HandleFileDirectoryFunc 处理文件或目录的元信息, 返回值控制是否退出递归
HandleFileDirectoryFunc func(depth int, fdPath string, fd *FileDirectory, pcsError pcserror.Error) bool
// FileDirectory 文件或目录的元信息
FileDirectory struct {
FsID int64 // fs_id
AppID int64 // app_id
Path string // 路径
Filename string // 文件名 或 目录名
Ctime int64 // 创建日期
Mtime int64 // 修改日期
MD5 string // md5 值
BlockListJSON
Size int64 // 文件大小 (目录为0)
Isdir bool // 是否为目录
Ifhassubdir bool // 是否含有子目录 (只对目录有效)
Parent *FileDirectory // 父目录信息
Children FileDirectoryList // 子目录信息
}
// FileDirectoryList FileDirectory 的 指针数组
FileDirectoryList []*FileDirectory
// fdJSON 用于解析远程JSON数据
fdJSON struct {
FsID int64 `json:"fs_id"` // fs_id
AppID int64 `json:"app_id"`
Path string `json:"path"` // 路径
Filename string `json:"server_filename"` // 文件名 或 目录名
Ctime int64 `json:"ctime"` // 创建日期
Mtime int64 `json:"mtime"` // 修改日期
MD5 string `json:"md5"` // md5 值
BlockListJSON
Size int64 `json:"size"` // 文件大小 (目录为0)
IsdirInt int8 `json:"isdir"`
IfhassubdirInt int8 `json:"ifhassubdir"`
// 对齐
_ *fdJSON
_ []*fdJSON
}
fdData struct {
*pcserror.PCSErrInfo
List FileDirectoryList
}
fdDataJSONExport struct {
*pcserror.PCSErrInfo
List []*fdJSON `json:"list"`
}
// OrderOptions 列文件/目录可选项
OrderOptions struct {
By OrderBy
Order Order
}
)
var (
// DefaultOrderOptions 默认的排序
DefaultOrderOptions = &OrderOptions{
By: OrderByName,
Order: OrderAsc,
}
defaultOrderOptionsStr = fmt.Sprint(DefaultOrderOptions)
)
// FilesDirectoriesMeta 获取单个文件/目录的元信息
func (pcs *BaiduPCS) FilesDirectoriesMeta(path string) (data *FileDirectory, pcsError pcserror.Error) {
if path == "" {
path = PathSeparator
}
fds, err := pcs.FilesDirectoriesBatchMeta(path)
if err != nil {
return nil, err
}
// 返回了多条元信息
if len(fds) != 1 {
return nil, &pcserror.PCSErrInfo{
Operation: OperationFilesDirectoriesMeta,
ErrType: pcserror.ErrTypeOthers,
Err: errors.New("未知返回数据"),
}
}
return fds[0], nil
}
// FilesDirectoriesBatchMeta 获取多个文件/目录的元信息
func (pcs *BaiduPCS) FilesDirectoriesBatchMeta(paths ...string) (data FileDirectoryList, pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareFilesDirectoriesBatchMeta(paths...)
if pcsError != nil {
return nil, pcsError
}
defer dataReadCloser.Close()
errInfo := pcserror.NewPCSErrorInfo(OperationFilesDirectoriesMeta)
// 服务器返回数据进行处理
jsonData := fdData{
PCSErrInfo: errInfo,
}
pcsError = pcserror.HandleJSONParse(OperationFilesDirectoriesMeta, dataReadCloser, (*fdDataJSONExport)(unsafe.Pointer(&jsonData)))
if pcsError != nil {
return
}
// 修复MD5
jsonData.List.fixMD5()
data = jsonData.List
return
}
// FilesDirectoriesList 获取目录下的文件和目录列表
func (pcs *BaiduPCS) FilesDirectoriesList(path string, options *OrderOptions) (data FileDirectoryList, pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareFilesDirectoriesList(path, options)
if pcsError != nil {
return nil, pcsError
}
defer dataReadCloser.Close()
jsonData := fdData{
PCSErrInfo: pcserror.NewPCSErrorInfo(OperationFilesDirectoriesList),
}
pcsError = pcserror.HandleJSONParse(OperationFilesDirectoriesList, dataReadCloser, (*fdDataJSONExport)(unsafe.Pointer(&jsonData)))
if pcsError != nil {
return nil, pcsError
}
// 修复MD5
jsonData.List.fixMD5()
data = jsonData.List
return
}
// Search 按文件名搜索文件, 不支持查找目录
func (pcs *BaiduPCS) Search(targetPath, keyword string, recursive bool) (fdl FileDirectoryList, pcsError pcserror.Error) {
if targetPath == "" {
targetPath = PathSeparator
}
dataReadCloser, pcsError := pcs.PrepareSearch(targetPath, keyword, recursive)
if pcsError != nil {
return nil, pcsError
}
defer dataReadCloser.Close()
errInfo := pcserror.NewPCSErrorInfo(OperationSearch)
jsonData := fdData{
PCSErrInfo: errInfo,
}
pcsError = pcserror.HandleJSONParse(OperationSearch, dataReadCloser, (*fdDataJSONExport)(unsafe.Pointer(&jsonData)))
if pcsError != nil {
return
}
// 修复MD5
jsonData.List.fixMD5()
fdl = jsonData.List
return
}
func (pcs *BaiduPCS) recurseList(path string, depth int, options *OrderOptions, handleFileDirectoryFunc HandleFileDirectoryFunc) (fdl FileDirectoryList, ok bool) {
fdl, pcsError := pcs.FilesDirectoriesList(path, options)
if pcsError != nil {
ok := handleFileDirectoryFunc(depth, path, nil, pcsError) // 传递错误
return nil, ok
}
for k := range fdl {
ok = handleFileDirectoryFunc(depth+1, fdl[k].Path, fdl[k], nil)
if !ok {
return
}
if !fdl[k].Isdir {
continue
}
fdl[k].Children, ok = pcs.recurseList(fdl[k].Path, depth+1, options, handleFileDirectoryFunc)
if !ok {
return
}
}
return fdl, true
}
// FilesDirectoriesRecurseList 递归获取目录下的文件和目录列表
func (pcs *BaiduPCS) FilesDirectoriesRecurseList(path string, options *OrderOptions, handleFileDirectoryFunc HandleFileDirectoryFunc) (data FileDirectoryList) {
fd, pcsError := pcs.FilesDirectoriesMeta(path)
if pcsError != nil {
handleFileDirectoryFunc(0, path, nil, pcsError) // 传递错误
return nil
}
if !fd.Isdir { // 不是一个目录
handleFileDirectoryFunc(0, path, fd, nil)
return FileDirectoryList{fd}
}
data, _ = pcs.recurseList(path, 0, options, handleFileDirectoryFunc)
return data
}
// fixMD5 尝试修复MD5字段
// 服务器返回的MD5字段不一定正确了, 即是BlockList只有一个md5
// MD5字段使用BlockList中的md5
func (f *FileDirectory) fixMD5() {
if len(f.BlockList) != 1 {
return
}
f.MD5 = f.BlockList[0]
}
func (f *FileDirectory) String() string {
builder := &strings.Builder{}
tb := pcstable.NewTable(builder)
tb.SetColumnAlignment([]int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
if f.Isdir {
tb.AppendBulk([][]string{
[]string{"类型", "目录"},
[]string{"目录路径", f.Path},
[]string{"目录名称", f.Filename},
})
} else {
var md5info string
if len(f.BlockList) > 1 {
md5info = "md5 (可能不正确)"
} else {
md5info = "md5 (截图请打码)"
}
tb.AppendBulk([][]string{
[]string{"类型", "文件"},
[]string{"文件路径", f.Path},
[]string{"文件名称", f.Filename},
[]string{"文件大小", strconv.FormatInt(f.Size, 10) + ", " + converter.ConvertFileSize(f.Size)},
[]string{md5info, f.MD5},
})
}
tb.Append([]string{"app_id", strconv.FormatInt(f.AppID, 10)})
tb.Append([]string{"fs_id", strconv.FormatInt(f.FsID, 10)})
tb.AppendBulk([][]string{
[]string{"创建日期", pcstime.FormatTime(f.Ctime)},
[]string{"修改日期", pcstime.FormatTime(f.Mtime)},
})
if f.Ifhassubdir {
tb.Append([]string{"是否含有子目录", "true"})
}
tb.Render()
return builder.String()
}
func (fl FileDirectoryList) fixMD5() {
for _, v := range fl {
v.fixMD5()
}
}
// TotalSize 获取目录下文件的总大小
func (fl FileDirectoryList) TotalSize() int64 {
var size int64
for k := range fl {
if fl[k] == nil {
continue
}
size += fl[k].Size
// 递归获取
if fl[k].Children != nil {
size += fl[k].Children.TotalSize()
}
}
return size
}
// Count 获取文件总数和目录总数
func (fl FileDirectoryList) Count() (fileN, directoryN int64) {
for k := range fl {
if fl[k] == nil {
continue
}
if fl[k].Isdir {
directoryN++
} else {
fileN++
}
// 递归获取
if fl[k].Children != nil {
fN, dN := fl[k].Children.Count()
fileN += fN
directoryN += dN
}
}
return
}
// AllFilePaths 返回所有的网盘路径, 包括子目录
func (fl FileDirectoryList) AllFilePaths() (pcspaths []string) {
fN, dN := fl.Count()
pcspaths = make([]string, 0, fN+dN)
for k := range fl {
if fl[k] == nil {
continue
}
pcspaths = append(pcspaths, fl[k].Path)
if fl[k].Children != nil {
pcspaths = append(pcspaths, fl[k].Children.AllFilePaths()...)
}
}
return
}

View File

@@ -0,0 +1,29 @@
package panhome
import (
"github.com/iikira/BaiduPCS-Go/baidupcs/expires"
"time"
)
// SetSignExpires 设置sign过期
func (ph *PanHome) SetSignExpires() {
if ph.signExpires != nil {
ph.signExpires.SetExpires(true)
}
}
// CacheSignature 在有效期内返回缓存结果
func (ph *PanHome) CacheSignature() (sign SignRes, err error) {
if ph.signExpires == nil || ph.signExpires.IsExpires() {
// 先签名再设置有效期
ph.signRes, err = ph.Signature()
if err != nil { // 空指针与空接口不等价
return nil, err
}
ph.signExpires = expires.NewExpires(1 * time.Hour) // 设置一小时有效期
return ph.signRes, nil
}
return ph.signRes, nil
}

View File

@@ -0,0 +1,50 @@
package panhome
import (
"github.com/iikira/BaiduPCS-Go/baidupcs/expires"
"github.com/iikira/BaiduPCS-Go/requester"
"net/url"
)
const (
// OperationSignature signature
OperationSignature = "signature"
)
var (
panBaiduComURL = &url.URL{
Scheme: "https",
Host: "pan.baidu.com",
}
// PanHomeUserAgent PanHome User-Agent
PanHomeUserAgent = "Mozilla/5.0"
)
type (
PanHome struct {
client *requester.HTTPClient
ua string
bduss string
sign1, sign3 []rune
timestamp string
signRes SignRes
signExpires expires.Expires
}
)
func NewPanHome(client *requester.HTTPClient) *PanHome {
ph := PanHome{}
if client != nil {
newC := *client
ph.client = &newC
}
return &ph
}
func (ph *PanHome) lazyInit() {
if ph.client == nil {
ph.client = requester.NewHTTPClient()
}
}

View File

@@ -0,0 +1,66 @@
package panhome
import (
"errors"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"unsafe"
)
var (
signInfoRE = regexp.MustCompile(`"sign1":"(.*?)"[\s\S]*"sign3":"(.*?)","timestamp":(\d*?),`)
ErrCookieInvalid = errors.New("cookie is invalid")
ErrUnknownLocation = errors.New("unknown location")
ErrMatchPanHome = errors.New("网盘首页数据匹配出错")
)
func (ph *PanHome) getSignInfo() error {
ph.client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
u := *panBaiduComURL
u.Path = "/disk/home"
resp, err := ph.client.Req(http.MethodGet, u.String(), nil, map[string]string{
"User-Agent": PanHomeUserAgent,
})
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
return err
}
loc := resp.Header.Get("Location")
switch loc {
case "/":
return ErrCookieInvalid
case "":
//pass
default:
locU, err := url.Parse(loc)
if err != nil {
return ErrUnknownLocation
}
if locU.Host == "passport.baidu.com" {
return ErrCookieInvalid
}
return ErrUnknownLocation
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
matchRes := signInfoRE.FindSubmatch(body)
if len(matchRes) <= 3 {
return ErrMatchPanHome
}
ph.sign1 = []rune(*(*string)(unsafe.Pointer(&matchRes[1])))
ph.sign3 = []rune(*(*string)(unsafe.Pointer(&matchRes[2])))
ph.timestamp = *(*string)(unsafe.Pointer(&matchRes[3]))
return nil
}

View File

@@ -0,0 +1,41 @@
package panhome
import (
"github.com/iikira/Baidu-Login/bdcrypto"
"github.com/iikira/BaiduPCS-Go/baidupcs/netdisksign"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
)
type (
// SignRes 签名结果
SignRes interface {
Sign() string
Timestamp() string
}
signRes struct {
sign string
timestamp string
}
)
func (sr *signRes) Sign() string {
return sr.sign
}
func (sr *signRes) Timestamp() string {
return sr.timestamp
}
func (ph *PanHome) Signature() (sign SignRes, err error) {
err = ph.getSignInfo()
if err != nil {
return nil, err
}
o := netdisksign.Sign2(ph.sign3, ph.sign1)
signed := bdcrypto.Base64Encode(o)
return &signRes{
sign: converter.ToString(signed),
timestamp: ph.timestamp,
}, nil
}

114
baidupcs/jsontable.go Normal file
View File

@@ -0,0 +1,114 @@
package baidupcs
import (
"errors"
"github.com/iikira/BaiduPCS-Go/pcstable"
"github.com/iikira/BaiduPCS-Go/pcsutil"
"github.com/json-iterator/go"
"path"
"strconv"
"strings"
)
type (
// PathJSON 网盘路径
PathJSON struct {
Path string `json:"path"`
}
// PathsListJSON 网盘路径列表
PathsListJSON struct {
List []*PathJSON `json:"list"`
}
// FsIDJSON 文件或目录ID
FsIDJSON struct {
FsID int64 `json:"fs_id"` // fs_id
}
// FsIDListJSON fs_id 列表
FsIDListJSON struct {
List []*FsIDJSON `json:"list"`
}
// CpMvJSON 源文件目录的地址和目标文件目录的地址
CpMvJSON struct {
From string `json:"from"` // 源文件或目录
To string `json:"to"` // 目标文件或目录
}
// CpMvJSONList CpMvJSON 列表
CpMvJSONList []*CpMvJSON
// CpMvListJSON []*CpMvJSON 对象数组
CpMvListJSON struct {
List CpMvJSONList `json:"list"`
}
// BlockListJSON 文件分块信息JSON
BlockListJSON struct {
BlockList []string `json:"block_list"`
}
)
var (
// ErrNilJSONValue 解析出的json值为空
ErrNilJSONValue = errors.New("json value is nil")
)
// JSON json 数据构造
func (plj *PathsListJSON) JSON(paths ...string) (data []byte, err error) {
plj.List = make([]*PathJSON, len(paths))
for k := range paths {
plj.List[k] = &PathJSON{
Path: paths[k],
}
}
data, err = jsoniter.Marshal(plj)
return
}
// JSON json 数据构造
func (cj *CpMvJSON) JSON() (data []byte, err error) {
data, err = jsoniter.Marshal(cj)
return
}
// JSON json 数据构造
func (clj *CpMvListJSON) JSON() (data []byte, err error) {
data, err = jsoniter.Marshal(clj)
return
}
func (clj *CpMvListJSON) String() string {
builder := &strings.Builder{}
tb := pcstable.NewTable(builder)
tb.SetHeader([]string{"#", "原路径", "目标路径"})
for k := range clj.List {
if clj.List[k] == nil {
continue
}
tb.Append([]string{strconv.Itoa(k), clj.List[k].From, clj.List[k].To})
}
tb.Render()
return builder.String()
}
// AllRelatedDir 获取所有相关的目录
func (cjl *CpMvJSONList) AllRelatedDir() (dirs []string) {
for _, cj := range *cjl {
fromDir, toDir := path.Dir(cj.From), path.Dir(cj.To)
if !pcsutil.ContainsString(dirs, fromDir) {
dirs = append(dirs, fromDir)
}
if !pcsutil.ContainsString(dirs, toDir) {
dirs = append(dirs, toDir)
}
}
return
}

View File

@@ -0,0 +1,20 @@
package netdisksign
import (
"bytes"
"crypto/md5"
"encoding/hex"
"github.com/iikira/BaiduPCS-Go/pcsutil/cachepool"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
)
func DevUID(feature string) string {
m := md5.New()
m.Write(converter.ToBytes(feature))
res := m.Sum(nil)
resHex := cachepool.RawMallocByteSlice(34)
hex.Encode(resHex[2:], res)
resHex[0] = 'O'
resHex[1] = '|'
return converter.ToString(bytes.ToUpper(resHex))
}

View File

@@ -0,0 +1,52 @@
package netdisksign
import (
"crypto/sha1"
"encoding/hex"
"github.com/iikira/BaiduPCS-Go/pcsutil/cachepool"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"strconv"
"time"
)
type (
LocateDownloadSign struct {
Time int64
Rand string
DevUID string
}
)
func NewLocateDownloadSign(uid uint64, bduss string) *LocateDownloadSign {
return NewLocateDownloadSignWithTimeAndDevUID(time.Now().Unix(), DevUID(bduss), uid, bduss)
}
func NewLocateDownloadSignWithTimeAndDevUID(timeunix int64, devuid string, uid uint64, bduss string) *LocateDownloadSign {
l := &LocateDownloadSign{
Time: timeunix,
DevUID: devuid,
}
l.Sign(uid, bduss)
return l
}
func (s *LocateDownloadSign) Sign(uid uint64, bduss string) {
randSha1 := sha1.New()
bdussSha1 := sha1.New()
bdussSha1.Write(converter.ToBytes(bduss))
sha1ResHex := cachepool.RawMallocByteSlice(40)
hex.Encode(sha1ResHex, bdussSha1.Sum(nil))
randSha1.Write(sha1ResHex)
uidStr := strconv.FormatUint(uid, 10)
randSha1.Write(converter.ToBytes(uidStr))
randSha1.Write([]byte{'\x65', '\x62', '\x72', '\x63', '\x55', '\x59', '\x69', '\x75', '\x78', '\x61', '\x5a', '\x76', '\x32', '\x58', '\x47', '\x75', '\x37', '\x4b', '\x49', '\x59', '\x4b', '\x78', '\x55', '\x72', '\x71', '\x66', '\x6e', '\x4f', '\x66', '\x70', '\x44', '\x46'})
timeStr := strconv.FormatInt(s.Time, 10)
randSha1.Write(converter.ToBytes(timeStr))
randSha1.Write(converter.ToBytes(s.DevUID))
hex.Encode(sha1ResHex, randSha1.Sum(nil))
s.Rand = converter.ToString(sha1ResHex)
}
func (s *LocateDownloadSign) URLParam() string {
return "time=" + strconv.FormatInt(s.Time, 10) + "&rand=" + s.Rand + "&devuid=" + s.DevUID
}

View File

@@ -0,0 +1,12 @@
package netdisksign_test
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs/netdisksign"
"testing"
)
func TestLocateDownloadSign(t *testing.T) {
sign := netdisksign.NewLocateDownloadSignWithTimeAndDevUID(1571140066, "O|1E67351CCE80B2CF48DB511CD77ACD9F", 10086, "test_bduss")
fmt.Printf("%#v\n", sign.Rand == "b6bb7a6f46899e181baea58798d4fdb889775c2c")
}

View File

@@ -0,0 +1,20 @@
package netdisksign
import (
"crypto/md5"
"encoding/hex"
"github.com/iikira/BaiduPCS-Go/pcsutil/cachepool"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"strconv"
)
func ShareSURLInfoSign(shareID int64) []byte {
s := strconv.FormatInt(shareID, 10)
m := md5.New()
m.Write(converter.ToBytes(s))
m.Write([]byte("_sharesurlinfo!@#"))
res := m.Sum(nil)
resHex := cachepool.RawMallocByteSlice(32)
hex.Encode(resHex, res)
return resHex
}

View File

@@ -0,0 +1,36 @@
package netdisksign
func Sign2(j, r []rune) []byte {
var (
a = make([]rune, 256)
p = make([]rune, 256)
o = make([]byte, len(r))
v = len(j)
q int
u rune
i int
k rune
dr int
)
if v == 0 {
return o
}
for ; q < 256; q++ {
dr = q % v
a[q] = j[dr : 1+dr][0]
p[q] = rune(q)
}
for q = 0; q < 256; q++ {
u = (u + p[q] + a[q]) % 256
p[q], p[u] = p[u], p[q]
}
u = 0
for q = 0; q < len(r); q++ {
i = (i + 1) % 256
u = (u + p[i]) % 256
p[i], p[u] = p[u], p[i]
k = p[(p[i]+p[u])%256]
o[q] = byte(r[q] ^ k)
}
return o
}

View File

@@ -0,0 +1,62 @@
function s(j, r) {
var a = [];
var p = [];
var o = "";
var v = j.length;
for (var q = 0; q < 256; q++) {
a[q] = j.substr((q % v), 1).charCodeAt(0);
// console.log(q, q%v);
p[q] = q;
// console.log(q, a[q], p[q]);
}
for (var u = q = 0; q < 256; q++) {
u = (u + p[q] + a[q]) % 256;
var t = p[q];
p[q] = p[u];
p[u] = t;
}
// console.log(p)
for (var i = u = q = 0; q < r.length; q++) {
i = (i + 1) % 256;
u = (u + p[i]) % 256;
var t = p[i];
p[i] = p[u];
p[u] = t;
k = p[((p[i] + p[u]) % 256)];
o += String.fromCharCode(r.charCodeAt(q) ^ k);
}
return o;
};
function base64encode(t) {
var r, e, a, o, n, i, s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for (a = t.length,
e = 0,
r = ""; a > e; ) {
if (o = 255 & t.charCodeAt(e++),
e == a) {
r += s.charAt(o >> 2),
r += s.charAt((3 & o) << 4),
r += "==";
break
}
if (n = t.charCodeAt(e++),
e == a) {
r += s.charAt(o >> 2),
r += s.charAt((3 & o) << 4 | (240 & n) >> 4),
r += s.charAt((15 & n) << 2),
r += "=";
break
}
i = t.charCodeAt(e++),
r += s.charAt(o >> 2),
r += s.charAt((3 & o) << 4 | (240 & n) >> 4),
r += s.charAt((15 & n) << 2 | (192 & i) >> 6),
r += s.charAt(63 & i)
}
return r
}
var s1 = s("e8c7d729eea7b54551aa594f942decbe", "37dbe07ade9359c1aa70807e847f768c13360ad2");
console.log(s1);
console.log(base64encode(s1));

View File

@@ -0,0 +1,20 @@
package netdisksign_test
import (
"fmt"
"github.com/iikira/Baidu-Login/bdcrypto"
"github.com/iikira/BaiduPCS-Go/baidupcs/netdisksign"
"testing"
)
func TestSign2(t *testing.T) {
standard := bdcrypto.Base64Decode([]byte("8RxCbsVeSzn2UjxJAAiV9QQs/WetOj2FJUGwjsMG6SgxFMWlLS/U1Q=="))
fmt.Println("standard,", standard)
fmt.Printf("standard s %s\n", standard)
res := netdisksign.Sign2([]rune("e8c7d729eea7b54551aa594f942decbe"), []rune("37dbe07ade9359c1aa70807e847f768c13360ad2"))
fmt.Println(res)
fmt.Printf("%s\n", string(res))
fmt.Println([]byte(string(res)))
fmt.Println(bdcrypto.Base64Encode([]byte(string(res))))
}

View File

@@ -0,0 +1,97 @@
package pcserror
import (
"fmt"
)
type (
// DlinkErrInfo dlink服务器错误信息
DlinkErrInfo struct {
Operation string
ErrType ErrType
Err error
ErrNo int `json:"errno"`
Msg string `json:"msg"`
}
)
// NewDlinkErrInfo 初始化DlinkErrInfo
func NewDlinkErrInfo(op string) *DlinkErrInfo {
return &DlinkErrInfo{
Operation: op,
}
}
// SetJSONError 设置JSON错误
func (dle *DlinkErrInfo) SetJSONError(err error) {
dle.ErrType = ErrTypeJSONParseError
dle.Err = err
}
// SetNetError 设置网络错误
func (dle *DlinkErrInfo) SetNetError(err error) {
dle.ErrType = ErrTypeNetError
dle.Err = err
}
// SetRemoteError 设置远端服务器错误
func (dle *DlinkErrInfo) SetRemoteError() {
dle.ErrType = ErrTypeRemoteError
}
// GetOperation 获取操作
func (dle *DlinkErrInfo) GetOperation() string {
return dle.Operation
}
// GetErrType 获取错误类型
func (dle *DlinkErrInfo) GetErrType() ErrType {
return dle.ErrType
}
// GetRemoteErrCode 获取远端服务器错误代码
func (dle *DlinkErrInfo) GetRemoteErrCode() int {
return dle.ErrNo
}
// GetRemoteErrMsg 获取远端服务器错误消息
func (dle *DlinkErrInfo) GetRemoteErrMsg() string {
return dle.Msg
}
// GetError 获取原始错误
func (dle *DlinkErrInfo) GetError() error {
return dle.Err
}
func (dle *DlinkErrInfo) Error() string {
if dle.Operation == "" {
if dle.Err != nil {
return dle.Err.Error()
}
return StrSuccess
}
switch dle.ErrType {
case ErrTypeInternalError:
return fmt.Sprintf("%s: %s, %s", dle.Operation, StrInternalError, dle.Err)
case ErrTypeJSONParseError:
return fmt.Sprintf("%s: %s, %s", dle.Operation, StrJSONParseError, dle.Err)
case ErrTypeNetError:
return fmt.Sprintf("%s: %s, %s", dle.Operation, StrNetError, dle.Err)
case ErrTypeRemoteError:
if dle.ErrNo == 0 {
return fmt.Sprintf("%s: %s", dle.Operation, StrSuccess)
}
return fmt.Sprintf("%s: 遇到错误, %s, 代码: %d, 消息: %s", dle.Operation, StrRemoteError, dle.ErrNo, dle.Msg)
case ErrTypeOthers:
if dle.Err == nil {
return fmt.Sprintf("%s: %s", dle.Operation, StrSuccess)
}
return fmt.Sprintf("%s, 遇到错误, %s", dle.Operation, dle.Err)
default:
panic("dlinkerrinfo: unknown ErrType")
}
}

View File

@@ -0,0 +1,177 @@
package pcserror
import (
"fmt"
)
type (
// PanErrorInfo 网盘网页的api错误
PanErrorInfo struct {
Operation string
ErrType ErrType
Err error
ErrNo int `json:"errno"`
// ErrMsg string `json:"err_msg"`
}
)
// NewPanErrorInfo 提供operation操作名称, 返回 *PanErrorInfo
func NewPanErrorInfo(operation string) *PanErrorInfo {
return &PanErrorInfo{
Operation: operation,
ErrType: ErrorTypeNoError,
}
}
// SetJSONError 设置JSON错误
func (pane *PanErrorInfo) SetJSONError(err error) {
pane.ErrType = ErrTypeJSONParseError
pane.Err = err
}
// SetNetError 设置网络错误
func (pane *PanErrorInfo) SetNetError(err error) {
pane.ErrType = ErrTypeNetError
pane.Err = err
}
// SetRemoteError 设置远端服务器错误
func (pane *PanErrorInfo) SetRemoteError() {
pane.ErrType = ErrTypeRemoteError
}
// GetOperation 获取操作
func (pane *PanErrorInfo) GetOperation() string {
return pane.Operation
}
// GetErrType 获取错误类型
func (pane *PanErrorInfo) GetErrType() ErrType {
return pane.ErrType
}
// GetRemoteErrCode 获取远端服务器错误代码
func (pane *PanErrorInfo) GetRemoteErrCode() int {
return pane.ErrNo
}
// GetRemoteErrMsg 获取远端服务器错误消息
func (pane *PanErrorInfo) GetRemoteErrMsg() string {
return FindPanErr(pane.ErrNo)
}
// GetError 获取原始错误
func (pane *PanErrorInfo) GetError() error {
return pane.Err
}
func (pane *PanErrorInfo) Error() string {
if pane.Operation == "" {
if pane.Err != nil {
return pane.Err.Error()
}
return StrSuccess
}
switch pane.ErrType {
case ErrTypeInternalError:
return fmt.Sprintf("%s: %s, %s", pane.Operation, StrInternalError, pane.Err)
case ErrTypeJSONParseError:
return fmt.Sprintf("%s: %s, %s", pane.Operation, StrJSONParseError, pane.Err)
case ErrTypeNetError:
return fmt.Sprintf("%s: %s, %s", pane.Operation, StrNetError, pane.Err)
case ErrTypeRemoteError:
if pane.ErrNo == 0 {
return fmt.Sprintf("%s: %s", pane.Operation, StrSuccess)
}
errmsg := FindPanErr(pane.ErrNo)
return fmt.Sprintf("%s: 遇到错误, %s, 代码: %d, 消息: %s", pane.Operation, StrRemoteError, pane.ErrNo, errmsg)
case ErrTypeOthers:
if pane.Err == nil {
return fmt.Sprintf("%s: %s", pane.Operation, StrSuccess)
}
return fmt.Sprintf("%s, 遇到错误, %s", pane.Operation, pane.Err)
default:
panic("panerrorinfo: unknown ErrType")
}
}
// FindPanErr 根据 ErrNo, 解析网盘错误信息
func FindPanErr(errno int) (errmsg string) {
switch errno {
case 0:
return StrSuccess
case -1:
return "由于您分享了违反相关法律法规的文件,分享功能已被禁用,之前分享出去的文件不受影响。"
case -2:
return "用户不存在,请刷新页面后重试"
case -3:
return "文件不存在,请刷新页面后重试"
case -4:
return "登录信息有误,请重新登录试试"
case -5:
return "host_key和user_key无效"
case -6:
return "请重新登录"
case -7:
return "该分享已删除或已取消"
case -8:
return "该分享已经过期"
case -9:
return "文件不存在"
case -10:
return "分享外链已经达到最大上限100000条不能再次分享"
case -11:
return "验证cookie无效"
case -12:
return "访问密码错误"
case -14:
return "对不起短信分享每天限制20条你今天已经分享完请明天再来分享吧"
case -15:
return "对不起邮件分享每天限制20封你今天已经分享完请明天再来分享吧"
case -16:
return "对不起,该文件已经限制分享!"
case -17:
return "文件分享超过限制"
case -19:
return "需要输入验证码"
case -21:
return "分享已取消或分享信息无效"
case -30:
return "文件已存在"
case -31:
return "文件保存失败"
case -33:
return "一次支持操作999个减点试试吧"
case -62:
return "可能需要输入验证码"
case -70:
return "你分享的文件中包含病毒或疑似病毒,为了你和他人的数据安全,换个文件分享吧"
case 2:
return "参数错误"
case 3:
return "未登录或帐号无效"
case 4:
return "存储好像出问题了,请稍候再试"
case 105:
return "啊哦,链接错误没找到文件,请打开正确的分享链接"
case 108:
return "文件名有敏感词,优化一下吧"
case 110:
return "分享次数超出限制,可以到“我的分享”中查看已分享的文件链接"
case 112:
return "页面已过期,请刷新后重试"
case 113:
return "签名错误"
case 114:
return "当前任务不存在,保存失败"
case 115:
return "该文件禁止分享"
case 132:
return "您的帐号可能存在安全风险,为了确保为您本人操作,请先进行安全验证。"
default:
return "未知错误"
}
}

View File

@@ -0,0 +1,90 @@
// Package pcserror PCS错误包
package pcserror
import (
"github.com/iikira/BaiduPCS-Go/pcsutil/jsonhelper"
"io"
)
type (
// ErrType 错误类型
ErrType int
// Error 错误信息接口
Error interface {
error
SetJSONError(err error)
SetNetError(err error)
SetRemoteError()
GetOperation() string
GetErrType() ErrType
GetRemoteErrCode() int
GetRemoteErrMsg() string
GetError() error
}
)
const (
// ErrorTypeNoError 无错误
ErrorTypeNoError ErrType = iota
// ErrTypeInternalError 内部错误
ErrTypeInternalError
// ErrTypeRemoteError 远端服务器返回错误
ErrTypeRemoteError
// ErrTypeNetError 网络错误
ErrTypeNetError
// ErrTypeJSONParseError json 数据解析失败
ErrTypeJSONParseError
// ErrTypeOthers 其他错误
ErrTypeOthers
)
const (
// StrSuccess 操作成功
StrSuccess = "操作成功"
// StrInternalError 内部错误
StrInternalError = "内部错误"
// StrRemoteError 远端服务器返回错误
StrRemoteError = "远端服务器返回错误"
// StrNetError 网络错误
StrNetError = "网络错误"
// StrJSONParseError json 数据解析失败
StrJSONParseError = "json 数据解析失败"
)
// DecodePCSJSONError 解析PCS JSON的错误
func DecodePCSJSONError(opreation string, data io.Reader) Error {
errInfo := NewPCSErrorInfo(opreation)
return HandleJSONParse(opreation, data, errInfo)
}
// DecodePanJSONError 解析Pan JSON的错误
func DecodePanJSONError(opreation string, data io.Reader) Error {
errInfo := NewPanErrorInfo(opreation)
return HandleJSONParse(opreation, data, errInfo)
}
// HandleJSONParse 处理解析json
func HandleJSONParse(op string, data io.Reader, info interface{}) (pcsError Error) {
var (
err = jsonhelper.UnmarshalData(data, info)
errInfo = info.(Error)
)
if errInfo == nil {
errInfo = NewPCSErrorInfo(op)
}
if err != nil {
errInfo.SetJSONError(err)
return errInfo
}
// 设置出错类型为远程错误
if errInfo.GetRemoteErrCode() != 0 {
errInfo.SetRemoteError()
return errInfo
}
return nil
}

View File

@@ -0,0 +1,115 @@
package pcserror
import (
"fmt"
)
type (
// PCSErrInfo PCS错误信息
PCSErrInfo struct {
Operation string // 正在进行的操作
ErrType ErrType
Err error
ErrCode int `json:"error_code"` // 错误代码
ErrMsg string `json:"error_msg"` // 错误消息
}
)
// NewPCSErrorInfo 提供operation操作名称, 返回 *PCSErrInfo
func NewPCSErrorInfo(operation string) *PCSErrInfo {
return &PCSErrInfo{
Operation: operation,
ErrType: ErrorTypeNoError,
}
}
// SetJSONError 设置JSON错误
func (pcse *PCSErrInfo) SetJSONError(err error) {
pcse.ErrType = ErrTypeJSONParseError
pcse.Err = err
}
// SetNetError 设置网络错误
func (pcse *PCSErrInfo) SetNetError(err error) {
pcse.ErrType = ErrTypeNetError
pcse.Err = err
}
// SetRemoteError 设置远端服务器错误
func (pcse *PCSErrInfo) SetRemoteError() {
pcse.ErrType = ErrTypeRemoteError
}
// GetOperation 获取操作
func (pcse *PCSErrInfo) GetOperation() string {
return pcse.Operation
}
// GetErrType 获取错误类型
func (pcse *PCSErrInfo) GetErrType() ErrType {
return pcse.ErrType
}
// GetRemoteErrCode 获取远端服务器错误代码
func (pcse *PCSErrInfo) GetRemoteErrCode() int {
return pcse.ErrCode
}
// GetRemoteErrMsg 获取远端服务器错误消息
func (pcse *PCSErrInfo) GetRemoteErrMsg() string {
_, msg := findPCSErr(pcse.ErrCode, pcse.ErrMsg)
return msg
}
// GetError 获取原始错误
func (pcse *PCSErrInfo) GetError() error {
return pcse.Err
}
func (pcse *PCSErrInfo) Error() string {
if pcse.Operation == "" {
if pcse.Err != nil {
return pcse.Err.Error()
}
return StrSuccess
}
switch pcse.ErrType {
case ErrTypeInternalError:
return fmt.Sprintf("%s: %s, %s", pcse.Operation, StrInternalError, pcse.Err)
case ErrTypeJSONParseError:
return fmt.Sprintf("%s: %s, %s", pcse.Operation, StrJSONParseError, pcse.Err)
case ErrTypeNetError:
return fmt.Sprintf("%s: %s, %s", pcse.Operation, StrNetError, pcse.Err)
case ErrTypeRemoteError:
if pcse.ErrCode == 0 {
return fmt.Sprintf("%s: %s", pcse.Operation, StrSuccess)
}
code, msg := findPCSErr(pcse.ErrCode, pcse.ErrMsg)
return fmt.Sprintf("%s: 遇到错误, %s, 代码: %d, 消息: %s", pcse.Operation, StrRemoteError, code, msg)
case ErrTypeOthers:
if pcse.Err == nil {
return fmt.Sprintf("%s: %s", pcse.Operation, StrSuccess)
}
return fmt.Sprintf("%s, 遇到错误, %s", pcse.Operation, pcse.Err)
default:
panic("pcserrorinfo: unknown ErrType")
}
}
// findPCSErr 检查 PCS 错误, 查找已知错误
func findPCSErr(errCode int, errMsg string) (int, string) {
switch errCode {
case 0:
return errCode, ""
case 31045: // user not exists
return errCode, "操作失败, 可能百度帐号登录状态过期, 请尝试重新登录, 消息: " + errMsg
case 31066: // file does not exist
return errCode, "文件或目录不存在"
case 31079: // file md5 not found, you should use upload api to upload the whole file.
return errCode, "秒传文件失败"
}
return errCode, errMsg
}

743
baidupcs/prepare.go Normal file
View File

@@ -0,0 +1,743 @@
package baidupcs
import (
"bytes"
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs/netdisksign"
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"github.com/iikira/BaiduPCS-Go/requester/multipartreader"
"github.com/iikira/baidu-tools/tieba"
"github.com/json-iterator/go"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"unsafe"
)
type (
reqType int
)
const (
reqTypePCS = iota
reqTypePan
)
func handleRespClose(resp *http.Response) error {
if resp != nil {
return resp.Body.Close()
}
return nil
}
func handleRespStatusError(opreation string, resp *http.Response) pcserror.Error {
errInfo := pcserror.NewPCSErrorInfo(opreation)
// http 响应错误处理
switch resp.StatusCode / 100 {
case 4, 5:
resp.Body.Close()
errInfo.SetNetError(fmt.Errorf("http 响应错误, %s", resp.Status))
return errInfo
}
return nil
}
func (pcs *BaiduPCS) sendReqReturnResp(rt reqType, op, method, urlStr string, post interface{}, header map[string]string) (resp *http.Response, pcsError pcserror.Error) {
if header == nil {
header = map[string]string{}
}
var (
_, uaok = header["User-Agent"]
)
if !uaok {
switch rt {
case reqTypePCS:
header["User-Agent"] = pcs.pcsUA
case reqTypePan:
header["User-Agent"] = pcs.panUA
}
}
resp, err := pcs.client.Req(method, urlStr, post, header)
if err != nil {
handleRespClose(resp)
switch rt {
case reqTypePCS:
return nil, &pcserror.PCSErrInfo{
Operation: op,
ErrType: pcserror.ErrTypeNetError,
Err: err,
}
case reqTypePan:
return nil, &pcserror.PanErrorInfo{
Operation: op,
ErrType: pcserror.ErrTypeNetError,
Err: err,
}
}
panic("unreachable")
}
return resp, nil
}
func (pcs *BaiduPCS) sendReqReturnReadCloser(rt reqType, op, method, urlStr string, post interface{}, header map[string]string) (readCloser io.ReadCloser, pcsError pcserror.Error) {
resp, pcsError := pcs.sendReqReturnResp(rt, op, method, urlStr, post, header)
if pcsError != nil {
return
}
return resp.Body, nil
}
// PrepareUK 获取用户 UK, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareUK() (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
query := url.Values{}
query.Set("need_selfinfo", "1")
panURL := &url.URL{
Scheme: "https",
Host: PanBaiduCom,
Path: "api/user/getinfo",
RawQuery: query.Encode(),
}
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, OperationGetUK, http.MethodGet, panURL.String(), nil, nil)
return
}
// PrepareQuotaInfo 获取当前用户空间配额信息, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareQuotaInfo() (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
pcsURL := pcs.generatePCSURL("quota", "info")
baiduPCSVerbose.Infof("%s URL: %s\n", OperationQuotaInfo, pcsURL)
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, OperationQuotaInfo, http.MethodGet, pcsURL.String(), nil, nil)
return
}
// PrepareFilesDirectoriesBatchMeta 获取多个文件/目录的元信息, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareFilesDirectoriesBatchMeta(paths ...string) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
sendData, err := (&PathsListJSON{}).JSON(paths...)
if err != nil {
panic(OperationFilesDirectoriesMeta + ", json 数据构造失败, " + err.Error())
}
pcsURL := pcs.generatePCSURL("file", "meta")
baiduPCSVerbose.Infof("%s URL: %s\n", OperationFilesDirectoriesMeta, pcsURL)
// 表单上传
mr := multipartreader.NewMultipartReader()
mr.AddFormFeild("param", bytes.NewReader(sendData))
mr.CloseMultipart()
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, OperationFilesDirectoriesMeta, http.MethodPost, pcsURL.String(), mr, nil)
return
}
// PrepareFilesDirectoriesList 获取目录下的文件和目录列表, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareFilesDirectoriesList(path string, options *OrderOptions) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
if options == nil {
options = DefaultOrderOptions
}
if path == "" {
path = PathSeparator
}
pcsURL := pcs.generatePCSURL("file", "list", map[string]string{
"path": path,
"by": *(*string)(unsafe.Pointer(&options.By)),
"order": *(*string)(unsafe.Pointer(&options.Order)),
"limit": "0-2147483647",
})
baiduPCSVerbose.Infof("%s URL: %s\n", OperationFilesDirectoriesList, pcsURL)
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, OperationFilesDirectoriesList, http.MethodGet, pcsURL.String(), nil, nil)
return
}
// PrepareSearch 按文件名搜索文件, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareSearch(targetPath, keyword string, recursive bool) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
var re string
if recursive {
re = "1"
} else {
re = "0"
}
pcsURL := pcs.generatePCSURL("file", "search", map[string]string{
"path": targetPath,
"wd": keyword,
"re": re,
})
baiduPCSVerbose.Infof("%s URL: %s\n", OperationSearch, pcsURL)
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, OperationSearch, http.MethodGet, pcsURL.String(), nil, nil)
return
}
// PrepareRemove 批量删除文件/目录, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareRemove(paths ...string) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
sendData, err := (&PathsListJSON{}).JSON(paths...)
if err != nil {
panic(OperationMove + ", json 数据构造失败, " + err.Error())
}
pcsURL := pcs.generatePCSURL("file", "delete")
baiduPCSVerbose.Infof("%s URL: %s\n", OperationRemove, pcsURL)
// 表单上传
mr := multipartreader.NewMultipartReader()
mr.AddFormFeild("param", bytes.NewReader(sendData))
mr.CloseMultipart()
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, OperationRemove, http.MethodPost, pcsURL.String(), mr, nil)
return
}
// PrepareMkdir 创建目录, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareMkdir(pcspath string) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
pcsURL := pcs.generatePCSURL("file", "mkdir", map[string]string{
"path": pcspath,
})
baiduPCSVerbose.Infof("%s URL: %s", OperationMkdir, pcsURL)
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, OperationMkdir, http.MethodPost, pcsURL.String(), nil, nil)
return
}
func (pcs *BaiduPCS) prepareCpMvOp(op string, cpmvJSON ...*CpMvJSON) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
var method string
switch op {
case OperationCopy:
method = "copy"
case OperationMove, OperationRename:
method = "move"
default:
panic("Unknown opreation: " + op)
}
sendData, err := (&CpMvListJSON{
List: cpmvJSON,
}).JSON()
if err != nil {
//json 数据生成失败
panic(err)
}
pcsURL := pcs.generatePCSURL("file", method)
baiduPCSVerbose.Infof("%s URL: %s\n", op, pcsURL)
// 表单上传
mr := multipartreader.NewMultipartReader()
mr.AddFormFeild("param", bytes.NewReader(sendData))
mr.CloseMultipart()
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, op, http.MethodPost, pcsURL.String(), mr, nil)
return
}
// PrepareRename 重命名文件/目录, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareRename(from, to string) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
return pcs.prepareCpMvOp(OperationRename, &CpMvJSON{
From: from,
To: to,
})
}
// PrepareCopy 批量拷贝文件/目录, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareCopy(cpmvJSON ...*CpMvJSON) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
return pcs.prepareCpMvOp(OperationCopy, cpmvJSON...)
}
// PrepareMove 批量移动文件/目录, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareMove(cpmvJSON ...*CpMvJSON) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
return pcs.prepareCpMvOp(OperationMove, cpmvJSON...)
}
// prepareRapidUpload 秒传文件, 不进行文件夹检查
func (pcs *BaiduPCS) prepareRapidUpload(targetPath, contentMD5, sliceMD5, crc32 string, length int64) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
pcsURL := pcs.generatePCSURL("file", "rapidupload", map[string]string{
"path": targetPath, // 上传文件的全路径名
"content-length": strconv.FormatInt(length, 10), // 待秒传的文件长度
"content-md5": contentMD5, // 待秒传的文件的MD5
"slice-md5": sliceMD5, // 待秒传的文件前256kb的MD5
"content-crc32": crc32, // 待秒传文件CRC32
"ondup": "overwrite", // overwrite: 表示覆盖同名文件; newcopy: 表示生成文件副本并进行重命名命名规则为“文件名_日期.后缀”
})
baiduPCSVerbose.Infof("%s URL: %s\n", OperationRapidUpload, pcsURL)
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, OperationRapidUpload, http.MethodGet, pcsURL.String(), nil, nil)
return
}
// PrepareRapidUpload 秒传文件, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareRapidUpload(targetPath, contentMD5, sliceMD5, crc32 string, length int64) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
pcsError = pcs.checkIsdir(OperationRapidUpload, targetPath)
if pcsError != nil {
return nil, pcsError
}
return pcs.prepareRapidUpload(targetPath, contentMD5, sliceMD5, crc32, length)
}
// PrepareLocateDownload 获取下载链接, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareLocateDownload(pcspath string) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
bduss := pcs.GetBDUSS()
// 检测uid
if pcs.uid == 0 {
t, err := tieba.NewUserInfoByBDUSS(bduss)
if err != nil {
return nil, &pcserror.PCSErrInfo{
Operation: OperationLocateDownload,
ErrType: pcserror.ErrTypeNetError,
Err: err,
}
}
pcs.uid = t.Baidu.UID
}
ns := netdisksign.NewLocateDownloadSign(pcs.uid, bduss)
pcsURL := &url.URL{
Scheme: GetHTTPScheme(pcs.isHTTPS),
Host: PCSBaiduCom,
Path: "/rest/2.0/pcs/file",
RawQuery: (url.Values{
"app_id": []string{PanAppID},
"method": []string{"locatedownload"},
"path": []string{pcspath},
"ver": []string{"2"},
}).Encode() + "&" + ns.URLParam(),
}
baiduPCSVerbose.Infof("%s URL: %s\n", OperationLocateDownload, pcsURL)
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, OperationLocateDownload, http.MethodGet, pcsURL.String(), nil, pcs.getPanUAHeader())
return
}
// PrepareLocatePanAPIDownload 从百度网盘首页获取下载链接, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareLocatePanAPIDownload(fidList ...int64) (dataReadCloser io.ReadCloser, panError pcserror.Error) {
pcs.lazyInit()
// 初始化
var (
sign, err = pcs.ph.CacheSignature()
)
if err != nil {
return nil, &pcserror.PanErrorInfo{
Operation: OperationLocatePanAPIDownload,
ErrType: pcserror.ErrTypeOthers,
Err: err,
}
}
panURL := pcs.generatePanURL("download", nil)
baiduPCSVerbose.Infof("%s URL: %s\n", OperationLocatePanAPIDownload, panURL)
dataReadCloser, panError = pcs.sendReqReturnReadCloser(reqTypePan, OperationLocatePanAPIDownload, http.MethodPost, panURL.String(), map[string]string{
"sign": sign.Sign(),
"timestamp": sign.Timestamp(),
"fidlist": mergeInt64List(fidList...),
}, map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
})
return
}
// PrepareUpload 上传单个文件, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareUpload(targetPath string, uploadFunc UploadFunc) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
pcsError = pcs.checkIsdir(OperationUpload, targetPath)
if pcsError != nil {
return nil, pcsError
}
pcsURL := pcs.generatePCSURL("file", "upload", map[string]string{
"path": targetPath,
"ondup": "overwrite",
})
baiduPCSVerbose.Infof("%s URL: %s\n", OperationUpload, pcsURL)
resp, err := uploadFunc(pcsURL.String(), pcs.client.Jar)
if err != nil {
handleRespClose(resp)
return nil, &pcserror.PCSErrInfo{
Operation: OperationUpload,
ErrType: pcserror.ErrTypeNetError,
Err: err,
}
}
pcsError = handleRespStatusError(OperationUpload, resp)
if pcsError != nil {
return
}
return resp.Body, nil
}
// PrepareUploadTmpFile 分片上传—文件分片及上传, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareUploadTmpFile(uploadFunc UploadFunc) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
pcsURL := pcs.generatePCSURL("file", "upload", map[string]string{
"type": "tmpfile",
})
baiduPCSVerbose.Infof("%s URL: %s\n", OperationUploadTmpFile, pcsURL)
resp, err := uploadFunc(pcsURL.String(), pcs.client.Jar)
if err != nil {
handleRespClose(resp)
return nil, &pcserror.PCSErrInfo{
Operation: OperationUploadTmpFile,
ErrType: pcserror.ErrTypeNetError,
Err: err,
}
}
pcsError = handleRespStatusError(OperationUploadTmpFile, resp)
if pcsError != nil {
return
}
return resp.Body, nil
}
// PrepareUploadCreateSuperFile 分片上传—合并分片文件, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareUploadCreateSuperFile(checkDir bool, targetPath string, blockList ...string) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
if checkDir {
// 检查是否为目录
pcsError = pcs.checkIsdir(OperationUploadCreateSuperFile, targetPath)
if pcsError != nil {
return nil, pcsError
}
}
bl := BlockListJSON{
BlockList: blockList,
}
sendData, err := jsoniter.Marshal(&bl)
if err != nil {
panic(err)
}
pcsURL := pcs.generatePCSURL("file", "createsuperfile", map[string]string{
"path": targetPath,
"ondup": "overwrite",
})
baiduPCSVerbose.Infof("%s URL: %s\n", OperationUploadCreateSuperFile, pcsURL)
// 表单上传
mr := multipartreader.NewMultipartReader()
mr.AddFormFeild("param", bytes.NewReader(sendData))
mr.CloseMultipart()
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, OperationUploadCreateSuperFile, http.MethodPost, pcsURL.String(), mr, nil)
return
}
// PrepareUploadPrecreate 分片上传—Precreate, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareUploadPrecreate(targetPath, contentMD5, sliceMD5, crc32 string, size int64, bolckList ...string) (dataReadCloser io.ReadCloser, panError pcserror.Error) {
pcs.lazyInit()
panURL := &url.URL{
Scheme: "https",
Host: PanBaiduCom,
Path: "api/precreate",
}
baiduPCSVerbose.Infof("%s URL: %s\n", OperationUploadPrecreate, panURL)
dataReadCloser, panError = pcs.sendReqReturnReadCloser(reqTypePan, OperationUploadPrecreate, http.MethodPost, panURL.String(), map[string]string{
"path": targetPath,
"size": strconv.FormatInt(size, 10),
"isdir": "0",
"block_list": mergeStringList(bolckList...),
"autoinit": "1",
"content-md5": contentMD5,
"slice-md5": sliceMD5,
"contentCrc32": crc32,
"rtype": "2",
}, map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
})
return
}
// PrepareUploadSuperfile2 另一个上传接口
func (pcs *BaiduPCS) PrepareUploadSuperfile2(uploadid, targetPath string, partseq int, partOffset int64, uploadFunc UploadFunc) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
pcsURL := pcs.generatePCSURL("superfile2", "upload", map[string]string{
"type": "tmpfile",
"path": targetPath,
"partseq": strconv.Itoa(partseq),
"partoffset": strconv.FormatInt(partOffset, 10),
"uploadid": uploadid,
"vip": "1",
})
baiduPCSVerbose.Infof("%s URL: %s\n", OperationUploadSuperfile2, pcsURL)
resp, err := uploadFunc(pcsURL.String(), pcs.client.Jar)
if err != nil {
handleRespClose(resp)
return nil, &pcserror.PCSErrInfo{
Operation: OperationUploadSuperfile2,
ErrType: pcserror.ErrTypeNetError,
Err: err,
}
}
pcsError = handleRespStatusError(OperationUpload, resp)
if pcsError != nil {
return
}
return resp.Body, nil
}
// PrepareCloudDlAddTask 添加离线下载任务, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareCloudDlAddTask(sourceURL, savePath string) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
pcsURL2 := pcs.generatePCSURL2("services/cloud_dl", "add_task", map[string]string{
"save_path": savePath,
"source_url": sourceURL,
"timeout": "2147483647",
})
baiduPCSVerbose.Infof("%s URL: %s\n", OperationCloudDlAddTask, pcsURL2)
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, OperationCloudDlAddTask, http.MethodPost, pcsURL2.String(), nil, nil)
return
}
// PrepareCloudDlQueryTask 精确查询离线下载任务, 只返回服务器响应数据和错误信息,
// taskids 例子: 12123,234234,2344, 用逗号隔开多个 task_id
func (pcs *BaiduPCS) PrepareCloudDlQueryTask(taskIDs string) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
pcsURL2 := pcs.generatePCSURL2("services/cloud_dl", "query_task", map[string]string{
"op_type": "1",
})
baiduPCSVerbose.Infof("%s URL: %s\n", OperationCloudDlQueryTask, pcsURL2)
// 表单上传
mr := multipartreader.NewMultipartReader()
mr.AddFormFeild("task_ids", strings.NewReader(taskIDs))
mr.CloseMultipart()
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, OperationCloudDlQueryTask, http.MethodPost, pcsURL2.String(), mr, nil)
return
}
// PrepareCloudDlListTask 查询离线下载任务列表, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareCloudDlListTask() (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
pcsURL2 := pcs.generatePCSURL2("services/cloud_dl", "list_task", map[string]string{
"need_task_info": "1",
"status": "255",
"start": "0",
"limit": "1000",
})
baiduPCSVerbose.Infof("%s URL: %s\n", OperationCloudDlListTask, pcsURL2)
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, OperationCloudDlListTask, http.MethodPost, pcsURL2.String(), nil, nil)
return
}
func (pcs *BaiduPCS) prepareCloudDlCDTask(opreation, method string, taskID int64) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
pcsURL2 := pcs.generatePCSURL2("services/cloud_dl", method, map[string]string{
"task_id": strconv.FormatInt(taskID, 10),
})
baiduPCSVerbose.Infof("%s URL: %s\n", opreation, pcsURL2)
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, opreation, http.MethodPost, pcsURL2.String(), nil, nil)
return
}
// PrepareCloudDlCancelTask 取消离线下载任务, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareCloudDlCancelTask(taskID int64) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
return pcs.prepareCloudDlCDTask(OperationCloudDlCancelTask, "cancel_task", taskID)
}
// PrepareCloudDlDeleteTask 取消离线下载任务, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareCloudDlDeleteTask(taskID int64) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
return pcs.prepareCloudDlCDTask(OperationCloudDlDeleteTask, "delete_task", taskID)
}
// PrepareCloudDlClearTask 清空离线下载任务记录, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareCloudDlClearTask() (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
pcsURL2 := pcs.generatePCSURL2("services/cloud_dl", "clear_task")
baiduPCSVerbose.Infof("%s URL: %s\n", OperationCloudDlClearTask, pcsURL2)
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, OperationCloudDlClearTask, http.MethodPost, pcsURL2.String(), nil, nil)
return
}
// PrepareSharePSet 私密分享文件, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareSharePSet(paths []string, period int) (dataReadCloser io.ReadCloser, panError pcserror.Error) {
pcs.lazyInit()
panURL := &url.URL{
Scheme: "https",
Host: PanBaiduCom,
Path: "share/pset",
}
baiduPCSVerbose.Infof("%s URL: %s\n", OperationShareSet, panURL)
dataReadCloser, panError = pcs.sendReqReturnReadCloser(reqTypePan, OperationShareSet, http.MethodPost, panURL.String(), map[string]string{
"path_list": mergeStringList(paths...),
"schannel": "0",
"channel_list": "[]",
"period": strconv.Itoa(period),
}, map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
})
return
}
// PrepareShareCancel 取消分享, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareShareCancel(shareIDs []int64) (dataReadCloser io.ReadCloser, panError pcserror.Error) {
pcs.lazyInit()
panURL := &url.URL{
Scheme: "https",
Host: PanBaiduCom,
Path: "share/cancel",
}
baiduPCSVerbose.Infof("%s URL: %s\n", OperationShareCancel, panURL)
ss := converter.SliceInt64ToString(shareIDs)
dataReadCloser, panError = pcs.sendReqReturnReadCloser(reqTypePan, OperationShareCancel, http.MethodPost, panURL.String(), map[string]string{
"shareid_list": "[" + strings.Join(ss, ",") + "]",
}, map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
})
return
}
// PrepareShareList 列出分享列表, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareShareList(page int) (dataReadCloser io.ReadCloser, panError pcserror.Error) {
pcs.lazyInit()
query := url.Values{}
query.Set("page", strconv.Itoa(page))
query.Set("desc", "1")
query.Set("order", "time")
panURL := &url.URL{
Scheme: "https",
Host: PanBaiduCom,
Path: "share/record",
RawQuery: query.Encode(),
}
baiduPCSVerbose.Infof("%s URL: %s\n", OperationShareList, panURL)
dataReadCloser, panError = pcs.sendReqReturnReadCloser(reqTypePan, OperationShareList, http.MethodGet, panURL.String(), nil, nil)
return
}
// PrepareShareSURLInfo 获取分享的详细信息, 包含密码, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareShareSURLInfo(shareID int64) (dataReadCloser io.ReadCloser, panError pcserror.Error) {
pcs.lazyInit()
query := url.Values{}
query.Set("shareid", strconv.FormatInt(shareID, 10))
query.Set("sign", converter.ToString(netdisksign.ShareSURLInfoSign(shareID)))
panURL := &url.URL{
Scheme: "https",
Host: PanBaiduCom,
Path: "share/surlinfoinrecord",
RawQuery: query.Encode(),
}
baiduPCSVerbose.Infof("%s URL: %s\n", OperationShareSURLInfo, panURL)
dataReadCloser, panError = pcs.sendReqReturnReadCloser(reqTypePan, OperationShareSURLInfo, http.MethodGet, panURL.String(), nil, nil)
return
}
// PrepareRecycleList 列出回收站文件列表, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareRecycleList(page int) (dataReadCloser io.ReadCloser, panError pcserror.Error) {
pcs.lazyInit()
panURL := pcs.generatePanURL("recycle/list", map[string]string{
"num": "100",
"page": strconv.Itoa(page),
})
baiduPCSVerbose.Infof("%s URL: %s\n", OperationRecycleList, panURL)
dataReadCloser, panError = pcs.sendReqReturnReadCloser(reqTypePan, OperationRecycleList, http.MethodGet, panURL.String(), nil, nil)
return
}
// PrepareRecycleRestore 还原回收站文件或目录, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareRecycleRestore(fidList ...int64) (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
pcsURL := pcs.generatePCSURL("file", "restore")
baiduPCSVerbose.Infof("%s URL: %s\n", OperationRecycleRestore, pcsURL)
fsIDList := make([]*FsIDJSON, 0, len(fidList))
for k := range fidList {
fsIDList = append(fsIDList, &FsIDJSON{
FsID: fidList[k],
})
}
fsIDListJSON := FsIDListJSON{
List: fsIDList,
}
sendData, err := jsoniter.Marshal(&fsIDListJSON)
if err != nil {
panic(err)
}
// 表单上传
mr := multipartreader.NewMultipartReader()
mr.AddFormFeild("param", bytes.NewReader(sendData))
mr.CloseMultipart()
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, OperationRecycleRestore, http.MethodPost, pcsURL.String(), mr, nil)
return
}
// PrepareRecycleDelete 删除回收站文件或目录, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareRecycleDelete(fidList ...int64) (dataReadCloser io.ReadCloser, panError pcserror.Error) {
pcs.lazyInit()
panURL := pcs.generatePanURL("recycle/delete", nil)
baiduPCSVerbose.Infof("%s URL: %s\n", OperationRecycleDelete, panURL)
dataReadCloser, panError = pcs.sendReqReturnReadCloser(reqTypePan, OperationRecycleDelete, http.MethodPost, panURL.String(), map[string]string{
"fidlist": mergeInt64List(fidList...),
}, map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
})
return
}
// PrepareRecycleClear 清空回收站, 只返回服务器响应数据和错误信息
func (pcs *BaiduPCS) PrepareRecycleClear() (dataReadCloser io.ReadCloser, pcsError pcserror.Error) {
pcs.lazyInit()
pcsURL := pcs.generatePCSURL("file", "delete", map[string]string{
"type": "recycle",
})
baiduPCSVerbose.Infof("%s URL: %s\n", OperationRecycleClear, pcsURL)
dataReadCloser, pcsError = pcs.sendReqReturnReadCloser(reqTypePCS, OperationRecycleClear, http.MethodGet, pcsURL.String(), nil, nil)
return
}

22
baidupcs/publicsuffix.go Normal file
View File

@@ -0,0 +1,22 @@
package baidupcs
import (
"net/http/cookiejar"
"strings"
)
type list struct{}
// PublicSuffixList baidupcs PublicSuffixList
var PublicSuffixList cookiejar.PublicSuffixList = list{}
func (list) PublicSuffix(domain string) string {
if strings.HasSuffix(domain, ".baidu.com") {
return "com"
}
return domain
}
func (list) String() string {
return "baidupcs"
}

32
baidupcs/quota.go Normal file
View File

@@ -0,0 +1,32 @@
package baidupcs
import (
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
)
type quotaInfo struct {
*pcserror.PCSErrInfo
Quota int64 `json:"quota"`
Used int64 `json:"used"`
}
// QuotaInfo 获取当前用户空间配额信息
func (pcs *BaiduPCS) QuotaInfo() (quota, used int64, pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareQuotaInfo()
if pcsError != nil {
return
}
defer dataReadCloser.Close()
quotaInfo := &quotaInfo{
PCSErrInfo: pcserror.NewPCSErrorInfo(OperationQuotaInfo),
}
pcsError = pcserror.HandleJSONParse(OperationQuotaInfo, dataReadCloser, quotaInfo)
if pcsError != nil {
return
}
return quotaInfo.Quota, quotaInfo.Used, nil
}

115
baidupcs/recycle.go Normal file
View File

@@ -0,0 +1,115 @@
package baidupcs
import (
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
)
type (
// RecycleFDInfo 回收站中文件/目录信息
RecycleFDInfo struct {
FsID int64 `json:"fs_id"` // fs_id
Isdir int `json:"isdir"`
LeftTime int `json:"leftTime"` // 剩余时间
Path string `json:"path"` // 路径
Filename string `json:"server_filename"` // 文件名 或 目录名
Ctime int64 `json:"server_ctime"` // 创建日期
Mtime int64 `json:"server_mtime"` // 修改日期
MD5 string `json:"md5"` // md5 值
Size int64 `json:"size"` // 文件大小 (目录为0)
}
// RecycleFDInfoList 回收站中文件/目录列表
RecycleFDInfoList []*RecycleFDInfo
recycleListJSON struct {
*pcserror.PanErrorInfo
List RecycleFDInfoList `json:"list"`
}
recycleRestoreJSON struct {
*pcserror.PCSErrInfo
Extra FsIDListJSON `json:"extra"`
}
// RecycleClearInfo 清空回收站的信息
RecycleClearInfo struct {
List RecycleFDInfoList `json:"list"`
SussNum int `json:"succNum"`
}
recycleClearJSON struct {
*pcserror.PCSErrInfo
Extra RecycleClearInfo `json:"extra"`
}
)
// RecycleList 列出回收站文件列表
func (pcs *BaiduPCS) RecycleList(page int) (fdl RecycleFDInfoList, panError pcserror.Error) {
dataReadCloser, panError := pcs.PrepareRecycleList(page)
if panError != nil {
return
}
defer dataReadCloser.Close()
errInfo := pcserror.NewPanErrorInfo(OperationRecycleList)
jsonData := recycleListJSON{
PanErrorInfo: errInfo,
}
panError = pcserror.HandleJSONParse(OperationRecycleList, dataReadCloser, &jsonData)
if panError != nil {
return
}
return jsonData.List, nil
}
// RecycleRestore 还原回收站文件或目录
func (pcs *BaiduPCS) RecycleRestore(fidList ...int64) (sussFsIDList []*FsIDJSON, pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareRecycleRestore(fidList...)
if pcsError != nil {
return
}
defer dataReadCloser.Close()
errInfo := pcserror.NewPCSErrorInfo(OperationRecycleRestore)
jsonData := recycleRestoreJSON{
PCSErrInfo: errInfo,
}
pcsError = pcserror.HandleJSONParse(OperationRecycleRestore, dataReadCloser, &jsonData)
return jsonData.Extra.List, pcsError
}
// RecycleDelete 删除回收站文件或目录
func (pcs *BaiduPCS) RecycleDelete(fidList ...int64) (panError pcserror.Error) {
dataReadCloser, panError := pcs.PrepareRecycleDelete(fidList...)
if panError != nil {
return
}
defer dataReadCloser.Close()
panError = pcserror.DecodePanJSONError(OperationRecycleDelete, dataReadCloser)
return
}
// RecycleClear 清空回收站
func (pcs *BaiduPCS) RecycleClear() (sussNum int, pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareRecycleClear()
if pcsError != nil {
return
}
defer dataReadCloser.Close()
errInfo := pcserror.NewPCSErrorInfo(OperationRecycleClear)
jsonData := recycleClearJSON{
PCSErrInfo: errInfo,
}
pcsError = pcserror.HandleJSONParse(OperationRecycleClear, dataReadCloser, &jsonData)
return jsonData.Extra.SussNum, pcsError
}

44
baidupcs/rm_mkdir.go Normal file
View File

@@ -0,0 +1,44 @@
package baidupcs
import (
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"path"
)
// Remove 批量删除文件/目录
func (pcs *BaiduPCS) Remove(paths ...string) (pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareRemove(paths...)
if pcsError != nil {
return
}
defer dataReadCloser.Close()
errInfo := pcserror.DecodePCSJSONError(OperationRemove, dataReadCloser)
if errInfo != nil {
return errInfo
}
// 更新缓存
pcs.deleteCache(allRelatedDir(paths))
return nil
}
// Mkdir 创建目录
func (pcs *BaiduPCS) Mkdir(pcspath string) (pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareMkdir(pcspath)
if pcsError != nil {
return
}
defer dataReadCloser.Close()
errInfo := pcserror.DecodePCSJSONError(OperationMkdir, dataReadCloser)
if errInfo != nil {
return errInfo
}
// 更新缓存
pcs.deleteCache([]string{path.Dir(pcspath)})
return
}

174
baidupcs/share.go Normal file
View File

@@ -0,0 +1,174 @@
package baidupcs
import (
"errors"
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"strings"
)
type (
// ShareOption 分享可选项
ShareOption struct {
Password string // 密码
Period int // 有效期
}
// Shared 分享信息
Shared struct {
Link string `json:"link"`
ShareID int64 `json:"shareid"`
}
// ShareRecordInfo 分享信息
ShareRecordInfo struct {
ShareID int64 `json:"shareId"`
FsIds []int64 `json:"fsIds"`
Passwd string `json:"-"` // 这个字段已经没有用了, 需要从ShareSURLInfo中获取
Shortlink string `json:"shortlink"`
Status int `json:"status"` // 状态
Public int `json:"public"` // 是否为公开分享
TypicalCategory int `json:"typicalCategory"` // 文件类型
TypicalPath string `json:"typicalPath"`
}
shareSURLInfo struct {
*pcserror.PanErrorInfo
*ShareSURLInfo
}
// ShareSURLInfo 分享的子信息
ShareSURLInfo struct {
Pwd string `json:"pwd"` // 新密码
ShortURL string `json:"shorturl"`
}
// ShareRecordInfoList 分享信息列表
ShareRecordInfoList []*ShareRecordInfo
sharePSetJSON struct {
*Shared
*pcserror.PanErrorInfo
}
shareListJSON struct {
List ShareRecordInfoList `json:"list"`
*pcserror.PanErrorInfo
}
)
var (
// ErrShareLinkNotFound 未找到分享链接
ErrShareLinkNotFound = errors.New("未找到分享链接")
)
// ShareSet 分享文件
func (pcs *BaiduPCS) ShareSet(paths []string, option *ShareOption) (s *Shared, pcsError pcserror.Error) {
if option == nil {
option = &ShareOption{}
}
dataReadCloser, pcsError := pcs.PrepareSharePSet(paths, option.Period)
if pcsError != nil {
return
}
defer dataReadCloser.Close()
errInfo := pcserror.NewPanErrorInfo(OperationShareSet)
jsonData := sharePSetJSON{
Shared: &Shared{},
PanErrorInfo: errInfo,
}
pcsError = pcserror.HandleJSONParse(OperationShareSet, dataReadCloser, &jsonData)
if pcsError != nil {
return
}
if jsonData.Link == "" {
errInfo.ErrType = pcserror.ErrTypeOthers
errInfo.Err = ErrShareLinkNotFound
return nil, errInfo
}
return jsonData.Shared, nil
}
// ShareCancel 取消分享
func (pcs *BaiduPCS) ShareCancel(shareIDs []int64) (pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareShareCancel(shareIDs)
if pcsError != nil {
return
}
defer dataReadCloser.Close()
pcsError = pcserror.DecodePanJSONError(OperationShareCancel, dataReadCloser)
return
}
// ShareList 列出分享列表
func (pcs *BaiduPCS) ShareList(page int) (records ShareRecordInfoList, pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareShareList(page)
if pcsError != nil {
return
}
defer dataReadCloser.Close()
errInfo := pcserror.NewPanErrorInfo(OperationShareList)
jsonData := shareListJSON{
List: records,
PanErrorInfo: errInfo,
}
pcsError = pcserror.HandleJSONParse(OperationShareList, dataReadCloser, &jsonData)
if pcsError != nil {
// json解析错误
if pcsError.GetErrType() == pcserror.ErrTypeJSONParseError {
// 服务器更改, List为空时变成{}, 导致解析错误
if strings.Contains(pcsError.GetError().Error(), `"list":{}`) {
// 返回空列表
return jsonData.List, nil
}
}
return
}
if jsonData.List == nil {
errInfo.ErrType = pcserror.ErrTypeOthers
errInfo.Err = errors.New("shared list is nil")
return nil, errInfo
}
return jsonData.List, nil
}
//ShareSURLInfo 获取分享的详细信息, 包含密码
func (pcs *BaiduPCS) ShareSURLInfo(shareID int64) (info *ShareSURLInfo, pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareShareSURLInfo(shareID)
if pcsError != nil {
return
}
defer dataReadCloser.Close()
errInfo := pcserror.NewPanErrorInfo(OperationShareSURLInfo)
jsonData := shareSURLInfo{
PanErrorInfo: errInfo,
}
pcsError = pcserror.HandleJSONParse(OperationShareList, dataReadCloser, &jsonData)
if pcsError != nil {
// json解析错误
return
}
// 去掉0
if jsonData.Pwd == "0" {
jsonData.Pwd = ""
}
return jsonData.ShareSURLInfo, nil
}

157
baidupcs/transfer.go Normal file
View File

@@ -0,0 +1,157 @@
package baidupcs
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path/filepath"
"regexp"
"strings"
"github.com/tidwall/gjson"
)
func (pcs *BaiduPCS) GenerateShareQueryURL(subPath string, params map[string]string) *url.URL {
shareURL := &url.URL{
Scheme: GetHTTPScheme(true),
Host: PanBaiduCom,
Path: "/share/" + subPath,
}
uv := shareURL.Query()
uv.Set("app_id", PanAppID)
uv.Set("channel", "chunlei")
uv.Set("clienttype", "0")
uv.Set("web", "1")
for key, value := range params {
uv.Set(key, value)
}
shareURL.RawQuery = uv.Encode()
return shareURL
}
func (pcs *BaiduPCS) PostShareQuery(url string, data map[string]string) (res map[string]string) {
dataReadCloser, panError := pcs.sendReqReturnReadCloser(reqTypePan, OperationShareFileSavetoLocal, http.MethodPost, url, data, map[string]string{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36",
"Content-Type": "application/x-www-form-urlencoded",
"Referer": "https://pan.baidu.com/disk/home",
})
res = make(map[string]string)
if panError != nil {
res["ErrMsg"] = "提交分享项查询请求时发生错误"
return
}
defer dataReadCloser.Close()
body, _ := ioutil.ReadAll(dataReadCloser)
errno := gjson.Get(string(body), `errno`).String()
if errno != "0" {
res["ErrMsg"] = "分享码错误"
return
}
res["ErrMsg"] = "0"
return
}
func (pcs *BaiduPCS) AccessSharePage(sharelink string, first bool) (tokens map[string]string) {
tokens = make(map[string]string)
tokens["ErrMsg"] = "0"
headers := make(map[string]string)
headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36"
headers["Referer"] = "https://pan.baidu.com/disk/home"
dataReadCloser, panError := pcs.sendReqReturnReadCloser(reqTypePan, OperationShareFileSavetoLocal, http.MethodGet, sharelink, nil, headers)
if panError != nil {
tokens["ErrMsg"] = "访问分享页失败"
return
}
defer dataReadCloser.Close()
body, _ := ioutil.ReadAll(dataReadCloser)
not_found_flag := strings.Contains(string(body), "platform-non-found")
error_page_title := strings.Contains(string(body), "error-404")
if error_page_title {
tokens["ErrMsg"] = "页面不存在"
return
}
if not_found_flag {
tokens["ErrMsg"] = "分享链接已失效"
return
} else {
re, _ := regexp.Compile(`"bdstoken":"([A-Za-z0-9]+)",`)
sub := re.FindSubmatch(body)
if len(sub) < 2 {
tokens["ErrMsg"] = "分享页面解析失败"
return
}
tokens["bdstoken"] = string(sub[1])
if first {
return
}
re, _ = regexp.Compile(`"shareid":([0-9]+),`)
sub = re.FindSubmatch(body)
if len(sub) < 2 && !first {
tokens["ErrMsg"] = "缺少提取码"
return
}
tokens["shareid"] = string(sub[1])
re, _ = regexp.Compile(`uk=([0-9]+)`)
sub = re.FindSubmatch(body)
tokens["uk"] = string(sub[1])
return
}
}
func (pcs *BaiduPCS) GenerateRequestQuery(mode, link string, params map[string]string) (res map[string]string) {
res = make(map[string]string)
headers := map[string]string{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36",
"Referer": "https://pan.baidu.com/disk/home",
}
if mode == "POST" {
headers["Content-Type"] = "application/x-www-form-urlencoded"
}
dataReadCloser, panError := pcs.sendReqReturnReadCloser(reqTypePan, OperationShareFileSavetoLocal, mode, link, params, headers)
if panError != nil {
res["ErrMsg"] = "未知错误"
return
}
defer dataReadCloser.Close()
body, err := ioutil.ReadAll(dataReadCloser)
if err != nil {
res["ErrMsg"] = "未知错误"
return
}
if !gjson.Valid(string(body)) {
res["ErrMsg"] = "返回值json解析错误"
return
}
errno := gjson.Get(string(body), `errno`).Int()
if errno != 0 {
res["ErrMsg"] = "获取分享项元数据错误"
if mode == "POST" && errno == 12 {
path := gjson.Get(string(body), `info.0.path`).String()
_, file := filepath.Split(path)
res["ErrMsg"] = fmt.Sprintf("当前目录下已有%s同名文件/文件夹", file)
}
return
}
if mode != "POST" {
res["title"] = gjson.Get(string(body), `title`).String()
res["filename"] = gjson.Get(string(body), `list.0.server_filename`).String()
var fids_str string = "["
fsids := gjson.Get(string(body), `list.#.fs_id`).Array()
if len(fsids) > 1 {
res["filename"] += "等多个文件"
}
for _, sid := range fsids {
fids_str += sid.String() + ","
}
res["fs_id"] = fids_str[:len(fids_str)-1] + "]"
}
res["ErrMsg"] = fmt.Sprintf("%d", errno)
return
}

269
baidupcs/upload.go Normal file
View File

@@ -0,0 +1,269 @@
package baidupcs
import (
"errors"
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"net/http"
"path"
)
const (
// MaxUploadBlockSize 最大上传的文件分片大小
MaxUploadBlockSize = 2 * converter.GB
// MinUploadBlockSize 最小的上传的文件分片大小
MinUploadBlockSize = 4 * converter.MB
// MaxRapidUploadSize 秒传文件支持的最大文件大小
MaxRapidUploadSize = 20 * converter.GB
// RecommendUploadBlockSize 推荐的上传的文件分片大小
RecommendUploadBlockSize = 1 * converter.GB
// SliceMD5Size 计算 slice-md5 所需的长度
SliceMD5Size = 256 * converter.KB
// EmptyContentMD5 空串的md5
EmptyContentMD5 = "d41d8cd98f00b204e9800998ecf8427e"
)
var (
// ErrUploadMD5NotFound 未找到md5
ErrUploadMD5NotFound = errors.New("unknown response data, md5 not found")
// ErrUploadSavePathFound 未找到保存路径
ErrUploadSavePathFound = errors.New("unknown response data, file saved path not found")
// ErrUploadSeqNotMatch 服务器返回的上传队列不匹配
ErrUploadSeqNotMatch = errors.New("服务器返回的上传队列不匹配")
)
type (
// UploadFunc 上传文件处理函数
UploadFunc func(uploadURL string, jar http.CookieJar) (resp *http.Response, err error)
// RapidUploadInfo 文件秒传信息
RapidUploadInfo struct {
Filename string
ContentLength int64
ContentMD5 string
SliceMD5 string
ContentCrc32 string
}
uploadJSON struct {
*PathJSON
*pcserror.PCSErrInfo
}
uploadTmpFileJSON struct {
MD5 string `json:"md5"`
*pcserror.PCSErrInfo
}
uploadPrecreateJSON struct {
ReturnType int `json:"return_type"` // 1上传, 2秒传
UploadID string `json:"uploadid"`
BlockList []int `json:"block_list"`
*pcserror.PanErrorInfo
fdJSON `json:"info"`
}
// UploadSeq 分片上传顺序
UploadSeq struct {
Seq int
Block string
}
// PrecreateInfo 预提交文件消息返回数据
PrecreateInfo struct {
IsRapidUpload bool
UploadID string
UploadSeqList []*UploadSeq
}
uploadSuperfile2JSON struct {
MD5 string `json:"md5"`
*pcserror.PCSErrInfo
}
)
// RapidUpload 秒传文件
func (pcs *BaiduPCS) RapidUpload(targetPath, contentMD5, sliceMD5, crc32 string, length int64) (pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareRapidUpload(targetPath, contentMD5, sliceMD5, crc32, length)
if pcsError != nil {
return
}
defer dataReadCloser.Close()
pcsError = pcserror.DecodePCSJSONError(OperationRapidUpload, dataReadCloser)
if pcsError != nil {
return
}
// 更新缓存
pcs.deleteCache([]string{path.Dir(targetPath)})
return nil
}
// RapidUploadNoCheckDir 秒传文件, 不进行目录检查, 会覆盖掉同名的目录!
func (pcs *BaiduPCS) RapidUploadNoCheckDir(targetPath, contentMD5, sliceMD5, crc32 string, length int64) (pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.prepareRapidUpload(targetPath, contentMD5, sliceMD5, crc32, length)
if pcsError != nil {
return
}
defer dataReadCloser.Close()
pcsError = pcserror.DecodePCSJSONError(OperationRapidUpload, dataReadCloser)
if pcsError != nil {
return
}
return nil
}
// Upload 上传单个文件
func (pcs *BaiduPCS) Upload(targetPath string, uploadFunc UploadFunc) (pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareUpload(targetPath, uploadFunc)
if pcsError != nil {
return pcsError
}
defer dataReadCloser.Close()
// 数据处理
jsonData := uploadJSON{
PCSErrInfo: pcserror.NewPCSErrorInfo(OperationUpload),
}
pcsError = pcserror.HandleJSONParse(OperationUpload, dataReadCloser, &jsonData)
if pcsError != nil {
return
}
if jsonData.Path == "" {
jsonData.PCSErrInfo.ErrType = pcserror.ErrTypeInternalError
jsonData.PCSErrInfo.Err = ErrUploadSavePathFound
return jsonData.PCSErrInfo
}
// 更新缓存
pcs.deleteCache([]string{path.Dir(targetPath)})
return nil
}
// UploadTmpFile 分片上传—文件分片及上传
func (pcs *BaiduPCS) UploadTmpFile(uploadFunc UploadFunc) (md5 string, pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareUploadTmpFile(uploadFunc)
if pcsError != nil {
return "", pcsError
}
defer dataReadCloser.Close()
// 数据处理
jsonData := uploadTmpFileJSON{
PCSErrInfo: pcserror.NewPCSErrorInfo(OperationUploadTmpFile),
}
pcsError = pcserror.HandleJSONParse(OperationUploadTmpFile, dataReadCloser, &jsonData)
if pcsError != nil {
return
}
// 未找到md5
if jsonData.MD5 == "" {
jsonData.PCSErrInfo.ErrType = pcserror.ErrTypeInternalError
jsonData.PCSErrInfo.Err = ErrUploadMD5NotFound
return "", jsonData.PCSErrInfo
}
return jsonData.MD5, nil
}
// UploadCreateSuperFile 分片上传—合并分片文件
func (pcs *BaiduPCS) UploadCreateSuperFile(checkDir bool, targetPath string, blockList ...string) (pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareUploadCreateSuperFile(checkDir, targetPath, blockList...)
if pcsError != nil {
return pcsError
}
defer dataReadCloser.Close()
errInfo := pcserror.DecodePCSJSONError(OperationUploadCreateSuperFile, dataReadCloser)
if errInfo != nil {
return errInfo
}
// 更新缓存
pcs.deleteCache([]string{path.Dir(targetPath)})
return nil
}
// UploadPrecreate 分片上传—Precreate,
// 支持检验秒传
func (pcs *BaiduPCS) UploadPrecreate(targetPath, contentMD5, sliceMD5, crc32 string, size int64, bolckList ...string) (precreateInfo *PrecreateInfo, pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareUploadPrecreate(targetPath, contentMD5, sliceMD5, crc32, size, bolckList...)
if pcsError != nil {
return
}
defer dataReadCloser.Close()
errInfo := pcserror.NewPanErrorInfo(OperationUploadPrecreate)
jsonData := uploadPrecreateJSON{
PanErrorInfo: errInfo,
}
pcsError = pcserror.HandleJSONParse(OperationUploadPrecreate, dataReadCloser, &jsonData)
if pcsError != nil {
return
}
switch jsonData.ReturnType {
case 1: // 上传
seqLen := len(jsonData.BlockList)
if seqLen != len(bolckList) {
errInfo.ErrType = pcserror.ErrTypeRemoteError
errInfo.Err = ErrUploadSeqNotMatch
return nil, errInfo
}
seqList := make([]*UploadSeq, 0, seqLen)
for k, seq := range jsonData.BlockList {
seqList = append(seqList, &UploadSeq{
Seq: seq,
Block: bolckList[k],
})
}
return &PrecreateInfo{
UploadID: jsonData.UploadID,
UploadSeqList: seqList,
}, nil
case 2: // 秒传
return &PrecreateInfo{
IsRapidUpload: true,
}, nil
default:
panic("unknown returntype")
}
}
// UploadSuperfile2 分片上传—Superfile2
func (pcs *BaiduPCS) UploadSuperfile2(uploadid, targetPath string, partseq int, partOffset int64, uploadFunc UploadFunc) (md5sum string, pcsError pcserror.Error) {
dataReadCloser, pcsError := pcs.PrepareUploadSuperfile2(uploadid, targetPath, partseq, partOffset, uploadFunc)
if pcsError != nil {
return
}
defer dataReadCloser.Close()
jsonData := uploadSuperfile2JSON{
PCSErrInfo: pcserror.NewPCSErrorInfo(OperationUploadSuperfile2),
}
pcsError = pcserror.HandleJSONParse(OperationUploadSuperfile2, dataReadCloser, &jsonData)
if pcsError != nil {
return
}
return jsonData.MD5, nil
}

73
baidupcs/util.go Normal file
View File

@@ -0,0 +1,73 @@
package baidupcs
import (
"errors"
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"github.com/iikira/BaiduPCS-Go/pcsutil"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"path"
"strings"
)
// Isdir 检查路径在网盘中是否为目录
func (pcs *BaiduPCS) Isdir(pcspath string) (isdir bool, pcsError pcserror.Error) {
if path.Clean(pcspath) == PathSeparator {
return true, nil
}
f, pcsError := pcs.FilesDirectoriesMeta(pcspath)
if pcsError != nil {
return false, pcsError
}
return f.Isdir, nil
}
func (pcs *BaiduPCS) checkIsdir(op string, targetPath string) pcserror.Error {
// 检测文件是否存在于网盘路径
// 很重要, 如果文件存在会直接覆盖!!! 即使是根目录!
isdir, pcsError := pcs.Isdir(targetPath)
if pcsError != nil {
// 忽略远程服务端返回的错误
if pcsError.GetErrType() != pcserror.ErrTypeRemoteError {
return pcsError
}
}
errInfo := pcserror.NewPCSErrorInfo(op)
if isdir {
errInfo.ErrType = pcserror.ErrTypeOthers
errInfo.Err = errors.New("保存路径不可以覆盖目录")
return errInfo
}
return nil
}
func mergeStringList(a ...string) string {
s := strings.Join(a, `","`)
return `["` + s + `"]`
}
func mergeInt64List(si ...int64) string {
i := converter.SliceInt64ToString(si)
s := strings.Join(i, ",")
return "[" + s + "]"
}
func allRelatedDir(pcspaths []string) (dirs []string) {
for _, pcspath := range pcspaths {
pathDir := path.Dir(pcspath)
if !pcsutil.ContainsString(dirs, pathDir) {
dirs = append(dirs, pathDir)
}
}
return
}
// GetHTTPScheme 获取 http 协议, https 或 http
func GetHTTPScheme(https bool) (scheme string) {
if https {
return "https"
}
return "http"
}

144
build.sh Executable file
View File

@@ -0,0 +1,144 @@
#!/bin/sh
name="BaiduPCS-Go"
version=$1
if [ "$1" = "" ]; then
version=v3.6.2
fi
output="out/"
old_golang() {
GOROOT=/usr/local/go1.10.8
go=$GOROOT/bin/go
}
new_golang() {
GOROOT=/usr/local/go
go=$GOROOT/bin/go
}
Build() {
old_golang
goarm=$4
if [ "$4" = "" ]; then
goarm=7
fi
echo "Building $1..."
export GOOS=$2 GOARCH=$3 GO386=sse2 CGO_ENABLED=0 GOARM=$4
if [ $2 = "windows" ]; then
goversioninfo -o=resource_windows_386.syso
goversioninfo -64 -o=resource_windows_amd64.syso
$go build -ldflags "-X main.Version=$version -s -w" -o "$output/$1/$name.exe"
RicePack $1 $name.exe
else
$go build -ldflags "-X main.Version=$version -s -w" -o "$output/$1/$name"
RicePack $1 $name
fi
Pack $1
}
AndroidBuild() {
new_golang
echo "Building $1..."
export GOOS=$2 GOARCH=$3 GOARM=$4 CGO_ENABLED=1
go build -ldflags "-X main.Version=$version -s -w -linkmode=external -extldflags=-pie" -o "$output/$1/$name"
RicePack $1 $name
Pack $1
}
IOSBuild() {
old_golang
echo "Building $1..."
mkdir -p "$output/$1"
cd "$output/$1"
export CC=/usr/local/go/misc/ios/clangwrap.sh GOOS=darwin GOARCH=arm GOARM=7 CGO_ENABLED=1
$go build -ldflags "-X main.Version=$version -s -w" -o "armv7" github.com/iikira/BaiduPCS-Go
jtool --sign --inplace --ent ../../entitlements.xml "armv7"
export GOARCH=arm64
$go build -ldflags "-X main.Version=$version -s -w" -o "arm64" github.com/iikira/BaiduPCS-Go
jtool --sign --inplace --ent ../../entitlements.xml "arm64"
lipo -create "armv7" "arm64" -output $name # merge
rm "armv7" "arm64"
cd ../..
RicePack $1 $name
Pack $1
}
# zip 打包
Pack() {
cp README.md "$output/$1"
cd $output
zip -q -r "$1.zip" "$1"
# 删除
rm -rf "$1"
cd ..
}
# rice 打包静态资源
RicePack() {
return # 已取消web功能
rice -i github.com/iikira/BaiduPCS-Go/internal/pcsweb append --exec "$output/$1/$2"
}
touch ./vendor/golang.org/x/sys/windows/windows.s
# Android
export NDK_INSTALL=$ANDROID_NDK_ROOT/bin
# CC=$NDK_INSTALL/arm-linux-androideabi-4.9/bin/arm-linux-androideabi-gcc AndroidBuild $name-$version"-android-16-armv5" android arm 5
# CC=$NDK_INSTALL/arm-linux-androideabi-4.9/bin/arm-linux-androideabi-gcc AndroidBuild $name-$version"-android-16-armv6" android arm 6
CC=$NDK_INSTALL/arm-linux-androideabi-4.9/bin/arm-linux-androideabi-gcc AndroidBuild $name-$version"-android-16-armv7" android arm 7
CC=$NDK_INSTALL/aarch64-linux-android-4.9/bin/aarch64-linux-android-gcc AndroidBuild $name-$version"-android-21-arm64" android arm64 7
CC=$NDK_INSTALL/i686-linux-android-4.9/bin/i686-linux-android-gcc AndroidBuild $name-$version"-android-16-386" android 386 7
CC=$NDK_INSTALL/x86_64-linux-android-4.9/bin/x86_64-linux-android-gcc AndroidBuild $name-$version"-android-21-amd64" android amd64 7
# iOS
IOSBuild $name-$version"-darwin-ios-arm"
# OS X / macOS
Build $name-$version"-darwin-osx-amd64" darwin amd64
# Build $name-$version"-darwin-osx-386" darwin 386
# Windows
Build $name-$version"-windows-x86" windows 386
Build $name-$version"-windows-x64" windows amd64
# Linux
Build $name-$version"-linux-386" linux 386
Build $name-$version"-linux-amd64" linux amd64
Build $name-$version"-linux-armv5" linux arm 5
Build $name-$version"-linux-armv7" linux arm 7
Build $name-$version"-linux-arm64" linux arm64
GOMIPS=softfloat Build $name-$version"-linux-mips" linux mips
Build $name-$version"-linux-mips64" linux mips64
GOMIPS=softfloat Build $name-$version"-linux-mipsle" linux mipsle
Build $name-$version"-linux-mips64le" linux mips64le
# Build $name-$version"-linux-ppc64" linux ppc64
# Build $name-$version"-linux-ppc64le" linux ppc64le
# Build $name-$version"-linux-s390x" linux s390x
# Others
# Build $name-$version"-solaris-amd64" solaris amd64
Build $name-$version"-freebsd-386" freebsd 386
Build $name-$version"-freebsd-amd64" freebsd amd64
# Build $name-$version"-freebsd-arm" freebsd arm
# Build $name-$version"-netbsd-386" netbsd 386
# Build $name-$version"-netbsd-amd64" netbsd amd64
# Build $name-$version"-netbsd-arm" netbsd arm
# Build $name-$version"-openbsd-386" openbsd 386
# Build $name-$version"-openbsd-amd64" openbsd amd64
# Build $name-$version"-openbsd-arm" openbsd arm
# Build $name-$version"-plan9-386" plan9 386
# Build $name-$version"-plan9-amd64" plan9 amd64
# Build $name-$version"-plan9-arm" plan9 arm
# Build $name-$version"-nacl-386" nacl 386
# Build $name-$version"-nacl-amd64p32" nacl amd64p32
# Build $name-$version"-nacl-arm" nacl arm
# Build $name-$version"-dragonflybsd-amd64" dragonfly amd64

130
cmd/AndroidNDKBuild/main.go Normal file
View File

@@ -0,0 +1,130 @@
// AndroidNDKBuild
// go build -ldflags "-X main.APILevel=15 -X main.Arch=x86_64"
// env ANDROID_API_LEVEL NDK ANDROID_NDK_ROOT GOARCH
package main
import (
"os"
"os/exec"
"path/filepath"
"runtime"
"syscall"
)
var (
// NDKPath path to Android NDK
NDKPath string
// APILevel Android api level
APILevel string
// Arch arch
Arch string
)
func getNDKPath() string {
ndkPath, ok := os.LookupEnv("NDK")
if ok {
return ndkPath
}
ndkPath, ok = os.LookupEnv("ANDROID_NDK_ROOT")
if ok {
return ndkPath
}
ndkPath, ok = os.LookupEnv("ANDROID_NDK_DIR")
if ok {
return ndkPath
}
return ""
}
func getAPILevel() string {
apiLevelStr, ok := os.LookupEnv("ANDROID_API_LEVEL")
if ok {
return apiLevelStr
}
return "21"
}
func getGoarch() string {
arch, ok := os.LookupEnv("GOARCH")
if ok {
return arch
}
return runtime.GOARCH
}
func getArch() string {
if Arch != "" {
return Arch
}
goarch := getGoarch()
switch goarch {
case "386":
return "x86"
case "amd64":
return "x86_64"
case "arm64":
return "aarch64"
}
return goarch
}
func getPlatformsArch() string {
arch := getArch()
switch arch {
case "aarch64":
return "arm64"
}
return arch
}
func main() {
if NDKPath == "" {
NDKPath = getNDKPath()
}
if APILevel == "" {
APILevel = getAPILevel()
}
if Arch == "" {
Arch = getArch()
}
lastPattern := "*-gcc"
if runtime.GOOS == "windows" {
lastPattern += ".exe"
}
gccPaths, err := filepath.Glob(filepath.Join(NDKPath, "toolchains", getArch()+"-*", "prebuilt", runtime.GOOS+"-*", "bin", lastPattern))
checkErr(err)
if len(gccPaths) == 0 {
panic("no match gcc")
}
args := make([]string, len(os.Args))
copy(args[1:], os.Args[1:])
args[0] = "--sysroot=" + filepath.Join(NDKPath, "platforms", "android-"+APILevel, "arch-"+getPlatformsArch())
gccExec := exec.Command(gccPaths[0], args...)
gccExec.Stdout = os.Stdout
gccExec.Stderr = os.Stderr
err = gccExec.Run()
exitError, ok := err.(*exec.ExitError)
if ok {
status := exitError.ProcessState.Sys().(syscall.WaitStatus)
os.Exit(status.ExitStatus())
}
if err != nil {
println(err.Error())
}
return
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}

1
debian/Packages.sh vendored Normal file
View File

@@ -0,0 +1 @@
dpkg-scanpackages . | gzip > Packages.gz

209
debian/copyright vendored Normal file
View File

@@ -0,0 +1,209 @@
Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135
Name: BaiduPCS-Go
Maintainer: iikira <i@mail.iikira.com>
Source: https://github.com/iikira/BaiduPCS-Go
Copyright: 2016-2019 iikira <i@mail.iikira.com>
License: Apache-2.0+
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright iikira.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

9
debian/iphoneos-arm/control vendored Normal file
View File

@@ -0,0 +1,9 @@
Package: BaiduPCS-Go
Version: 3.6
Homepage: https://github.com/iikira/BaiduPCS-Go
Section: 实用工具
Priority: optional
Architecture: iphoneos-arm
Installed-Size: 4096
Maintainer: iikira <i@mail.iikira.com>
Description: BaiduPCS-Go 使用Go语言编写的百度网盘命令行客户端, 为操作百度网盘, 提供实用功能.

9
debian/linux-amd64/control vendored Normal file
View File

@@ -0,0 +1,9 @@
Package: BaiduPCS-Go
Version: 3.6
Homepage: https://github.com/iikira/BaiduPCS-Go
Section: utils
Priority: optional
Architecture: amd64
Installed-Size: 4096
Maintainer: iikira <i@mail.iikira.com>
Description: BaiduPCS-Go 使用Go语言编写的百度网盘命令行客户端, 为操作百度网盘, 提供实用功能.

19
docs/README.md Normal file
View File

@@ -0,0 +1,19 @@
# 文档目录
## 百度PCS文档
### 文件API
[综述](https://github.com/iikira/BaiduPCS-Go/blob/master/docs/overview.md)
[文件API列表](https://github.com/iikira/BaiduPCS-Go/blob/master/docs/file_data_apis_list.md)
[文件API错误码列表](https://github.com/iikira/BaiduPCS-Go/blob/master/docs/file_data_apis_error.md)
### 结构化数据API
[综述](https://github.com/iikira/BaiduPCS-Go/blob/master/docs/structured_data_apis_overview.md)
[结构化数据API列表](https://github.com/iikira/BaiduPCS-Go/blob/master/docs/structured_data_api_list.md)
[结构化数据API错误码](https://github.com/iikira/BaiduPCS-Go/blob/master/docs/structured_data_apis_error.md)

View File

@@ -0,0 +1,71 @@
# 文件数据API错误码
| HTTP状态码 | 错误码 | 错误信息 | 备注 |
| :- | -: | :- | :- |
| 200 | 0 | no error | 没有错误 |
| 400 | 3 | Unsupported open api | 不支持此接口 |
| 403 | 4 | No permission to do this operation | 没有权限执行此操作 |
| 403 | 5 | Unauthorized client IP address | IP未授权 |
| 503 | 31001 | db query error | 数据库查询错误 |
| 503 | 31002 | db connect error | 数据库连接错误 |
| 503 | 31003 | db result set is empty | 数据库返回空结果 |
| 503 | 31021 | network error | 网络错误 |
| 503 | 31022 | can not access server | 暂时无法连接服务器 |
| 400 | 31023 | param error | 输入参数错误 |
| 400 | 31024 | app id is empty | app id为空 |
| 503 | 31025 | bcs error | 后端存储错误 |
| 403 | 31041 | bduss is invalid | 用户的cookie不是合法的百度cookie |
| 403 | 31042 | user is not login | 用户未登陆 |
| 403 | 31043 | user is not active | 用户未激活 |
| 403 | 31044 | user is not authorized | 用户未授权 |
| 403 | 31045 | user not exists | 用户不存在 |
| 403 | 31046 | user already exists | 用户已经存在 |
| 400 | 31061 | file already exists | 文件已经存在 |
| 400 | 31062 | file name is invalid | 文件名非法 |
| 400 | 31063 | file parent path does not exist | 文件父目录不存在 |
| 403 | 31064 | file is not authorized | 无权访问此文件 |
| 400 | 31065 | directory is full | 目录已满 |
| 403 | 31066 | file does not exist | 文件不存在 |
| 503 | 31067 | file deal failed | 文件处理出错 |
| 503 | 31068 | file create failed | 文件创建失败 |
| 503 | 31069 | file copy failed | 文件拷贝失败 |
| 503 | 31070 | file delete failed | 文件删除失败 |
| 503 | 31071 | get file meta failed | 不能读取文件元信息 |
| 503 | 31072 | file move failed | 文件移动失败 |
| 503 | 31073 | file rename failed | 文件重命名失败 |
| 503 | 31081 | superfile create failed | superfile创建失败 |
| 503 | 31082 | superfile block list is empty | superfile 块列表为空 |
| 503 | 31083 | superfile update failed | superfile 更新失败 |
| 503 | 31101 | tag internal error | tag系统内部错误 |
| 503 | 31102 | tag param error | tag参数错误 |
| 503 | 31103 | tag database error | tag系统错误 |
| 403 | 31110 | access denied to set quota | 未授权设置此目录配额 |
| 400 | 31111 | quota only sopport 2 level directories | 配额管理只支持两级目录 |
| 400 | 31112 | exceed quota | 超出配额 |
| 403 | 31113 | the quota is bigger than one of its parent directorys | 配额不能超出目录祖先的配额 |
| 403 | 31114 | the quota is smaller than one of its sub directorys | 配额不能比子目录配额小 |
| 503 | 31141 | thumbnail failed, internal error | 请求缩略图服务失败 |
| 401 | 110 | Access token invalid or no longer valid | Access Token不正确或者已经过期 |
| 400 | 31201 | signature error | 签名错误 |
| 400 | 31203 | acl put error | 设置acl失败 |
| 400 | 31204 | acl query error | 请求acl验证失败 |
| 400 | 31205 | acl get error | 获取acl失败 |
| 404 | 31079 | File md5 not found, you should use upload API to upload the whole file. | 未找到文件MD5 || 请使用上传API上传整个文件。 |
| 404 | 31202 | object not exists | 文件不存在 |
| 404 | 31206 | acl get error | acl不存在 |
| 400 | 31207 | bucket already exists | bucket已存在 |
| 400 | 31208 | bad request | 用户请求错误 |
| 500 | 31209 | baidubs internal error | 服务器错误 |
| 501 | 31210 | not implement | 服务器不支持 |
| 403 | 31211 | access denied | 禁止访问 |
| 503 | 31212 | service unavailable | 服务不可用 |
| 503 | 31213 | service unavailable | 重试出错 |
| 503 | 31214 | put object data error | 上传文件data失败 |
| 503 | 31215 | put object meta error | 上传文件meta失败 |
| 503 | 31216 | get object data error | 下载文件data失败 |
| 503 | 31217 | get object meta error | 下载文件meta失败 |
| 403 | 31218 | storage exceed limit | 容量超出限额 |
| 403 | 31219 | request exceed limit | 请求数超出限额 |
| 403 | 31220 | transfer exceed limit | 流量超出限额 |
| 500 | 31298 | the value of KEY[VALUE] in pcs response headers is invalid | 服务器返回值KEY非法 |
| 500 | 31299 | no KEY in pcs response headers | 服务器返回值KEY不存在 |

1630
docs/file_data_apis_list.md Normal file

File diff suppressed because it is too large Load Diff

191
docs/overview.md Normal file
View File

@@ -0,0 +1,191 @@
# 概述
百度开放云平台为广大开发者提供了访问PCS资源的系列接口目前开放的接口主要分两个部分
* 文件API:
主要提供文件上传、下载、拷贝、删除、搜索、断点续传及缩略图等功能。
* 结构化数据API:
主要提供结构数据存储、查询、删除及同步等功能。
通过对这些API的组合调用开发者可以实现基本的用户文件操作以及结构数据存储和管理功能也能够支持用户数据在多种不同终端上的同步以提供更优质的用户体验。
除了原生的RESTRepresentational State Transfer即“表述性状态转移” API之外百度开放云平台还提供了多种平台的SDK来帮助开发者缩短开发周期具体请参考“SDK”部分相关内容。
## PCS REST API使用说明
### 开通PCS API权限
PCS所有REST API都必须经过开通权限才能正常使用。申请的方法请参考“开通PCS API权限”部分相关内容。
注意PCS未提供分享接口download等接口仅供个人获取数据使用。 access_token不能泄露否则会直接封禁应用。
### API请求方式说明
目前所有的提交类接口仅支持POST方式查询类接口同时支持POST方式和GET方式。
PCS REST API的所有参数在传入时应当使用UTF-8编码。
#### HTTP 请求方式
GET | POST
#### URL
https://pcs.baidu.com/rest/2.0/pcs/{object_name}?{query_string}
#### 参数说明
| 参数名称 | 描述 |
| :- | :- |
| object_name | PCS REST API操作实体名称quota、file、thumbnail。 |
| query_string | 放在HTTP头部传入的参数必须经过UrlEncode编码。 |
#### HTTP GET和POST方式使用说明
<table width="600" border="1" cellpadding="1" cellspacing="1">
<tbody>
<tr>
<th scope="row" width="80">请求方式
</th>
<th scope="col">GET
</th>
<th scope="col">POST
</th>
</tr>
<tr>
<th scope="row">URL
</th>
<td colspan="2">https://pcs.baidu.com/rest/2.0/pcs/{object_name}?{query_string}
</td>
</tr>
<tr>
<th scope="row">请求参数
</th>
<td> 全部携带在 HTTPS 请求头部的 query_string 中。
</td>
<td> 既可携带在 query_string 中,也可携带在 HTTP Body 中。
<dl>
<dd>
<ul>
<li> method 及 access token 等参数必须携带在 query_string 中进行传输请参考各个API的具体说明
</li>
<li> 携带在 query_string 中的参数的值,必须进行 UrlEncode 编码;
</li>
<li> 携带在 HTTP Body 中的参数,则不需要进行 UrlEncode 编码。
</li>
</ul>
</dd>
</dl>
<div style="border:solid 1px #d7d7d7;padding:10px 16px 2px 16px; background-color:#fbfafb;">
<div>注</div>
HTTP URL 长度有限,若参数值长度过长,建议将参数放在 HTTP Body 中进行传输。</div>
</td>
</tr>
<tr>
<th scope="row"> HTTP BODY
</th>
<td> 不携带HTTP Body
</td>
<td> multipart/form-data
</td>
</tr>
<tr>
<th scope="row">注意
</th>
<td colspan="2">如果 HTTP Body 和 query_string 存在相同的参数,则以 query_string 中的参数为准。
</td>
</tr>
</tbody>
</table>
#### 使用示例
1. GET请求
用HTTP GET请求方式发送两个参数key1=value1和key2=value2。
https://pcs.baidu.com/rest/2.0/pcs/quota?key1=UrlEncode(value1)&key2=UrlEncode(value2)
2. POST请求
分别用两种方式使用POST方式发送三个参数key1=value1、key2=value2和key3=value3方式一与方式二效果等同。
##### 方式一:
POST /rest/2.0/pcs/quota?key2=value2&key3=value3 HTTP/1.1
User-Agent: curl/7.12.1 (x86_64-redhat-linux-gnu) libcurl/7.12.1 OpenSSL/0.9.7a zlib/1.2.1.2 libidn/0.5.6
Pragma: no-cache
Accept: */*
Host:pcs.baidu.com
Content-Length:123
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryS0JIa4uHF7yHd8xJ
------WebKitFormBoundaryS0JIa4uHF7yHd8xJ
Content-Disposition: form-data; name="key1"
value1
------WebKitFormBoundaryS0JIa4uHF7yHd8xJ—
##### 方式二:
POST /rest/2.0/pcs/quota HTTP/1.1
User-Agent: curl/7.12.1 (x86_64-redhat-linux-gnu) libcurl/7.12.1 OpenSSL/0.9.7a zlib/1.2.1.2 libidn/0.5.6
Pragma: no-cache
Accept: */*
Host:pcs.baidu.com
Content-Length:123
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryS0JIa4uHF7yHd8xJ
------WebKitFormBoundaryS0JIa4uHF7yHd8xJ
Content-Disposition: form-data; name="key1"
value1
------WebKitFormBoundaryS0JIa4uHF7yHd8xJ
Content-Disposition: form-data; name="key2"
value2
------WebKitFormBoundaryS0JIa4uHF7yHd8xJ
Content-Disposition: form-data; name="key3"
value3
------WebKitFormBoundaryS0JIa4uHF7yHd8xJ--
### API响应格式说明
<table>
<tbody>
<tr>
<th scope="col">
</th>
<th scope="col">正常请求
</th>
<th scope="col">异常请求
</th>
</tr>
<tr>
<th scope="row">HTTP状态码
</th>
<td> 200 OK
</td>
<td> 4**&nbsp;: 用户请求错误。<br>5** server服务失败。
</td>
</tr>
<tr>
<th scope="row">HTTP BODY
</th>
<td> API响应内容
</td>
<td> 异常请求的返回值为JSON字符串。<br>例如:<br>{<br>"error_code":110,<br>"error_msg":"Access token invalid or no longer valid",<br>"request_id":729562373<br>}<br>说明:<br>
<dl>
<dd>- error_code错误码<br>
</dd>
<dd>- error_msg: 错误描述信息;<br>
</dd>
<dd>- request_id: 请求ID。由server生成用于追查和定位请求日志。<br>
</dd>
</dl>
</td>
</tr>
</tbody>
</table>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,90 @@
# 结构化数据API错误码
结构化数据API以HTTP提供因此请求者应首先检查HTTP协议级别的响应状态码。 当请求有错误或执行失败时HTTP协议的返回响应状态码不为“200”且在Content-body的JSON格式数据中以error_code给出错误码并可能以error_msg字段提示错误信息。错误码的详细信息请参考以下错误码列表。 一般来说, 错误分为客户端错误和服务器端错误:
* 客户端错误:
返回状态码4xx表示结构化数据平台认为客户端请求有错误例如auth失败参数不对超出quota等。这些情况客户端需要先解决参数问题再向服务器端重新发起请求。
* 服务器端错误:
返回状态码为5xx表示结构化数据平台内部发生错误客户端需要重试。
## 注意
请求成功时HTTP协议的返回响应状态码为“200”不会设置error_code和error_msg。
## 结构化数据API错误码
| HTTP状态码 | 错误码 | 错误信息 | 备注 | 是否重试 |
| - | :- | :- | :- | - |
| 500 | 1 | Unknown error | 未知错误 | 是 |
| 500 | 2 | Service temporarily unavailable | 服务暂不可用 | 是 |
| 403 | 6 | No permission to access user data | 无权访问用户数据 | 否 |
| 403 | 7 | No permission to access data for this referer | 无权访问数据 | 否 |
| 400 | 100 | Invalid parameter | 无效参数 | 否 |
| 401 | 101 | Invalid API key | 无效API Key | 否 |
| 401 | 102 | Session key invalid or no longer valid | 会话密钥无效 | 否 |
| 401 | 103 | Invalid/Used call_id parameter | call_id参数无效/已被使用 | 否 |
| 400 | 104 | Incorrect signature | 签名错误 | 否 |
| 400 | 105 | Too many parameters | 参数过多 | 否 |
| 400 | 106 | Unsupported signature method | 不支持此签名方式 | 否 |
| 400 | 107 | Invalid/Used timestamp parameter | 时间戳无效 | 否 |
| 401 | 108 | Invalid user id | 用户ID无效 | 否 |
| 400 | 109 | Invalid user info field | 用户信息字段无效 | 否 |
| 401 | 110 | Access token invalid or no longer valid | Access token无效或已失效 | 否 |
| 401 | 111 | Access token expired | Access token已过期 | 否 |
| 401 | 112 | Session key expired | 会话密钥已过期 | 否 |
| 400 | 114 | Invalid Ip | 无效IP | 否 |
| 400 | 31400 | param error | 参数错误 | 否 |
| 400 | 31401 | malformed json | JSON格式错误 | 否 |
| 400 | 31402 | no "table" in request | 请求中没有“table”字段 | 否 |
| 400 | 31403 | no "records" in request | 请求中没有“records”字段 | 否 |
| 400 | 31405 | too many records in request | 请求中的records 过多目前限制为500 | 否 |
| 400 | 31406 | bad columnname | 列名非法请参考API文档 | 否 |
| 400 | 31407 | record too large | record过大> 1M | 否 |
| 400 | 31408 | bad table name | table名称不合法 | 否 |
| 400 | 31409 | table not exist | table不存在请先创建 | 否 |
| 400 | 31410 | bad record | record格式错误请检查JSON | 否 |
| 400 | 31411 | no appid | 请求中没有“app_id”字段 | 否 |
| 400 | 31412 | no userid | 请求中没有“user_id”字段 | 否 |
| 400 | 31420 | bad condition | condition描述错误。 | 否 |
| 400 | 31421 | bad projection | projection描述错误 | 否 |
| 400 | 31422 | bad order_by | order_by描述错误 | 否 |
| 400 | 31423 | bad operator | condition中的operation 非法 | 否 |
| 400 | 31424 | bad start/limit | start/limit 错误 | 否 |
| 400 | 31425 | unsupported operator | 操作符暂未支持or、like、regex等 | 否 |
| 400 | 31430 | no key in record | update/delete 请求但是record 中没有_key 字段 | 否 |
| 400 | 31431 | record not exist | 符合条件的record不存在比如if-match不匹配、在回收站等 | 否 |
| 400 | 31432 | unknown op | 参数op非法 | 否 |
| 400 | 31433 | bad key | key非法 | 否 |
| 400 | 31440 | param cursor not set | 参数cursor未设值 | 否 |
| 400 | 31441 | param cursor format error | 参数cursor格式错误 | 否 |
| 400 | 31442 | param cursor appid wrong | 参数cursor appid错误 | 否 |
| 400 | 31443 | param cursor user_id wrong | 参数cursor user_id错误 | 否 |
| 400 | 31450 | exceed quota | 超出配额 | 否 |
| 400 | 31451 | quota size param not exist | 找不到参数quota size | 否 |
| 503 | 31452 | quota info fail | quota info失败 | 是 |
| 400 | 31453 | quota too big | quota过大 | 否 |
| 400 | 31454 | quota size param not numberic | quota size 参数未数值化 | 否 |
| 400 | 31460 | no permission | 未授权 | 否 |
| 400 | 31461 | account not login | 账户为登录使用bduss认证失败 | 否 |
| 400 | 31462 | access token errro | access token校验失败 | 否 |
| 400 | 31470 | index num too much | index num太多 | 否 |
| 400 | 31472 | table already exist | table已存在 | 否 |
| 400 | 31473 | abnormal table already exist | 异常table已存在 | 否 |
| 400 | 31474 | table not drop, cannot restore | table不在回收站无法恢复 | 否 |
| 400 | 31475 | engine not support | 不支持此项操作 | 否 |
| 400 | 31480 | param op wrong, should be recycled or permanent | 参数op错误应为可回收或永久的 | 否 |
| 400 | 31490 | api not support | 调用了错误的API | 否 |
| 500 | 31500 | Internal error (Try Again Later) | 内部错误 | 是 |
| 503 | 31501 | storeengine construct fail | construct失败 | 是 |
| 503 | 31502 | storeengine select fail | 选择操作失败 | 是 |
| 503 | 31503 | storeengine insert fail | 插入操作失败 | 是 |
| 503 | 31504 | storeengine update fail | 更新操作失败 | 是 |
| 503 | 31505 | storeengine delete fail | 删除操作失败 | 是 |
| 503 | 31506 | storeengine count fail | count操作失败 | 是 |
| 503 | 31507 | storeengine ensure index fail | 查询或创建索引失败 | 是 |
| 503 | 31508 | storeengine delete index fail | 删除索引失败 | 是 |
| 503 | 31509 | storeengine drop table fail | 删除table操作失败 | 是 |
| 503 | 31530 | config set num match fail | 配置中num匹配失败 | 是 |
| 503 | 31590 | db query error | db交互出错 | 是 |
| 503 | 31591 | network error | 内部网络交互错误 | 是 |

View File

@@ -0,0 +1,128 @@
# 结构化数据API说明
## 结构化数据API简要说明
结构化数据API包括table操作API和record操作API。
* Table操作API
是由开发者在使用结构化数据API开发应用时调用包括创建、修改、删除、查看表信息及恢复表等接口
* Record操作API
即数据操作API是由最终用户在使用基于PCS结构化数据API开发的应用时触发第三方应用通过使用用户身份以Access Token认证在服务器端对用户数据进行增加、删除、修改、查询等系列操作。
## 结构化数据基本概念
1.table
结构化数据以表为单位组织用户数据;表名和表中的索引由开发者定义。
2. 表名tablename
表的名字。表名可以由字母和下划线组成a-zA-Z0-9'_', 长度为3-63个字符必须字母开头同一个应用中不能重复使用同一表名。
3. 记录record
每一条结构化数据就是一个记录,一个表中可有多条记录。
4.column
结构中的每一个字段就是一列。列名可以由字母和下划线组成a-zA-Z0-9'_', 长度为1-255个字符必须字母开头。
5. 索引index
在表中的某些列上创建索引可以提高查询速度。一个表中最多可以定义5个索引只能定义普通索引。
6. 限额quota
同一个表中同一个用户最多多少条数据现在每个用户最多10000条数据。
## 结构化数据功能列表
使用PCS结构化数据功能可以轻松的完成应用上结构化数据的存储及同步等功能。
PCS结构化数据具有如下的特性
* 提供Restful API
* 数据灵活性大:定义表时,不需要完全确定表中的所有列,可以增加一列或减少一列而无需重新定义表。
* 在数据上动态增减索引:定义表时,无需完全确定所有的索引;
* 数据多端同步:结构化数据提供接口,支持数据多端同步;
* 对数据进行增加、删除、修改及查询;
* 回收站数据删除后10天内可以还原
* 支持应用数据隔离:不同应用的数据可以支持物理隔离;
* 支持超大数据量:
* 一个应用可以创建5个表
* 单个表没有数据总量限制;
* 单个表中单个用户的数据量可以达到1万条
* 用户数无限制。
## 结构化数据存储限制
### 1全局限制
每个记录的大小限制为1MB 服务器端会把记录做json_encode, 得到的大小为记录大小);
每个应用最多只能创建5个表
批量的insert/update/delete/restore每次最多500条
批量的select每次最多返回10000条记录。
### 2表级别限制quota
应用上的用户在表里的条目数限制创建表的时候每个表都有一个default_quota以后用户在该表上都使用此default_quota单个用户的quota可以通过quota接口设置
## Record操作API说明
### 1 insert、update、delete默认都支持批量操作但是每次批量的record个数不能超过500
### 2 数据操作API对用户需要保持最终一致性
insert、update、delete操作不保证返回后即可select 出来通常延迟小于1s
update、delete操作不支持条件操作即where子句的操作如有需要请先select出来。
## 字段说明
### 字段定义
#### PCS结构化数据平台中表中的字段支持如下类型
| 字段类型 | 描述 |
| :- | :- |
| string | 字符串。 |
| int | 整型数字64位有符号范围为[-9223372036854775808, 9223372036854775807]超出范围自动转为float。 |
| float | 带小数点的数字。 |
| boolean | true 或者false。 |
| array | 数组元素可以是string/number。 |
| object | 可以是一个json 对象。 |
| null | null |
#### 说明:
字段名必须符合以下标准:
可以是a-z、A-Z、0-9、'_'
长度范围是[1, 255]
必须字母开头。
### 默认字段
#### 插入的记录将会有以下默认字段:
| 字段 | 类型 | 描述 |
| :- | :- | :- |
| _key | string | 全局唯一key i string(16)。 |
| _ctime | int | record创建时间。 |
| _mtime | int | record修改时间。 |
| _isdelete | int | 是否删除。 |

16
entitlements.xml Normal file
View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>application-identifier</key>
<string>com.iikira.baidupcsgo</string>
<key>get-task-allow</key>
<true/>
<key>platform-application</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>com.iikira.baidupcsgo</string>
</array>
</dict>
</plist>

28
go.mod Normal file
View File

@@ -0,0 +1,28 @@
module github.com/iikira/BaiduPCS-Go
go 1.12
require (
github.com/GeertJohan/go.incremental v1.0.0
github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da // indirect
github.com/astaxie/beego v1.10.1 // indirect
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/fatih/color v0.0.0-20150510220652-1b35f289c47d
github.com/golang/protobuf v1.3.2
github.com/iikira/Baidu-Login v1.2.2-0.20180427090606-ecf146973528
github.com/iikira/baidu-tools v0.0.0-20190609113215-4dd64618064d
github.com/json-iterator/go v1.1.7
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1
github.com/mattn/go-runewidth v0.0.5-0.20181218000649-703b5e6b11ae
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce
github.com/peterh/liner v1.1.1-0.20190305032635-6f820f8f90ce
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586
golang.org/x/sys v0.0.0-20191025090151-53bf42e6b339
github.com/tidwall/gjson v1.6.0
)

96
go.sum Normal file
View File

@@ -0,0 +1,96 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da h1:UVU3a9pRUyLdnBtn60WjRl0s4SEyJc2ChCY56OAR6wI=
github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da/go.mod h1:DgrzXonpdQbfN3uYaGz1EG4Sbhyum/MMIn6Cphlh2bw=
github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/astaxie/beego v1.10.1 h1:M2ciUnyiZycuTpGEA+idJF0gX24h58EbPvGqjnO/DCg=
github.com/astaxie/beego v1.10.1/go.mod h1:0R4++1tUqERR0WYFWdfkcrsyoVBCG4DgpDGokT3yb+U=
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb h1:tUf55Po0vzOendQ7NWytcdK0VuzQmfAgvGBUOQvN0WA=
github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb/go.mod h1:U0vRfAucUOohvdCxt5MWLF+TePIL0xbCkbKIiV8TQCE=
github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/fatih/color v0.0.0-20150510220652-1b35f289c47d h1:wMOLsfJ92CRGk3+GtVk+aaUme+jWTH21jH3cVL1jzmI=
github.com/fatih/color v0.0.0-20150510220652-1b35f289c47d/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/iikira/Baidu-Login v0.0.0-20180427090606-ecf146973528 h1:WaZCDfR+C0z17lO7syZFXpJ7d8YUxv5iG1/PeYzjKiM=
github.com/iikira/Baidu-Login v0.0.0-20180427090606-ecf146973528/go.mod h1:oMWxZOoEMgQG+0rq8xhEIdxvXwkyG4aN3sZoOQ59R4Q=
github.com/iikira/Baidu-Login v1.2.2-0.20180427090606-ecf146973528 h1:An+h6HX8d2omGXt69E5bsMjOmOgbuFQtrddp2clNCOU=
github.com/iikira/Baidu-Login v1.2.2-0.20180427090606-ecf146973528/go.mod h1:oMWxZOoEMgQG+0rq8xhEIdxvXwkyG4aN3sZoOQ59R4Q=
github.com/iikira/baidu-tools v0.0.0-20180922000818-79c93d8033cd h1:meH/UWnJi8GuWEjMFivCnwp2NWdqhDnTsIIkb5SptZQ=
github.com/iikira/baidu-tools v0.0.0-20180922000818-79c93d8033cd/go.mod h1:UHD1R9RGXz5nDsKm3KbdD3yxCJZ18nmR+1ZAc5GkrNw=
github.com/iikira/baidu-tools v0.0.0-20190609113215-4dd64618064d h1:nmsuPx1si1QI3boPuaQjxp536X7RAeLW26DBmcdRXO0=
github.com/iikira/baidu-tools v0.0.0-20190609113215-4dd64618064d/go.mod h1:UHD1R9RGXz5nDsKm3KbdD3yxCJZ18nmR+1ZAc5GkrNw=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.5-0.20181218000649-703b5e6b11ae h1:575usOHCDzxtuyWeSjPySew7uvoOuaCsM0uUesjgAo4=
github.com/mattn/go-runewidth v0.0.5-0.20181218000649-703b5e6b11ae/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb h1:x0yCvYsspui5SAxSRvLd2zFg7PfFijzKdCo7QAtN92I=
github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb/go.mod h1:ym0w0flrmBtGvApLDgFLa0sfGJkWxDQqnm0/0ok5w3Y=
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce h1:RLmZmfx/K62HKpbwPqtW3tg+V2GgugN/XNNx+uiMH/Y=
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
github.com/peterh/liner v1.1.0 h1:f+aAedNJA6uk7+6rXsYBnhdo4Xux7ESLe+kcuVUF5os=
github.com/peterh/liner v1.1.0/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
github.com/peterh/liner v1.1.1-0.20190305032635-6f820f8f90ce h1:Lz+a/i+oS4A7tb6J6IyH4ZFiWgqvNv2yslv0Qn79wok=
github.com/peterh/liner v1.1.1-0.20190305032635-6f820f8f90ce/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo=
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f h1:xKDKjIsL76VUyHcA0G4Qe1cIAUB/nrq6Pt8D411bd1g=
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f/go.mod h1:qXyCeJubPqsgeiLd3kvHOGHHSrQcNdjZ2ScXIcVZK/I=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191025090151-53bf42e6b339 h1:zSqWKgm/o7HAnlAzBQ+aetp9fpuyytsXnKA8eiLHYQM=
golang.org/x/sys v0.0.0-20191025090151-53bf42e6b339/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

37
internal/pcscommand/cd.go Normal file
View File

@@ -0,0 +1,37 @@
package pcscommand
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/internal/pcsconfig"
)
// RunChangeDirectory 执行更改工作目录
func RunChangeDirectory(targetPath string, isList bool) {
pcs := GetBaiduPCS()
err := matchPathByShellPatternOnce(&targetPath)
if err != nil {
fmt.Println(err)
return
}
data, err := pcs.FilesDirectoriesMeta(targetPath)
if err != nil {
fmt.Println(err)
return
}
if !data.Isdir {
fmt.Printf("错误: %s 不是一个目录 (文件夹)\n", targetPath)
return
}
GetActiveUser().Workdir = targetPath
pcsconfig.Config.Save()
fmt.Printf("改变工作目录: %s\n", targetPath)
if isList {
RunLs(".", nil, baidupcs.DefaultOrderOptions)
}
}

View File

@@ -0,0 +1,90 @@
package pcscommand
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs"
)
// RunCloudDlAddTask 执行添加离线下载任务
func RunCloudDlAddTask(sourceURLs []string, savePath string) {
var (
err error
pcs = GetBaiduPCS()
)
err = matchPathByShellPatternOnce(&savePath)
if err != nil {
fmt.Println(err)
return
}
var taskid int64
for k := range sourceURLs {
taskid, err = pcs.CloudDlAddTask(sourceURLs[k], savePath+baidupcs.PathSeparator)
if err != nil {
fmt.Printf("[%d] %s, 地址: %s\n", k+1, err, sourceURLs[k])
continue
}
fmt.Printf("[%d] 添加离线任务成功, 任务ID(task_id): %d, 源地址: %s, 保存路径: %s\n", k+1, taskid, sourceURLs[k], savePath)
}
}
// RunCloudDlQueryTask 精确查询离线下载任务
func RunCloudDlQueryTask(taskIDs []int64) {
cl, err := GetBaiduPCS().CloudDlQueryTask(taskIDs)
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Println(cl)
}
// RunCloudDlListTask 查询离线下载任务列表
func RunCloudDlListTask() {
cl, err := GetBaiduPCS().CloudDlListTask()
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Println(cl)
}
// RunCloudDlCancelTask 取消离线下载任务
func RunCloudDlCancelTask(taskIDs []int64) {
for _, id := range taskIDs {
err := GetBaiduPCS().CloudDlCancelTask(id)
if err != nil {
fmt.Printf("[%d] %s\n", id, err)
continue
}
fmt.Printf("[%d] 取消成功\n", id)
}
}
// RunCloudDlDeleteTask 删除离线下载任务
func RunCloudDlDeleteTask(taskIDs []int64) {
for _, id := range taskIDs {
err := GetBaiduPCS().CloudDlDeleteTask(id)
if err != nil {
fmt.Printf("[%d] %s\n", id, err)
continue
}
fmt.Printf("[%d] 删除成功\n", id)
}
}
// RunCloudDlClearTask 清空离线下载任务记录
func RunCloudDlClearTask() {
total, err := GetBaiduPCS().CloudDlClearTask()
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Printf("%s成功, 共清除 %d 条记录\n", baidupcs.OperationCloudDlClearTask, total)
return
}

View File

@@ -0,0 +1,155 @@
package pcscommand
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"path"
"strings"
)
// RunCopy 执行 批量拷贝文件/目录
func RunCopy(paths ...string) {
runCpMvOp("copy", paths...)
}
// RunMove 执行 批量 重命名/移动 文件/目录
func RunMove(paths ...string) {
runCpMvOp("move", paths...)
}
func runCpMvOp(op string, paths ...string) {
err := cpmvPathValid(paths...) // 检查路径的有效性, 目前只是判断数量
if err != nil {
fmt.Printf("%s path error, %s\n", op, err)
return
}
froms, to := cpmvParsePath(paths...) // 分割
froms, err = matchPathByShellPattern(froms...)
if err != nil {
fmt.Println(err)
return
}
to = GetActiveUser().PathJoin(to)
// 尝试匹配
if strings.ContainsAny(to, baidupcs.ShellPatternCharacters) {
tos, _ := matchPathByShellPattern(to)
switch len(tos) {
case 0:
// do nothing
case 1:
to = tos[0]
default:
fmt.Printf("目标目录有 %d 条匹配结果, 请检查通配符\n", len(tos))
return
}
}
pcs := GetBaiduPCS()
toInfo, pcsError := pcs.FilesDirectoriesMeta(to)
switch {
case toInfo != nil && toInfo.Path != to:
fallthrough
case pcsError != nil && pcsError.GetErrType() == pcserror.ErrTypeRemoteError:
// 判断路径是否存在
// 如果不存在, 则为重命名或同目录拷贝操作
// 如果 froms 数不是1, 则意义不明确.
if len(froms) != 1 {
fmt.Println(err)
return
}
if op == "copy" { // 拷贝
err = pcs.Copy(&baidupcs.CpMvJSON{
From: froms[0],
To: to,
})
if err != nil {
fmt.Println(err)
fmt.Println("文件/目录拷贝失败: ")
fmt.Printf("%s <-> %s\n", froms[0], to)
return
}
fmt.Println("文件/目录拷贝成功: ")
fmt.Printf("%s <-> %s\n", froms[0], to)
} else { // 重命名
err = pcs.Rename(froms[0], to)
if err != nil {
fmt.Println(err)
fmt.Println("重命名失败: ")
fmt.Printf("%s -> %s\n", froms[0], to)
return
}
fmt.Println("重命名成功: ")
fmt.Printf("%s -> %s\n", froms[0], to)
}
return
case pcsError != nil && pcsError.GetErrType() != pcserror.ErrTypeRemoteError:
fmt.Println(pcsError)
return
}
if !toInfo.Isdir {
fmt.Printf("目标 %s 不是一个目录, 操作失败\n", toInfo.Path)
return
}
cj := new(baidupcs.CpMvListJSON)
cj.List = make([]*baidupcs.CpMvJSON, len(froms))
for k := range froms {
cj.List[k] = &baidupcs.CpMvJSON{
From: froms[k],
To: path.Clean(to + baidupcs.PathSeparator + path.Base(froms[k])),
}
}
switch op {
case "copy":
err = pcs.Copy(cj.List...)
if err != nil {
fmt.Println(err)
fmt.Println("操作失败, 以下文件/目录拷贝失败: ")
fmt.Println(cj)
return
}
fmt.Println("操作成功, 以下文件/目录拷贝成功: ")
fmt.Println(cj)
case "move":
err = pcs.Move(cj.List...)
if err != nil {
fmt.Println(err)
fmt.Println("操作失败, 以下文件/目录移动失败: ")
fmt.Println(cj)
return
}
fmt.Println("操作成功, 以下文件/目录移动成功: ")
fmt.Println(cj)
default:
panic("Unknown operation:" + op)
}
return
}
// cpmvPathValid 检查路径的有效性
func cpmvPathValid(paths ...string) (err error) {
if len(paths) <= 1 {
return fmt.Errorf("参数不完整")
}
return nil
}
// cpmvParsePath 解析路径
func cpmvParsePath(paths ...string) (froms []string, to string) {
if len(paths) == 0 {
return nil, ""
}
froms = paths[:len(paths)-1]
to = paths[len(paths)-1]
return
}

View File

@@ -0,0 +1,183 @@
package pcscommand
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"github.com/iikira/BaiduPCS-Go/internal/pcsconfig"
"github.com/iikira/BaiduPCS-Go/internal/pcsfunctions/pcsdownload"
"github.com/iikira/BaiduPCS-Go/pcstable"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"github.com/iikira/BaiduPCS-Go/pcsutil/taskframework"
"github.com/iikira/BaiduPCS-Go/requester/downloader"
"github.com/iikira/BaiduPCS-Go/requester/transfer"
"os"
"path/filepath"
"runtime"
)
type (
//DownloadOptions 下载可选参数
DownloadOptions struct {
IsTest bool
IsPrintStatus bool
IsExecutedPermission bool
IsOverwrite bool
DownloadMode pcsdownload.DownloadMode
SaveTo string
Parallel int
Load int
MaxRetry int
NoCheck bool
}
// LocateDownloadOption 获取下载链接可选参数
LocateDownloadOption struct {
FromPan bool
}
)
func downloadPrintFormat(load int) string {
if load <= 1 {
return pcsdownload.DefaultPrintFormat
}
return "[%d] ↓ %s/%s %s/s in %s, left %s ...\n"
}
// RunDownload 执行下载网盘内文件
func RunDownload(paths []string, options *DownloadOptions) {
if options == nil {
options = &DownloadOptions{}
}
if options.Load <= 0 {
options.Load = pcsconfig.Config.MaxDownloadLoad
}
if options.MaxRetry < 0 {
options.MaxRetry = pcsdownload.DefaultDownloadMaxRetry
}
if runtime.GOOS == "windows" {
// windows下不加执行权限
options.IsExecutedPermission = false
}
// 设置下载配置
cfg := &downloader.Config{
Mode: transfer.RangeGenMode_BlockSize,
CacheSize: pcsconfig.Config.CacheSize,
BlockSize: baidupcs.MaxDownloadRangeSize,
MaxRate: pcsconfig.Config.MaxDownloadRate,
InstanceStateStorageFormat: downloader.InstanceStateStorageFormatProto3,
IsTest: options.IsTest,
TryHTTP: !pcsconfig.Config.EnableHTTPS,
}
// 设置下载最大并发量
if options.Parallel < 1 {
options.Parallel = pcsconfig.Config.MaxParallel
}
paths, err := matchPathByShellPattern(paths...)
if err != nil {
fmt.Println(err)
return
}
fmt.Print("\n")
fmt.Printf("[0] 提示: 当前下载最大并发量为: %d, 下载缓存为: %d\n", options.Parallel, cfg.CacheSize)
var (
pcs = GetBaiduPCS()
loadCount = 0
)
// 预测要下载的文件数量
// TODO: pcscache
for k := range paths {
pcs.FilesDirectoriesRecurseList(paths[k], baidupcs.DefaultOrderOptions, func(depth int, _ string, fd *baidupcs.FileDirectory, pcsError pcserror.Error) bool {
if pcsError != nil {
pcsCommandVerbose.Warnf("%s\n", pcsError)
return true
}
// 忽略统计文件夹数量
if !fd.Isdir {
loadCount++
if loadCount >= options.Load {
return false
}
}
return true
})
if loadCount >= options.Load {
break
}
}
// 修改Load, 设置MaxParallel
if loadCount > 0 {
options.Load = loadCount
// 取平均值
cfg.MaxParallel = pcsconfig.AverageParallel(options.Parallel, loadCount)
} else {
cfg.MaxParallel = options.Parallel
}
var (
executor = taskframework.TaskExecutor{
IsFailedDeque: true, // 统计失败的列表
}
statistic = &pcsdownload.DownloadStatistic{}
)
// 处理队列
for k := range paths {
newCfg := *cfg
unit := pcsdownload.DownloadTaskUnit{
Cfg: &newCfg, // 复制一份新的cfg
PCS: pcs,
VerbosePrinter: pcsCommandVerbose,
PrintFormat: downloadPrintFormat(options.Load),
ParentTaskExecutor: &executor,
DownloadStatistic: statistic,
IsPrintStatus: options.IsPrintStatus,
IsExecutedPermission: options.IsExecutedPermission,
IsOverwrite: options.IsOverwrite,
NoCheck: options.NoCheck,
DownloadMode: options.DownloadMode,
PcsPath: paths[k],
}
// 设置储存的路径
if options.SaveTo != "" {
unit.SavePath = filepath.Join(options.SaveTo, filepath.Base(paths[k]))
} else {
// 使用默认的保存路径
unit.SavePath = GetActiveUser().GetSavePath(paths[k])
}
info := executor.Append(&unit, options.MaxRetry)
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), paths[k])
}
// 开始计时
statistic.StartTimer()
// 开始执行
executor.Execute()
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", statistic.Elapsed()/1e6*1e6, converter.ConvertFileSize(statistic.TotalSize()))
// 输出失败的文件列表
failedList := executor.FailedDeque()
if failedList.Size() != 0 {
fmt.Printf("以下文件下载失败: \n")
tb := pcstable.NewTable(os.Stdout)
for e := failedList.Shift(); e != nil; e = failedList.Shift() {
item := e.(*taskframework.TaskInfoItem)
tb.Append([]string{item.Info.Id(), item.Unit.(*pcsdownload.DownloadTaskUnit).PcsPath})
}
tb.Render()
}
}

View File

@@ -0,0 +1,216 @@
package pcscommand
import (
"container/list"
"fmt"
"os"
"path"
"strings"
"time"
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"github.com/iikira/BaiduPCS-Go/pcsutil/pcstime"
)
type (
etask struct {
*ListTask
path string
rootPath string
fd *baidupcs.FileDirectory
err pcserror.Error
}
// ExportOptions 导出可选项
ExportOptions struct {
RootPath string // 根路径
SavePath string // 输出路径
MaxRetry int
Recursive bool
LinkFormat bool
}
)
func (task *etask) handleExportTaskError(l *list.List, failedList *list.List) {
if task.err == nil {
return
}
// 不重试
switch task.err.GetError() {
case baidupcs.ErrGetRapidUploadInfoMD5NotFound, baidupcs.ErrGetRapidUploadInfoCrc32NotFound:
fmt.Printf("[%d] - [%s] 导出失败, 可能是服务器未刷新文件的md5, 请过一段时间再试一试\n", task.ID, task.path)
failedList.PushBack(task)
return
case baidupcs.ErrFileTooLarge:
fmt.Printf("[%d] - [%s] 导出失败, 文件大于20GB, 无法导出\n", task.ID, task.path)
failedList.PushBack(task)
return
}
// 未达到失败重试最大次数, 将任务推送到队列末尾
if task.retry < task.MaxRetry {
task.retry++
fmt.Printf("[%d] - [%s] 导出错误, %s, 重试 %d/%d\n", task.ID, task.path, task.err, task.retry, task.MaxRetry)
l.PushBack(task)
time.Sleep(3 * time.Duration(task.retry) * time.Second)
} else {
fmt.Printf("[%d] - [%s] 导出错误, %s\n", task.ID, task.path, task.err)
failedList.PushBack(task)
}
}
func changeRootPath(dstRootPath, dstPath, srcRootPath string) string {
if srcRootPath == "" {
return dstPath
}
return path.Join(srcRootPath, strings.TrimPrefix(dstPath, dstRootPath))
}
// GetExportFilename 获取导出路径
func GetExportFilename() string {
return "BaiduPCS-Go_export_" + pcstime.BeijingTimeOption("") + ".txt"
}
// RunExport 执行导出文件和目录
func RunExport(pcspaths []string, opt *ExportOptions) {
if opt == nil {
opt = &ExportOptions{}
}
if opt.SavePath == "" {
opt.SavePath = GetExportFilename()
}
pcspaths, err := matchPathByShellPattern(pcspaths...)
if err != nil {
fmt.Println(err)
return
}
saveFile, err := os.OpenFile(opt.SavePath, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
if err != nil { // 不可写
fmt.Printf("%s\n", err)
return
}
defer saveFile.Close()
fmt.Printf("导出的信息将保存在: %s\n", opt.SavePath)
var (
au = GetActiveUser()
pcs = GetBaiduPCS()
l = list.New()
failedList = list.New()
writeErr error
id int
)
for id = range pcspaths {
var rootPath string
if pcspaths[id] == au.Workdir {
rootPath = pcspaths[id]
} else {
rootPath = path.Dir(pcspaths[id])
}
// 加入队列
l.PushBack(&etask{
ListTask: &ListTask{
ID: id,
MaxRetry: opt.MaxRetry,
},
path: pcspaths[id],
rootPath: rootPath,
})
}
for {
e := l.Front()
if e == nil { // 结束
break
}
l.Remove(e) // 载入任务后, 移除队列
task := e.Value.(*etask)
root := task.fd == nil
// 获取文件信息
if task.fd == nil { // 第一次初始化
fd, pcsError := pcs.FilesDirectoriesMeta(task.path)
if pcsError != nil {
task.err = pcsError
task.handleExportTaskError(l, failedList)
continue
}
task.fd = fd
}
if task.fd.Isdir { // 导出目录
if !root && !opt.Recursive { // 非递归
continue
}
fds, pcsError := pcs.FilesDirectoriesList(task.path, baidupcs.DefaultOrderOptions)
if pcsError != nil {
task.err = pcsError
task.handleExportTaskError(l, failedList)
continue
}
if len(fds) == 0 {
_, writeErr = saveFile.Write(converter.ToBytes(fmt.Sprintf("BaiduPCS-Go mkdir \"%s\"\n", changeRootPath(task.rootPath, task.path, opt.RootPath))))
if writeErr != nil {
fmt.Printf("写入文件失败: %s\n", writeErr)
return // 直接返回
}
fmt.Printf("[%d] - [%s] 导出成功\n", task.ID, task.path)
continue
}
// 加入队列
for _, fd := range fds {
// 加入队列
id++
l.PushBack(&etask{
ListTask: &ListTask{
ID: id,
MaxRetry: opt.MaxRetry,
},
path: fd.Path,
fd: fd,
rootPath: task.rootPath,
})
}
continue
}
rinfo, pcsError := pcs.ExportByFileInfo(task.fd)
if pcsError != nil {
task.err = pcsError
task.handleExportTaskError(l, failedList)
continue
}
var outTemplate string = fmt.Sprintf("BaiduPCS-Go rapidupload -length=%d -md5=%s -slicemd5=%s -crc32=%s \"%s\"\n", rinfo.ContentLength, rinfo.ContentMD5, rinfo.SliceMD5, rinfo.ContentCrc32, changeRootPath(task.rootPath, task.path, opt.RootPath))
if opt.LinkFormat {
outTemplate = fmt.Sprintf("%s#%s#%d#%s\n", rinfo.ContentMD5, rinfo.SliceMD5, rinfo.ContentLength, path.Base(task.path))
}
_, writeErr = saveFile.Write(converter.ToBytes(outTemplate))
if writeErr != nil {
fmt.Printf("写入文件失败: %s\n", writeErr)
return // 直接返回
}
fmt.Printf("[%d] - [%s] 导出成功\n", task.ID, task.path)
}
if failedList.Len() > 0 {
fmt.Printf("\n以下目录导出失败: \n")
fmt.Printf("%s\n", strings.Repeat("-", 100))
for e := failedList.Front(); e != nil; e = e.Next() {
et := e.Value.(*etask)
fmt.Printf("[%d] %s\n", et.ID, et.path)
}
}
}

View File

@@ -0,0 +1,36 @@
package pcscommand
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs"
)
// RunFixMD5 执行修复md5
func RunFixMD5(pcspaths ...string) {
absPaths, err := matchPathByShellPattern(pcspaths...)
if err != nil {
fmt.Println(err)
return
}
pcs := GetBaiduPCS()
finfoList, err := pcs.FilesDirectoriesBatchMeta(absPaths...)
if err != nil {
fmt.Println(err)
return
}
for k, finfo := range finfoList {
err := pcs.FixMD5ByFileInfo(finfo)
if err == nil {
fmt.Printf("[%d] - [%s] 修复md5成功\n", k, finfo.Path)
continue
}
if err.GetError() == baidupcs.ErrFixMD5Failed {
fmt.Printf("[%d] - [%s] 修复md5失败, 可能是服务器未刷新\n", k, finfo.Path)
continue
}
fmt.Printf("[%d] - [%s] 修复md5失败, 错误信息: %s\n", k, finfo.Path, err)
}
}

View File

@@ -0,0 +1,82 @@
package pcscommand
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/internal/pcsconfig"
"github.com/iikira/BaiduPCS-Go/pcstable"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"os"
"strconv"
)
// RunLocateDownload 执行获取直链
func RunLocateDownload(pcspaths []string, opt *LocateDownloadOption) {
if opt == nil {
opt = &LocateDownloadOption{}
}
absPaths, err := matchPathByShellPattern(pcspaths...)
if err != nil {
fmt.Println(err)
return
}
pcs := GetBaiduPCS()
if opt.FromPan {
fds, err := pcs.FilesDirectoriesBatchMeta(absPaths...)
if err != nil {
fmt.Printf("%s\n", err)
return
}
fidList := make([]int64, 0, len(fds))
for i := range fds {
fidList = append(fidList, fds[i].FsID)
}
list, err := pcs.LocatePanAPIDownload(fidList...)
if err != nil {
fmt.Printf("%s\n", err)
return
}
tb := pcstable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "fs_id", "路径", "链接"})
var (
i int
fidStrList = converter.SliceInt64ToString(fidList)
)
for k := range fidStrList {
for i = range list {
if fidStrList[k] == list[i].FsID {
tb.Append([]string{strconv.Itoa(k), list[i].FsID, fds[k].Path, list[i].Dlink})
list = append(list[:i], list[i+1:]...)
break
}
}
}
tb.Render()
fmt.Printf("\n注意: 以上链接不能直接访问, 需要登录百度帐号才可以下载\n")
return
}
for i, pcspath := range absPaths {
info, err := pcs.LocateDownload(pcspath)
if err != nil {
fmt.Printf("[%d] %s, 路径: %s\n", i, err, pcspath)
continue
}
fmt.Printf("[%d] %s: \n", i, pcspath)
tb := pcstable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "链接"})
for k, u := range info.URLStrings(pcsconfig.Config.EnableHTTPS) {
tb.Append([]string{strconv.Itoa(k), u.String()})
}
tb.Render()
fmt.Println()
}
fmt.Printf("提示: 访问下载链接, 需将下载器的 User-Agent 设置为: %s\n", pcsconfig.Config.PanUA)
}

View File

@@ -0,0 +1,151 @@
package pcscommand
import (
"bytes"
"fmt"
"image/png"
"io/ioutil"
baidulogin "github.com/iikira/Baidu-Login"
"github.com/iikira/BaiduPCS-Go/internal/pcsfunctions/pcscaptcha"
"github.com/iikira/BaiduPCS-Go/pcsliner"
"github.com/iikira/BaiduPCS-Go/requester"
)
// handleVerifyImg 处理验证码, 下载到本地
func handleVerifyImg(imgURL string) (savePath string, err error) {
imgContents, err := requester.Fetch("GET", imgURL, nil, nil)
if err != nil {
return "", fmt.Errorf("获取验证码失败, 错误: %s", err)
}
_, err = png.Decode(bytes.NewReader(imgContents))
if err != nil {
return "", fmt.Errorf("验证码解析错误: %s", err)
}
savePath = pcscaptcha.CaptchaPath()
return savePath, ioutil.WriteFile(savePath, imgContents, 0777)
}
// RunLogin 登录百度帐号
func RunLogin(username, password string) (bduss, ptoken, stoken string, cookies string, err error) {
line := pcsliner.NewLiner()
defer line.Close()
bc := baidulogin.NewBaiduClinet()
if username == "" {
username, err = line.State.Prompt("请输入百度用户名(手机号/邮箱/用户名), 回车键提交 > ")
if err != nil {
return
}
}
if password == "" {
// liner 的 PasswordPrompt 不安全, 拆行之后密码就会显示出来了
fmt.Printf("请输入密码(输入的密码无回显, 确认输入完成, 回车提交即可) > ")
password, err = line.State.PasswordPrompt("")
if err != nil {
return
}
}
var vcode, vcodestr string
// 移除验证码文件
defer func() {
pcscaptcha.RemoveCaptchaPath()
pcscaptcha.RemoveOldCaptchaPath()
}()
for_1:
for i := 0; i < 10; i++ {
lj := bc.BaiduLogin(username, password, vcode, vcodestr)
switch lj.ErrInfo.No {
case "0": // 登录成功, 退出循环
return lj.Data.BDUSS, lj.Data.PToken, lj.Data.SToken, lj.Data.CookieString, nil
case "400023", "400101": // 需要验证手机或邮箱
fmt.Printf("\n需要验证手机或邮箱才能登录\n选择一种验证方式\n")
fmt.Printf("1: 手机: %s\n", lj.Data.Phone)
fmt.Printf("2: 邮箱: %s\n", lj.Data.Email)
fmt.Printf("\n")
var verifyType string
for et := 0; et < 3; et++ {
verifyType, err = line.State.Prompt("请输入验证方式 (1 或 2) > ")
if err != nil {
return
}
switch verifyType {
case "1":
verifyType = "mobile"
case "2":
verifyType = "email"
default:
fmt.Printf("[%d/3] 验证方式不合法\n", et+1)
continue
}
break
}
if verifyType != "mobile" && verifyType != "email" {
err = fmt.Errorf("验证方式不合法")
return
}
msg := bc.SendCodeToUser(verifyType, lj.Data.Token) // 发送验证码
fmt.Printf("消息: %s\n\n", msg)
for et := 0; et < 5; et++ {
vcode, err = line.State.Prompt("请输入接收到的验证码 > ")
if err != nil {
return
}
nlj := bc.VerifyCode(verifyType, lj.Data.Token, vcode, lj.Data.U)
if nlj.ErrInfo.No != "0" {
fmt.Printf("[%d/5] 错误消息: %s\n\n", et+1, nlj.ErrInfo.Msg)
continue
}
// 登录成功
return nlj.Data.BDUSS, nlj.Data.PToken, nlj.Data.SToken, lj.Data.CookieString, nil
}
break for_1
case "500001", "500002": // 验证码
fmt.Printf("\n%s\n", lj.ErrInfo.Msg)
vcodestr = lj.Data.CodeString
if vcodestr == "" {
err = fmt.Errorf("未找到codeString")
return
}
// 图片验证码
var (
verifyImgURL = "https://wappass.baidu.com/cgi-bin/genimage?" + vcodestr
savePath string
)
savePath, err = handleVerifyImg(verifyImgURL)
if err != nil {
fmt.Println(err)
} else {
fmt.Printf("打开以下路径, 以查看验证码\n%s\n\n", savePath)
}
fmt.Printf("或者打开以下的网址, 以查看验证码\n")
fmt.Printf("%s\n\n", verifyImgURL)
vcode, err = line.State.Prompt("请输入验证码 > ")
if err != nil {
return
}
continue
default:
err = fmt.Errorf("错误代码: %s, 消息: %s", lj.ErrInfo.No, lj.ErrInfo.Msg)
return
}
}
return
}

View File

@@ -0,0 +1,144 @@
package pcscommand
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/pcstable"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"github.com/iikira/BaiduPCS-Go/pcsutil/pcstime"
"github.com/olekukonko/tablewriter"
"os"
"strconv"
)
type (
// LsOptions 列目录可选项
LsOptions struct {
Total bool
}
// SearchOptions 搜索可选项
SearchOptions struct {
Total bool
Recurse bool
}
)
const (
opLs int = iota
opSearch
)
// RunLs 执行列目录
func RunLs(pcspath string, lsOptions *LsOptions, orderOptions *baidupcs.OrderOptions) {
err := matchPathByShellPatternOnce(&pcspath)
if err != nil {
fmt.Println(err)
return
}
files, err := GetBaiduPCS().FilesDirectoriesList(pcspath, orderOptions)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("\n当前目录: %s\n----\n", pcspath)
if lsOptions == nil {
lsOptions = &LsOptions{}
}
renderTable(opLs, lsOptions.Total, pcspath, files)
return
}
// RunSearch 执行搜索
func RunSearch(targetPath, keyword string, opt *SearchOptions) {
err := matchPathByShellPatternOnce(&targetPath)
if err != nil {
fmt.Println(err)
return
}
if opt == nil {
opt = &SearchOptions{}
}
files, err := GetBaiduPCS().Search(targetPath, keyword, opt.Recurse)
if err != nil {
fmt.Println(err)
return
}
renderTable(opSearch, opt.Total, targetPath, files)
return
}
func renderTable(op int, isTotal bool, path string, files baidupcs.FileDirectoryList) {
tb := pcstable.NewTable(os.Stdout)
var (
fN, dN int64
showPath string
)
switch op {
case opLs:
showPath = "文件(目录)"
case opSearch:
showPath = "路径"
}
if isTotal {
tb.SetHeader([]string{"#", "fs_id", "app_id", "文件大小", "创建日期", "修改日期", "md5(截图请打码)", showPath})
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
for k, file := range files {
if file.Isdir {
tb.Append([]string{strconv.Itoa(k), strconv.FormatInt(file.FsID, 10), strconv.FormatInt(file.AppID, 10), "-", pcstime.FormatTime(file.Ctime), pcstime.FormatTime(file.Mtime), file.MD5, file.Filename + baidupcs.PathSeparator})
continue
}
var md5 string
if len(file.BlockList) > 1 {
md5 = "(可能不正确)" + file.MD5
} else {
md5 = file.MD5
}
switch op {
case opLs:
tb.Append([]string{strconv.Itoa(k), strconv.FormatInt(file.FsID, 10), strconv.FormatInt(file.AppID, 10), converter.ConvertFileSize(file.Size, 2), pcstime.FormatTime(file.Ctime), pcstime.FormatTime(file.Mtime), md5, file.Filename})
case opSearch:
tb.Append([]string{strconv.Itoa(k), strconv.FormatInt(file.FsID, 10), strconv.FormatInt(file.AppID, 10), converter.ConvertFileSize(file.Size, 2), pcstime.FormatTime(file.Ctime), pcstime.FormatTime(file.Mtime), md5, file.Path})
}
}
fN, dN = files.Count()
tb.Append([]string{"", "", "总: " + converter.ConvertFileSize(files.TotalSize(), 2), "", "", "", fmt.Sprintf("文件总数: %d, 目录总数: %d", fN, dN)})
} else {
tb.SetHeader([]string{"#", "文件大小", "修改日期", showPath})
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
for k, file := range files {
if file.Isdir {
tb.Append([]string{strconv.Itoa(k), "-", pcstime.FormatTime(file.Mtime), file.Filename + baidupcs.PathSeparator})
continue
}
switch op {
case opLs:
tb.Append([]string{strconv.Itoa(k), converter.ConvertFileSize(file.Size, 2), pcstime.FormatTime(file.Mtime), file.Filename})
case opSearch:
tb.Append([]string{strconv.Itoa(k), converter.ConvertFileSize(file.Size, 2), pcstime.FormatTime(file.Mtime), file.Path})
}
}
fN, dN = files.Count()
tb.Append([]string{"", "总: " + converter.ConvertFileSize(files.TotalSize(), 2), "", fmt.Sprintf("文件总数: %d, 目录总数: %d", fN, dN)})
}
tb.Render()
if fN+dN >= 50 {
fmt.Printf("\n当前目录: %s\n", path)
}
fmt.Printf("----\n")
}

View File

@@ -0,0 +1,25 @@
package pcscommand
import (
"fmt"
)
// RunGetMeta 执行 获取文件/目录的元信息
func RunGetMeta(targetPaths ...string) {
targetPaths, err := matchPathByShellPattern(targetPaths...)
if err != nil {
fmt.Println(err)
return
}
for k, targetPath := range targetPaths {
fmt.Printf("[%d] - [%s] --------------\n", k, targetPath)
data, err := GetBaiduPCS().FilesDirectoriesMeta(targetPath)
if err != nil {
fmt.Println(err)
return
}
fmt.Println()
fmt.Println(data)
}
}

View File

@@ -0,0 +1,22 @@
// Package pcscommand 命令包
package pcscommand
import (
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/internal/pcsconfig"
"github.com/iikira/BaiduPCS-Go/pcsverbose"
)
var (
pcsCommandVerbose = pcsverbose.New("PCSCOMMAND")
)
// GetActiveUser 获取当前登录的百度帐号
func GetActiveUser() *pcsconfig.Baidu {
return pcsconfig.Config.ActiveUser()
}
// GetBaiduPCS 从配置读取BaiduPCS
func GetBaiduPCS() *baidupcs.BaiduPCS {
return pcsconfig.Config.ActiveUserBaiduPCS()
}

View File

@@ -0,0 +1,21 @@
package pcscommand
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
)
// RunGetQuota 执行 获取当前用户空间配额信息, 并输出
func RunGetQuota() {
quota, used, err := GetBaiduPCS().QuotaInfo()
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("用户名: %s, 总空间: %s, 已用空间: %s, 比率: %f%%\n",
GetActiveUser().Name,
converter.ConvertFileSize(quota),
converter.ConvertFileSize(used),
100*float64(used)/float64(quota),
)
}

View File

@@ -0,0 +1,86 @@
package pcscommand
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/pcstable"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"github.com/iikira/BaiduPCS-Go/pcsutil/pcstime"
"github.com/olekukonko/tablewriter"
"os"
"strconv"
)
// RunRecycleList 执行列出回收站文件列表
func RunRecycleList(page int) {
if page < 1 {
page = 1
}
pcs := GetBaiduPCS()
fdl, err := pcs.RecycleList(page)
if err != nil {
fmt.Println(err)
return
}
tb := pcstable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "fs_id", "文件大小", "创建日期", "修改日期", "md5(截图请打码)", "剩余时间", "路径"})
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT})
for k, file := range fdl {
if file.Isdir == 1 {
tb.Append([]string{strconv.Itoa(k), strconv.FormatInt(file.FsID, 10), "-", pcstime.FormatTime(file.Ctime), pcstime.FormatTime(file.Mtime), file.MD5, strconv.Itoa(file.LeftTime), file.Path + baidupcs.PathSeparator})
continue
}
tb.Append([]string{strconv.Itoa(k), strconv.FormatInt(file.FsID, 10), converter.ConvertFileSize(file.Size, 2), pcstime.FormatTime(file.Ctime), pcstime.FormatTime(file.Mtime), file.MD5, strconv.Itoa(file.LeftTime), file.Path})
}
tb.Render()
}
// RunRecycleRestore 执行还原回收站文件或目录
func RunRecycleRestore(fidStrList ...string) {
var (
fidList = converter.SliceStringToInt64(fidStrList)
pcs = GetBaiduPCS()
ex, err = pcs.RecycleRestore(fidList...)
)
if err != nil {
fmt.Println(err)
if len(ex) > 0 {
fmt.Printf("\n以下的 fs_id 还原成功, 数量: %d\n", len(ex))
for k := range ex {
fmt.Println(ex[k].FsID)
}
}
return
}
fmt.Printf("还原成功, 数量: %d\n", len(ex))
}
// RunRecycleDelete 执行删除回收站文件或目录
func RunRecycleDelete(fidStrList ...string) {
var (
fidList = converter.SliceStringToInt64(fidStrList)
pcs = GetBaiduPCS()
err = pcs.RecycleDelete(fidList...)
)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("删除成功\n")
}
// RunRecycleClear 清空回收站
func RunRecycleClear() {
pcs := GetBaiduPCS()
sussNum, err := pcs.RecycleClear()
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("清空回收站成功, 数量: %d\n", sussNum)
}

View File

@@ -0,0 +1,49 @@
package pcscommand
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/pcstable"
"os"
"strconv"
)
// RunRemove 执行 批量删除文件/目录
func RunRemove(paths ...string) {
paths, err := matchPathByShellPattern(paths...)
if err != nil {
fmt.Println(err)
return
}
pnt := func() {
tb := pcstable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "文件/目录"})
for k := range paths {
tb.Append([]string{strconv.Itoa(k), paths[k]})
}
tb.Render()
}
err = GetBaiduPCS().Remove(paths...)
if err != nil {
fmt.Println(err)
fmt.Println("操作失败, 以下文件/目录删除失败: ")
pnt()
return
}
fmt.Println("操作成功, 以下文件/目录已删除, 可在网盘文件回收站找回: ")
pnt()
}
// RunMkdir 执行 创建目录
func RunMkdir(path string) {
activeUser := GetActiveUser()
err := GetBaiduPCS().Mkdir(activeUser.PathJoin(path))
if err != nil {
fmt.Printf("创建目录 %s 失败, %s\n", path, err)
return
}
fmt.Println("创建目录成功:", path)
}

View File

@@ -0,0 +1,21 @@
package pcscommand
import (
"io"
"os"
)
var (
// DefaultRunner 默认 Runner
DefaultRunner = Runner{
Output: os.Stdout,
}
)
type (
// Runner 执行器
Runner struct {
Output io.Writer
IsBackground bool
}
)

View File

@@ -0,0 +1,76 @@
package pcscommand
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/pcstable"
"os"
"path"
"strconv"
)
// RunShareSet 执行分享
func RunShareSet(paths []string, option *baidupcs.ShareOption) {
pcspaths, err := matchPathByShellPattern(paths...)
if err != nil {
fmt.Println(err)
return
}
shared, err := GetBaiduPCS().ShareSet(pcspaths, option)
if err != nil {
fmt.Printf("%s失败: %s\n", baidupcs.OperationShareSet, err)
return
}
fmt.Printf("shareID: %d, 链接: %s\n", shared.ShareID, shared.Link)
}
// RunShareCancel 执行取消分享
func RunShareCancel(shareIDs []int64) {
if len(shareIDs) == 0 {
fmt.Printf("%s失败, 没有任何 shareid\n", baidupcs.OperationShareCancel)
return
}
err := GetBaiduPCS().ShareCancel(shareIDs)
if err != nil {
fmt.Printf("%s失败: %s\n", baidupcs.OperationShareCancel, err)
return
}
fmt.Printf("%s成功\n", baidupcs.OperationShareCancel)
}
// RunShareList 执行列出分享列表
func RunShareList(page int) {
if page < 1 {
page = 1
}
pcs := GetBaiduPCS()
records, err := pcs.ShareList(page)
if err != nil {
fmt.Printf("%s失败: %s\n", baidupcs.OperationShareList, err)
return
}
tb := pcstable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "ShareID", "分享链接", "提取密码", "特征目录", "特征路径"})
for k, record := range records {
// 获取Passwd
if record.Public == 0 {
// 私密分享
info, pcsError := pcs.ShareSURLInfo(record.ShareID)
if pcsError != nil {
// 获取错误
fmt.Printf("[%d] 获取分享密码错误: %s\n", k, pcsError)
} else {
record.Passwd = info.Pwd
}
}
tb.Append([]string{strconv.Itoa(k), strconv.FormatInt(record.ShareID, 10), record.Shortlink, record.Passwd, path.Clean(path.Dir(record.TypicalPath)), record.TypicalPath})
}
tb.Render()
}

View File

@@ -0,0 +1,132 @@
package pcscommand
import (
"encoding/base64"
"fmt"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/iikira/BaiduPCS-Go/baidupcs"
)
// RunShareTransfer 执行分享链接转存到网盘
func RunShareTransfer(params []string) {
var link string
var extracode string
if len(params) == 1 {
link = params[0]
if strings.Contains(link, "bdlink=") || !strings.Contains(link, "pan.baidu.com/") {
RunRapidTransfer(link)
return
}
extracode = "none"
} else if len(params) == 2 {
link = params[0]
extracode = params[1]
}
if link[len(link)-1:len(link)] == "/" {
link = link[0 : len(link)-1]
}
featurestrs := strings.Split(link, "/")
featurestr := featurestrs[len(featurestrs)-1]
if strings.Contains(featurestr, "init?") {
featurestr = "1" + strings.Split(featurestr, "=")[1]
}
if len(featurestr) != 23 || featurestr[0:1] != "1" || len(extracode) != 4 {
fmt.Printf("%s失败: %s\n", baidupcs.OperationShareFileSavetoLocal, "链接地址或提取码非法")
return
}
// link = "pan.baidu.com/share/init?surl=" + featurestr[1:]
link = "pan.baidu.com/s/" + featurestr
pcs := GetBaiduPCS()
tokens := pcs.AccessSharePage("https://"+link, true)
if tokens["ErrMsg"] != "0" {
fmt.Printf("%s失败: %s\n", baidupcs.OperationShareFileSavetoLocal, tokens["ErrMsg"])
return
}
var vefiryurl string
featuremap := make(map[string]string)
featuremap["surl"] = featurestr[1:]
featuremap["bdstoken"] = tokens["bdstoken"]
if extracode != "none" {
vefiryurl = pcs.GenerateShareQueryURL("verify", featuremap).String()
res := pcs.PostShareQuery(vefiryurl, map[string]string{
"pwd": extracode,
})
if res["ErrMsg"] != "0" {
fmt.Printf("%s失败: %s\n", baidupcs.OperationShareFileSavetoLocal, res["ErrMsg"])
return
}
}
tokens = pcs.AccessSharePage("https://pan.baidu.com/s/"+featurestr, false)
if tokens["ErrMsg"] != "0" {
fmt.Printf("%s失败: %s\n", baidupcs.OperationShareFileSavetoLocal, tokens["ErrMsg"])
return
}
bdstoken := tokens["bdstoken"]
delete(tokens, "bdstoken")
tokens["desc"] = "1"
tokens["num"] = "100"
tokens["order"] = "time"
tokens["page"] = "1"
tokens["root"] = "1"
tokens["showempty"] = "0"
listurl := pcs.GenerateShareQueryURL("list", tokens).String()
emptymap := make(map[string]string)
metas := pcs.GenerateRequestQuery("GET", listurl, nil)
if metas["ErrMsg"] != "0" {
fmt.Printf("%s失败: %s\n", baidupcs.OperationShareFileSavetoLocal, metas["ErrMsg"])
return
}
emptymap["bdstoken"] = bdstoken
emptymap["from"] = tokens["uk"]
emptymap["shareid"] = tokens["shareid"]
transferurl := pcs.GenerateShareQueryURL("transfer", emptymap).String()
postdata := make(map[string]string)
postdata["fsidlist"] = metas["fs_id"]
postdata["path"] = GetActiveUser().Workdir
resp := pcs.GenerateRequestQuery("POST", transferurl, postdata)
if resp["ErrMsg"] != "0" {
fmt.Printf("%s失败: %s\n", baidupcs.OperationShareFileSavetoLocal, resp["ErrMsg"])
return
}
fmt.Printf("%s成功, 保存了%s到当前目录\n", baidupcs.OperationShareFileSavetoLocal, metas["filename"])
}
// RunRapidTransfer 执行秒传链接解析及保存
func RunRapidTransfer(link string) {
if strings.Contains(link, "bdlink=") || strings.Contains(link, "bdpan://") {
r, _ := regexp.Compile(`(bdlink=|bdpan://)([^\s]+)`)
link1 := r.FindStringSubmatch(link)[2]
decodeBytes, err := base64.StdEncoding.DecodeString(link1)
if err != nil {
fmt.Printf("%s失败: %s\n", baidupcs.OperationRapidLinkSavetoLocal, "秒传链接格式错误")
return
}
link = string(decodeBytes)
}
substrs := strings.Split(link, "#")
if len(substrs) == 4 {
md5 := strings.ToLower(substrs[0])
slicemd5 := strings.ToLower(substrs[1])
length, _ := strconv.ParseInt(substrs[2], 10, 64)
filename := filepath.Join(GetActiveUser().Workdir, substrs[3])
RunRapidUpload(filename, md5, slicemd5, "", length)
return
}
substrs = strings.Split(link, "|")
if len(substrs) == 4 {
md5 := strings.ToLower(substrs[2])
slicemd5 := strings.ToLower(substrs[3])
length, _ := strconv.ParseInt(substrs[1], 10, 64)
filename := filepath.Join(GetActiveUser().Workdir, substrs[0])
RunRapidUpload(filename, md5, slicemd5, "", length)
return
}
fmt.Printf("%s失败: %s\n", baidupcs.OperationRapidLinkSavetoLocal, "秒传链接格式错误")
}

View File

@@ -0,0 +1,59 @@
package pcscommand
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs"
"strings"
)
const (
indentPrefix = "│ "
pathPrefix = "├──"
lastFilePrefix = "└──"
)
func getTree(pcspath string, depth int) {
var (
err error
files baidupcs.FileDirectoryList
)
if depth == 0 {
err := matchPathByShellPatternOnce(&pcspath)
if err != nil {
fmt.Println(err)
return
}
}
files, err = GetBaiduPCS().FilesDirectoriesList(pcspath, baidupcs.DefaultOrderOptions)
if err != nil {
fmt.Println(err)
return
}
var (
prefix = pathPrefix
fN = len(files)
indentPrefixStr = strings.Repeat(indentPrefix, depth)
)
for i, file := range files {
if file.Isdir {
fmt.Printf("%v%v %v/\n", indentPrefixStr, pathPrefix, file.Filename)
getTree(file.Path, depth+1)
continue
}
if i+1 == fN {
prefix = lastFilePrefix
}
fmt.Printf("%v%v %v\n", indentPrefixStr, prefix, file.Filename)
}
return
}
// RunTree 列出树形图
func RunTree(path string) {
getTree(path, 0)
}

View File

@@ -0,0 +1,176 @@
package pcscommand
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/internal/pcsconfig"
"github.com/iikira/BaiduPCS-Go/internal/pcsfunctions/pcsupload"
"github.com/iikira/BaiduPCS-Go/pcstable"
"github.com/iikira/BaiduPCS-Go/pcsutil"
"github.com/iikira/BaiduPCS-Go/pcsutil/checksum"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"github.com/iikira/BaiduPCS-Go/pcsutil/taskframework"
"os"
"path"
"path/filepath"
"strings"
)
const (
// DefaultUploadMaxRetry 默认上传失败最大重试次数
DefaultUploadMaxRetry = 3
)
type (
// UploadOptions 上传可选项
UploadOptions struct {
Parallel int
MaxRetry int
NoRapidUpload bool
NoSplitFile bool // 禁用分片上传
}
)
// RunRapidUpload 执行秒传文件, 前提是知道文件的大小, md5, 前256KB切片的 md5, crc32
func RunRapidUpload(targetPath, contentMD5, sliceMD5, crc32 string, length int64) {
err := matchPathByShellPatternOnce(&targetPath)
if err != nil {
fmt.Printf("警告: %s, 获取网盘路径 %s 错误, %s\n", baidupcs.OperationRapidUpload, targetPath, err)
}
err = GetBaiduPCS().RapidUpload(targetPath, contentMD5, sliceMD5, crc32, length)
if err != nil {
fmt.Printf("%s失败, 消息: %s\n", baidupcs.OperationRapidUpload, err)
return
}
fmt.Printf("%s成功, 保存到网盘路径: %s\n", baidupcs.OperationRapidUpload, targetPath)
return
}
// RunCreateSuperFile 执行分片上传—合并分片文件
func RunCreateSuperFile(targetPath string, blockList ...string) {
err := matchPathByShellPatternOnce(&targetPath)
if err != nil {
fmt.Printf("警告: %s, 获取网盘路径 %s 错误, %s\n", baidupcs.OperationUploadCreateSuperFile, targetPath, err)
}
err = GetBaiduPCS().UploadCreateSuperFile(true, targetPath, blockList...)
if err != nil {
fmt.Printf("%s失败, 消息: %s\n", baidupcs.OperationUploadCreateSuperFile, err)
return
}
fmt.Printf("%s成功, 保存到网盘路径: %s\n", baidupcs.OperationUploadCreateSuperFile, targetPath)
return
}
// RunUpload 执行文件上传
func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
if opt == nil {
opt = &UploadOptions{}
}
// 检测opt
if opt.Parallel <= 0 {
opt.Parallel = pcsconfig.Config.MaxUploadParallel
}
if opt.MaxRetry < 0 {
opt.MaxRetry = DefaultUploadMaxRetry
}
err := matchPathByShellPatternOnce(&savePath)
if err != nil {
fmt.Printf("警告: 上传文件, 获取网盘路径 %s 错误, %s\n", savePath, err)
}
switch len(localPaths) {
case 0:
fmt.Printf("本地路径为空\n")
return
}
// 打开上传状态
uploadDatabase, err := pcsupload.NewUploadingDatabase()
if err != nil {
fmt.Printf("打开上传未完成数据库错误: %s\n", err)
return
}
defer uploadDatabase.Close()
var (
pcs = GetBaiduPCS()
// 使用 task framework
executor = &taskframework.TaskExecutor{
IsFailedDeque: true, // 失败统计
}
subSavePath string
// 统计
statistic = &pcsupload.UploadStatistic{}
)
statistic.StartTimer() // 开始计时
for k := range localPaths {
walkedFiles, err := pcsutil.WalkDir(localPaths[k], "")
if err != nil {
fmt.Printf("警告: 遍历错误: %s\n", err)
continue
}
for k3 := range walkedFiles {
var localPathDir string
// 针对 windows 的目录处理
if os.PathSeparator == '\\' {
walkedFiles[k3] = pcsutil.ConvertToUnixPathSeparator(walkedFiles[k3])
localPathDir = pcsutil.ConvertToUnixPathSeparator(filepath.Dir(localPaths[k]))
} else {
localPathDir = filepath.Dir(localPaths[k])
}
// 避免去除文件名开头的"."
if localPathDir == "." {
localPathDir = ""
}
subSavePath = strings.TrimPrefix(walkedFiles[k3], localPathDir)
info := executor.Append(&pcsupload.UploadTaskUnit{
LocalFileChecksum: checksum.NewLocalFileChecksum(walkedFiles[k3], int(baidupcs.SliceMD5Size)),
SavePath: path.Clean(savePath + baidupcs.PathSeparator + subSavePath),
PCS: pcs,
UploadingDatabase: uploadDatabase,
Parallel: opt.Parallel,
NoRapidUpload: opt.NoRapidUpload,
NoSplitFile: opt.NoSplitFile,
UploadStatistic: statistic,
}, opt.MaxRetry)
fmt.Printf("[%s] 加入上传队列: %s\n", info.Id(), walkedFiles[k3])
}
}
// 没有添加任何任务
if executor.Count() == 0 {
fmt.Printf("未检测到上传的文件.\n")
return
}
// 执行上传任务
executor.Execute()
fmt.Printf("\n")
fmt.Printf("上传结束, 时间: %s, 总大小: %s\n", statistic.Elapsed()/1e6*1e6, converter.ConvertFileSize(statistic.TotalSize()))
// 输出上传失败的文件列表
failedList := executor.FailedDeque()
if failedList.Size() != 0 {
fmt.Printf("以下文件上传失败: \n")
tb := pcstable.NewTable(os.Stdout)
for e := failedList.Shift(); e != nil; e = failedList.Shift() {
item := e.(*taskframework.TaskInfoItem)
tb.Append([]string{item.Info.Id(), item.Unit.(*pcsupload.UploadTaskUnit).LocalFileChecksum.Path})
}
tb.Render()
}
}

View File

@@ -0,0 +1,64 @@
package pcscommand
import (
"errors"
"fmt"
)
var (
// ErrShellPatternMultiRes 多条通配符匹配结果
ErrShellPatternMultiRes = errors.New("多条通配符匹配结果")
// ErrShellPatternNoHit 未匹配到路径
ErrShellPatternNoHit = errors.New("未匹配到路径, 请检测通配符")
)
// ListTask 队列状态 (基类)
type ListTask struct {
ID int // 任务id
MaxRetry int // 最大重试次数
retry int // 任务失败的重试次数
}
// RunTestShellPattern 执行测试通配符
func RunTestShellPattern(pattern string) {
pcs := GetBaiduPCS()
paths, err := pcs.MatchPathByShellPattern(GetActiveUser().PathJoin(pattern))
if err != nil {
fmt.Println(err)
return
}
for k := range paths {
fmt.Printf("%s\n", paths[k])
}
return
}
func matchPathByShellPatternOnce(pattern *string) error {
paths, err := GetBaiduPCS().MatchPathByShellPattern(GetActiveUser().PathJoin(*pattern))
if err != nil {
return err
}
switch len(paths) {
case 0:
return ErrShellPatternNoHit
case 1:
*pattern = paths[0]
default:
return ErrShellPatternMultiRes
}
return nil
}
func matchPathByShellPattern(patterns ...string) (pcspaths []string, err error) {
acUser, pcs := GetActiveUser(), GetBaiduPCS()
for k := range patterns {
ps, err := pcs.MatchPathByShellPattern(acUser.PathJoin(patterns[k]))
if err != nil {
return nil, err
}
pcspaths = append(pcspaths, ps...)
}
return pcspaths, nil
}

119
internal/pcsconfig/baidu.go Normal file
View File

@@ -0,0 +1,119 @@
package pcsconfig
import (
"errors"
"fmt"
"path"
"path/filepath"
"strconv"
"strings"
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/pcstable"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"github.com/iikira/baidu-tools/tieba"
"github.com/olekukonko/tablewriter"
)
var (
//ErrNoSuchBaiduUser 未登录任何百度帐号
ErrNoSuchBaiduUser = errors.New("no such baidu user")
//ErrBaiduUserNotFound 未找到百度帐号
ErrBaiduUserNotFound = errors.New("baidu user not found")
)
//BaiduBase Baidu基
type BaiduBase struct {
UID uint64 `json:"uid"` // 百度ID对应的uid
Name string `json:"name"` // 真实ID
}
// Baidu 百度帐号对象
type Baidu struct {
BaiduBase
Sex string `json:"sex"` // 性别
Age float64 `json:"age"` // 帐号年龄
BDUSS string `json:"bduss"`
PTOKEN string `json:"ptoken"`
STOKEN string `json:"stoken"`
COOKIES string `json:"cookies"`
Workdir string `json:"workdir"` // 工作目录
}
// BaiduPCS 初始化*baidupcs.BaiduPCS
func (baidu *Baidu) BaiduPCS() *baidupcs.BaiduPCS {
pcs := baidupcs.NewPCS(Config.AppID, baidu.BDUSS)
pcs.SetStoken(baidu.STOKEN)
if strings.Contains(baidu.COOKIES, "BAIDUID=") {
// fmt.Println("已加载完整Cookies可以使用转存功能")
pcs = baidupcs.NewPCSWithCookieStr(Config.AppID, baidu.COOKIES)
} else if baidu.BDUSS != "" {
fmt.Println("注:分享链接转存功能无法使用,可使用-cookies参数重新登录以启用")
}
pcs.SetHTTPS(Config.EnableHTTPS)
pcs.SetPCSUserAgent(Config.PCSUA)
pcs.SetPanUserAgent(Config.PanUA)
pcs.SetUID(baidu.UID)
return pcs
}
// GetSavePath 根据提供的网盘文件路径 pcspath, 返回本地储存路径,
// 返回绝对路径, 获取绝对路径出错时才返回相对路径...
func (baidu *Baidu) GetSavePath(pcspath string) string {
dirStr := filepath.Join(Config.SaveDir, fmt.Sprintf("%d_%s", baidu.UID, converter.TrimPathInvalidChars(baidu.Name)), pcspath)
dir, err := filepath.Abs(dirStr)
if err != nil {
dir = filepath.Clean(dirStr)
}
return dir
}
// PathJoin 合并工作目录和相对路径p, 若p为绝对路径则忽略
func (baidu *Baidu) PathJoin(p string) string {
if path.IsAbs(p) {
return p
}
return path.Join(baidu.Workdir, p)
}
// BaiduUserList 百度帐号列表
type BaiduUserList []*Baidu
// NewUserInfoByBDUSS 检测BDUSS有效性, 同时获取百度详细信息 (无法获取 ptoken 和 stoken)
func NewUserInfoByBDUSS(bduss string) (b *Baidu, err error) {
t, err := tieba.NewUserInfoByBDUSS(bduss)
if err != nil {
return nil, err
}
b = &Baidu{
BaiduBase: BaiduBase{
UID: t.Baidu.UID,
Name: t.Baidu.Name,
},
Sex: t.Baidu.Sex,
Age: t.Baidu.Age,
BDUSS: bduss,
Workdir: "/",
}
return b, nil
}
// String 格式输出百度帐号列表
func (bl *BaiduUserList) String() string {
builder := &strings.Builder{}
tb := pcstable.NewTable(builder)
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER})
tb.SetHeader([]string{"#", "uid", "用户名", "性别", "age"})
for k, baiduInfo := range *bl {
tb.Append([]string{strconv.Itoa(k), strconv.FormatUint(baiduInfo.UID, 10), baiduInfo.Name, baiduInfo.Sex, fmt.Sprint(baiduInfo.Age)})
}
tb.Render()
return builder.String()
}

View File

@@ -0,0 +1,18 @@
package pcsconfig
import (
"errors"
)
var (
//ErrNotLogin 未登录帐号错误
ErrNotLogin = errors.New("baidu user not login")
//ErrConfigFilePathNotSet 未设置配置文件
ErrConfigFilePathNotSet = errors.New("config file not set")
//ErrConfigFileNotExist 未设置Config, 未初始化
ErrConfigFileNotExist = errors.New("config file not exist")
//ErrConfigFileNoPermission Config文件无权限访问
ErrConfigFileNoPermission = errors.New("config file permission denied")
//ErrConfigContentsParseError 解析Config数据错误
ErrConfigContentsParseError = errors.New("config contents parse error")
)

View File

@@ -0,0 +1,85 @@
package pcsconfig
import (
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/pcstable"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"github.com/iikira/BaiduPCS-Go/requester"
"github.com/olekukonko/tablewriter"
"os"
"strconv"
)
// ActiveUser 获取当前登录的用户
func (c *PCSConfig) ActiveUser() *Baidu {
if c.activeUser == nil {
return &Baidu{}
}
return c.activeUser
}
// ActiveUserBaiduPCS 获取当前登录的用户的baidupcs.BaiduPCS
func (c *PCSConfig) ActiveUserBaiduPCS() *baidupcs.BaiduPCS {
if c.pcs == nil {
c.pcs = c.ActiveUser().BaiduPCS()
}
return c.pcs
}
func (c *PCSConfig) httpClientWithUA(ua string) *requester.HTTPClient {
client := requester.NewHTTPClient()
client.SetHTTPSecure(c.EnableHTTPS)
client.SetUserAgent(ua)
return client
}
// HTTPClient 返回设置好的 HTTPClient
func (c *PCSConfig) HTTPClient() *requester.HTTPClient {
return c.httpClientWithUA(c.UserAgent)
}
// PCSHTTPClient 返回设置好的 PCS HTTPClient
func (c *PCSConfig) PCSHTTPClient() *requester.HTTPClient {
return c.httpClientWithUA(c.PCSUA)
}
// PanHTTPClient 返回设置好的 Pan HTTPClient
func (c *PCSConfig) PanHTTPClient() *requester.HTTPClient {
return c.httpClientWithUA(c.PanUA)
}
// NumLogins 获取登录的用户数量
func (c *PCSConfig) NumLogins() int {
return len(c.BaiduUserList)
}
// AverageParallel 返回平均的下载最大并发量
func (c *PCSConfig) AverageParallel() int {
return AverageParallel(c.MaxParallel, c.MaxDownloadLoad)
}
// PrintTable 输出表格
func (c *PCSConfig) PrintTable() {
tb := pcstable.NewTable(os.Stdout)
tb.SetHeader([]string{"名称", "值", "建议值", "描述"})
tb.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
tb.AppendBulk([][]string{
[]string{"appid", fmt.Sprint(c.AppID), "", "百度 PCS 应用ID"},
[]string{"cache_size", converter.ConvertFileSize(int64(c.CacheSize), 2), "1KB ~ 256KB", "下载缓存, 如果硬盘占用高或下载速度慢, 请尝试调大此值"},
[]string{"max_parallel", strconv.Itoa(c.MaxParallel), "1 ~ 64", "下载最大并发量"},
[]string{"max_upload_parallel", strconv.Itoa(c.MaxUploadParallel), "1 ~ 100", "上传最大并发量"},
[]string{"max_download_load", strconv.Itoa(c.MaxDownloadLoad), "1 ~ 5", "同时进行下载文件的最大数量"},
[]string{"max_download_rate", showMaxRate(c.MaxDownloadRate), "", "限制最大下载速度, 0代表不限制"},
[]string{"max_upload_rate", showMaxRate(c.MaxUploadRate), "", "限制最大上传速度, 0代表不限制"},
[]string{"savedir", c.SaveDir, "", "下载文件的储存目录"},
[]string{"enable_https", fmt.Sprint(c.EnableHTTPS), "true", "启用 https"},
[]string{"user_agent", c.UserAgent, requester.DefaultUserAgent, "浏览器标识"},
[]string{"pcs_ua", c.PCSUA, "", "PCS 浏览器标识"},
[]string{"pan_ua", c.PanUA, baidupcs.NetdiskUA, "Pan 浏览器标识"},
[]string{"proxy", c.Proxy, "", "设置代理, 支持 http/socks5 代理"},
[]string{"local_addrs", c.LocalAddrs, "", "设置本地网卡地址, 多个地址用逗号隔开"},
})
tb.Render()
}

View File

@@ -0,0 +1,222 @@
package pcsconfig
import (
"regexp"
"strings"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"github.com/iikira/BaiduPCS-Go/requester"
)
const (
opDelete = "delete"
opSwitch = "switch"
opGet = "get"
)
func (c *PCSConfig) manipUser(op string, baiduBase *BaiduBase) (*Baidu, error) {
// empty baiduBase
if baiduBase == nil || (baiduBase.UID == 0 && baiduBase.Name == "") {
switch op {
case opGet:
return &Baidu{}, nil
default:
return nil, ErrBaiduUserNotFound
}
}
if len(c.BaiduUserList) == 0 {
return nil, ErrNoSuchBaiduUser
}
for k, user := range c.BaiduUserList {
if user == nil {
continue
}
switch {
case baiduBase.UID != 0 && baiduBase.Name != "":
// 不区分大小写
if user.UID == baiduBase.UID && strings.EqualFold(user.Name, baiduBase.Name) {
goto handle
}
continue
case baiduBase.UID == 0 && baiduBase.Name != "":
// 不区分大小写
if strings.EqualFold(user.Name, baiduBase.Name) {
goto handle
}
continue
case baiduBase.UID != 0 && baiduBase.Name == "":
if user.UID == baiduBase.UID {
goto handle
}
continue
default:
continue
}
// unreachable zone
handle:
switch op {
case opSwitch:
c.setupNewUser(user)
case opDelete:
c.BaiduUserList = append(c.BaiduUserList[:k], c.BaiduUserList[k+1:]...)
// 修改 正在使用的 百度帐号
// 如果要删除的帐号为当前登录的帐号, 则设置当前登录帐号为列表中第一个帐号
if c.BaiduActiveUID == user.UID {
if len(c.BaiduUserList) != 0 {
c.setupNewUser(c.BaiduUserList[0])
} else {
c.BaiduActiveUID = 0
}
}
case opGet:
// do nothing
default:
// do nothing
}
return user, nil
}
return nil, ErrBaiduUserNotFound
}
//setupNewUser 从已有用户中, 设置新的当前登录用户
func (c *PCSConfig) setupNewUser(user *Baidu) {
if user == nil {
return
}
c.BaiduActiveUID = user.UID
c.activeUser = user
c.pcs = user.BaiduPCS()
}
// SwitchUser 切换用户, 返回切换成功的用户
func (c *PCSConfig) SwitchUser(baiduBase *BaiduBase) (*Baidu, error) {
return c.manipUser(opSwitch, baiduBase)
}
// DeleteUser 删除用户, 返回删除成功的用户
func (c *PCSConfig) DeleteUser(baiduBase *BaiduBase) (*Baidu, error) {
return c.manipUser(opDelete, baiduBase)
}
// GetBaiduUser 获取百度用户信息
func (c *PCSConfig) GetBaiduUser(baidubase *BaiduBase) (*Baidu, error) {
return c.manipUser(opGet, baidubase)
}
// CheckBaiduUserExist 检查百度用户是否存在于已登录列表
func (c *PCSConfig) CheckBaiduUserExist(baidubase *BaiduBase) bool {
_, err := c.manipUser("", baidubase)
return err == nil
}
// SetupUserByBDUSS 设置百度 bduss, ptoken, stoken, cookies 并保存
func (c *PCSConfig) SetupUserByBDUSS(bduss, ptoken, stoken, cookies string) (baidu *Baidu, err error) {
if cookies != "" {
re, _ := regexp.Compile(`BDUSS=(.+?);`)
sub := re.FindSubmatch([]byte(cookies))
bduss = string(sub[1])
}
b, err := NewUserInfoByBDUSS(bduss)
if err != nil {
return nil, err
}
c.DeleteUser(&BaiduBase{
UID: b.UID,
}) // 删除旧的信息
b.PTOKEN = ptoken
b.STOKEN = stoken
b.COOKIES = cookies
c.BaiduUserList = append(c.BaiduUserList, b)
// 自动切换用户
c.setupNewUser(b)
return b, nil
}
// SetAppID 设置app_id
func (c *PCSConfig) SetAppID(appID int) {
c.AppID = appID
if c.pcs != nil {
c.pcs.SetAPPID(appID)
}
}
// SetCacheSizeByStr 设置cache_size
func (c *PCSConfig) SetCacheSizeByStr(sizeStr string) error {
size, err := converter.ParseFileSizeStr(sizeStr)
if err != nil {
return err
}
c.CacheSize = int(size)
return nil
}
// SetMaxDownloadRateByStr 设置 max_download_rate
func (c *PCSConfig) SetMaxDownloadRateByStr(sizeStr string) error {
size, err := converter.ParseFileSizeStr(stripPerSecond(sizeStr))
if err != nil {
return err
}
c.MaxDownloadRate = size
return nil
}
// SetMaxUploadRateByStr 设置 max_upload_rate
func (c *PCSConfig) SetMaxUploadRateByStr(sizeStr string) error {
size, err := converter.ParseFileSizeStr(stripPerSecond(sizeStr))
if err != nil {
return err
}
c.MaxUploadRate = size
return nil
}
// SetUserAgent 设置User-Agent
func (c *PCSConfig) SetUserAgent(userAgent string) {
c.UserAgent = userAgent
requester.UserAgent = userAgent
}
// SetPCSUA 设置 PCS User-Agent
func (c *PCSConfig) SetPCSUA(pcsUA string) {
c.PCSUA = pcsUA
if c.pcs != nil {
c.pcs.SetPCSUserAgent(pcsUA)
}
}
// SetPanUA 设置 Pan User-Agent
func (c *PCSConfig) SetPanUA(panUA string) {
c.PanUA = panUA
if c.pcs != nil {
c.pcs.SetPanUserAgent(panUA)
}
}
// SetEnableHTTPS 设置是否启用https
func (c *PCSConfig) SetEnableHTTPS(https bool) {
c.EnableHTTPS = https
if c.pcs != nil {
c.pcs.SetHTTPS(https)
}
}
// SetProxy 设置代理
func (c *PCSConfig) SetProxy(proxy string) {
c.Proxy = proxy
requester.SetGlobalProxy(proxy)
}
// SetLocalAddrs 设置localAddrs
func (c *PCSConfig) SetLocalAddrs(localAddrs string) {
c.LocalAddrs = localAddrs
requester.SetLocalTCPAddrList(strings.Split(localAddrs, ",")...)
}

View File

@@ -0,0 +1,308 @@
// Package pcsconfig 配置包
package pcsconfig
import (
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/pcsutil"
"github.com/iikira/BaiduPCS-Go/pcsutil/jsonhelper"
"github.com/iikira/BaiduPCS-Go/pcsverbose"
"github.com/iikira/BaiduPCS-Go/requester"
"github.com/json-iterator/go"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
)
const (
// EnvConfigDir 配置路径环境变量
EnvConfigDir = "BAIDUPCS_GO_CONFIG_DIR"
// ConfigName 配置文件名
ConfigName = "pcs_config.json"
)
var (
pcsConfigVerbose = pcsverbose.New("PCSCONFIG")
configFilePath = filepath.Join(GetConfigDir(), ConfigName)
// Config 配置信息, 由外部调用
Config = NewConfig(configFilePath)
)
// PCSConfig 配置详情
type PCSConfig struct {
BaiduActiveUID uint64 `json:"baidu_active_uid"`
BaiduUserList BaiduUserList `json:"baidu_user_list"`
AppID int `json:"appid"` // appid
CacheSize int `json:"cache_size"` // 下载缓存
MaxParallel int `json:"max_parallel"` // 最大下载并发量
MaxUploadParallel int `json:"max_upload_parallel"` // 最大上传并发量
MaxDownloadLoad int `json:"max_download_load"` // 同时进行下载文件的最大数量
MaxDownloadRate int64 `json:"max_download_rate"` // 限制最大下载速度
MaxUploadRate int64 `json:"max_upload_rate"` // 限制最大上传速度
UserAgent string `json:"user_agent"` // 浏览器标识
PCSUA string `json:"pcs_ua"` // PCS浏览器标识
PanUA string `json:"pan_ua"` // PAN浏览器标识
SaveDir string `json:"savedir"` // 下载储存路径
EnableHTTPS bool `json:"enable_https"` // 启用https
Proxy string `json:"proxy"` // 代理
LocalAddrs string `json:"local_addrs"` // 本地网卡地址
configFilePath string
configFile *os.File
fileMu sync.Mutex
activeUser *Baidu
pcs *baidupcs.BaiduPCS
}
// NewConfig 返回 PCSConfig 指针对象
func NewConfig(configFilePath string) *PCSConfig {
c := &PCSConfig{
configFilePath: configFilePath,
}
return c
}
// Init 初始化配置
func (c *PCSConfig) Init() error {
return c.init()
}
// Reload 从文件重载配置
func (c *PCSConfig) Reload() error {
return c.init()
}
// Close 关闭配置文件
func (c *PCSConfig) Close() error {
if c.configFile != nil {
err := c.configFile.Close()
c.configFile = nil
return err
}
return nil
}
// Save 保存配置信息到配置文件
func (c *PCSConfig) Save() error {
// 检测配置项是否合法, 不合法则自动修复
c.fix()
err := c.lazyOpenConfigFile()
if err != nil {
return err
}
c.fileMu.Lock()
defer c.fileMu.Unlock()
data, err := jsoniter.MarshalIndent(c, "", " ")
if err != nil {
// json数据生成失败
panic(err)
}
// 减掉多余的部分
err = c.configFile.Truncate(int64(len(data)))
if err != nil {
return err
}
_, err = c.configFile.Seek(0, os.SEEK_SET)
if err != nil {
return err
}
_, err = c.configFile.Write(data)
if err != nil {
return err
}
return nil
}
func (c *PCSConfig) init() error {
if c.configFilePath == "" {
return ErrConfigFileNotExist
}
c.initDefaultConfig()
err := c.loadConfigFromFile()
if err != nil {
return err
}
// 载入配置
// 如果 activeUser 已初始化, 则跳过
if c.activeUser != nil && c.activeUser.UID == c.BaiduActiveUID {
return nil
}
c.activeUser, err = c.GetBaiduUser(&BaiduBase{
UID: c.BaiduActiveUID,
})
if err != nil {
return err
}
c.pcs = c.activeUser.BaiduPCS()
// 设置全局User-Agent
requester.UserAgent = c.UserAgent
// 设置全局代理
requester.SetGlobalProxy(c.Proxy)
// 设置本地网卡地址
requester.SetLocalTCPAddrList(strings.Split(c.LocalAddrs, ",")...)
return nil
}
// lazyOpenConfigFile 打开配置文件
func (c *PCSConfig) lazyOpenConfigFile() (err error) {
if c.configFile != nil {
return nil
}
c.fileMu.Lock()
os.MkdirAll(filepath.Dir(c.configFilePath), 0700)
c.configFile, err = os.OpenFile(c.configFilePath, os.O_CREATE|os.O_RDWR, 0600)
c.fileMu.Unlock()
if err != nil {
if os.IsPermission(err) {
return ErrConfigFileNoPermission
}
if os.IsExist(err) {
return ErrConfigFileNotExist
}
return err
}
return nil
}
// loadConfigFromFile 载入配置
func (c *PCSConfig) loadConfigFromFile() (err error) {
err = c.lazyOpenConfigFile()
if err != nil {
return err
}
// 未初始化
info, err := c.configFile.Stat()
if err != nil {
return err
}
if info.Size() == 0 {
err = c.Save()
return err
}
c.fileMu.Lock()
defer c.fileMu.Unlock()
_, err = c.configFile.Seek(0, os.SEEK_SET)
if err != nil {
return err
}
err = jsonhelper.UnmarshalData(c.configFile, c)
if err != nil {
return ErrConfigContentsParseError
}
return nil
}
func (c *PCSConfig) initDefaultConfig() {
c.AppID = 266719
c.CacheSize = 65536
c.MaxParallel = 1
c.MaxUploadParallel = 8
c.MaxDownloadLoad = 1
c.UserAgent = requester.UserAgent
c.PCSUA = ""
c.PanUA = baidupcs.NetdiskUA
c.EnableHTTPS = true
// 设置默认的下载路径
switch runtime.GOOS {
case "windows":
c.SaveDir = pcsutil.ExecutablePathJoin("Downloads")
case "android":
// TODO: 获取完整的的下载路径
c.SaveDir = "/sdcard/Download"
default:
dataPath, ok := os.LookupEnv("HOME")
if !ok {
pcsConfigVerbose.Warn("Environment HOME not set")
c.SaveDir = pcsutil.ExecutablePathJoin("Downloads")
} else {
c.SaveDir = filepath.Join(dataPath, "Downloads")
}
}
}
// GetConfigDir 获取配置路径
func GetConfigDir() string {
// 从环境变量读取
configDir, ok := os.LookupEnv(EnvConfigDir)
if ok {
if filepath.IsAbs(configDir) {
return configDir
}
// 如果不是绝对路径, 从程序目录寻找
return pcsutil.ExecutablePathJoin(configDir)
}
// 使用旧版
// 如果旧版的配置文件存在, 则使用旧版
oldConfigDir := pcsutil.ExecutablePath()
_, err := os.Stat(filepath.Join(oldConfigDir, ConfigName))
if err == nil {
return oldConfigDir
}
switch runtime.GOOS {
case "windows":
dataPath, ok := os.LookupEnv("APPDATA")
if !ok {
pcsConfigVerbose.Warn("Environment APPDATA not set")
return oldConfigDir
}
return filepath.Join(dataPath, "BaiduPCS-Go")
default:
dataPath, ok := os.LookupEnv("HOME")
if !ok {
pcsConfigVerbose.Warn("Environment HOME not set")
return oldConfigDir
}
configDir = filepath.Join(dataPath, ".config", "BaiduPCS-Go")
// 检测是否可写
err = os.MkdirAll(configDir, 0700)
if err != nil {
pcsConfigVerbose.Warnf("check config dir error: %s\n", err)
return oldConfigDir
}
return configDir
}
}
func (c *PCSConfig) fix() {
if c.CacheSize < 1024 {
c.CacheSize = 1024
}
if c.MaxParallel < 1 {
c.MaxParallel = 1
}
if c.MaxUploadParallel < 1 {
c.MaxUploadParallel = 1
}
if c.MaxDownloadLoad < 1 {
c.MaxDownloadLoad = 1
}
}

View File

@@ -0,0 +1,34 @@
package pcsconfig
import (
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"strings"
)
// AverageParallel 返回平均的下载最大并发量
func AverageParallel(parallel, downloadLoad int) int {
if downloadLoad < 1 {
return 1
}
p := parallel / downloadLoad
if p < 1 {
return 1
}
return p
}
func stripPerSecond(sizeStr string) string {
i := strings.LastIndex(sizeStr, "/")
if i < 0 {
return sizeStr
}
return sizeStr[:i]
}
func showMaxRate(size int64) string {
if size <= 0 {
return "不限制"
}
return converter.ConvertFileSize(size, 2) + "/s"
}

View File

@@ -0,0 +1,11 @@
package pcsfunctions
import "time"
// RetryWait 失败重试等待事件
func RetryWait(retry int) time.Duration {
if retry < 3 {
return 2 * time.Duration(retry) * time.Second
}
return 6 * time.Second
}

View File

@@ -0,0 +1,11 @@
package pcscaptcha
import (
"os"
"path/filepath"
)
// CaptchaPath 返回验证码存放路径
func CaptchaPath() string {
return filepath.Join(os.TempDir(), CaptchaName)
}

View File

@@ -0,0 +1,24 @@
// Package pcscaptcha 验证码处理包
// TODO: 直接打开验证码
package pcscaptcha
import (
"github.com/iikira/BaiduPCS-Go/internal/pcsconfig"
"os"
"path/filepath"
)
const (
// CaptchaName 验证码文件名称
CaptchaName = "captcha.png"
)
// RemoveOldCaptchaPath 移除旧的验证码路径
func RemoveOldCaptchaPath() error {
return os.Remove(filepath.Join(pcsconfig.GetConfigDir(), CaptchaName))
}
// RemoveCaptchaPath 移除验证码路径
func RemoveCaptchaPath() error {
return os.Remove(CaptchaPath())
}

View File

@@ -0,0 +1,21 @@
package pcsdownload
import (
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/internal/pcsconfig"
"net/url"
)
func GetLocateDownloadLinks(pcs *baidupcs.BaiduPCS, pcspath string) (dlinks []*url.URL, err error) {
dInfo, pcsError := pcs.LocateDownload(pcspath)
if pcsError != nil {
return nil, pcsError
}
us := dInfo.URLStrings(pcsconfig.Config.EnableHTTPS)
if len(us) == 0 {
return nil, ErrDlinkNotFound
}
return us, nil
}

View File

@@ -0,0 +1,11 @@
package pcsdownload
import (
"github.com/iikira/BaiduPCS-Go/internal/pcsfunctions"
)
type (
DownloadStatistic struct {
pcsfunctions.Statistic
}
)

View File

@@ -0,0 +1,499 @@
package pcsdownload
import (
"errors"
"fmt"
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"github.com/iikira/BaiduPCS-Go/internal/pcsconfig"
"github.com/iikira/BaiduPCS-Go/internal/pcsfunctions"
"github.com/iikira/BaiduPCS-Go/pcstable"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"github.com/iikira/BaiduPCS-Go/pcsutil/taskframework"
"github.com/iikira/BaiduPCS-Go/pcsverbose"
"github.com/iikira/BaiduPCS-Go/requester"
"github.com/iikira/BaiduPCS-Go/requester/downloader"
"github.com/iikira/BaiduPCS-Go/requester/transfer"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
type (
// DownloadMode 下载模式
DownloadMode int
// DownloadTaskUnit 下载的任务单元
DownloadTaskUnit struct {
taskInfo *taskframework.TaskInfo // 任务信息
Cfg *downloader.Config
PCS *baidupcs.BaiduPCS
ParentTaskExecutor *taskframework.TaskExecutor
DownloadStatistic *DownloadStatistic // 下载统计
// 可选项
VerbosePrinter *pcsverbose.PCSVerbose
PrintFormat string
IsPrintStatus bool // 是否输出各个下载线程的详细信息
IsExecutedPermission bool // 下载成功后是否加上执行权限
IsOverwrite bool // 是否覆盖已存在的文件
NoCheck bool // 不校验文件
DownloadMode DownloadMode // 下载模式
PcsPath string // 要下载的网盘文件路径
SavePath string // 保存的路径
fileInfo *baidupcs.FileDirectory // 文件或目录详情
}
)
const (
// DefaultPrintFormat 默认的下载进度输出格式
DefaultPrintFormat = "\r[%s] ↓ %s/%s %s/s in %s, left %s ............"
//DownloadSuffix 文件下载后缀
DownloadSuffix = ".BaiduPCS-Go-downloading"
//StrDownloadInitError 初始化下载发生错误
StrDownloadInitError = "初始化下载发生错误"
// StrDownloadFailed 下载文件失败
StrDownloadFailed = "下载文件失败"
// StrDownloadGetDlinkFailed 获取下载链接失败
StrDownloadGetDlinkFailed = "获取下载链接失败"
// StrDownloadChecksumFailed 检测文件有效性失败
StrDownloadChecksumFailed = "检测文件有效性失败"
// DefaultDownloadMaxRetry 默认下载失败最大重试次数
DefaultDownloadMaxRetry = 3
)
const (
DownloadModeLocate DownloadMode = iota
DownloadModePCS
DownloadModeStreaming
)
func (dtu *DownloadTaskUnit) SetTaskInfo(info *taskframework.TaskInfo) {
dtu.taskInfo = info
}
func (dtu *DownloadTaskUnit) verboseInfof(format string, a ...interface{}) {
if dtu.VerbosePrinter != nil {
dtu.VerbosePrinter.Infof(format, a...)
}
}
// download 执行下载
func (dtu *DownloadTaskUnit) download(downloadURL string, client *requester.HTTPClient) (err error) {
var (
writer downloader.Writer
file *os.File
)
if !dtu.Cfg.IsTest {
// 非测试下载
dtu.Cfg.InstanceStatePath = dtu.SavePath + DownloadSuffix
// 创建下载的目录
// 获取SavePath所在的目录
dir := filepath.Dir(dtu.SavePath)
fileInfo, err := os.Stat(dir)
if err != nil {
// 目录不存在, 创建
err = os.MkdirAll(dir, 0777)
if err != nil {
return err
}
} else if !fileInfo.IsDir() {
// SavePath所在的目录不是目录
return fmt.Errorf("%s, path %s: not a directory", StrDownloadInitError, dir)
}
// 打开文件
writer, file, err = downloader.NewDownloaderWriterByFilename(dtu.SavePath, os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
return fmt.Errorf("%s, %s", StrDownloadInitError, err)
}
defer file.Close()
}
der := downloader.NewDownloader(downloadURL, writer, dtu.Cfg)
der.SetClient(client)
der.SetDURLCheckFunc(BaiduPCSURLCheckFunc)
der.SetStatusCodeBodyCheckFunc(func(respBody io.Reader) error {
// 返回的错误可能是pcs的json
// 解析错误
return pcserror.DecodePCSJSONError(baidupcs.OperationDownloadFile, respBody)
})
// 检查输出格式
if dtu.PrintFormat == "" {
dtu.PrintFormat = DefaultPrintFormat
}
// 这里用共享变量的方式
isComplete := false
der.OnDownloadStatusEvent(func(status transfer.DownloadStatuser, workersCallback func(downloader.RangeWorkerFunc)) {
// 这里可能会下载结束了, 还会输出内容
builder := &strings.Builder{}
if dtu.IsPrintStatus {
// 输出所有的worker状态
var (
tb = pcstable.NewTable(builder)
)
tb.SetHeader([]string{"#", "status", "range", "left", "speeds", "error"})
workersCallback(func(key int, worker *downloader.Worker) bool {
wrange := worker.GetRange()
tb.Append([]string{fmt.Sprint(worker.ID()), worker.GetStatus().StatusText(), wrange.ShowDetails(), strconv.FormatInt(wrange.Len(), 10), strconv.FormatInt(worker.GetSpeedsPerSecond(), 10), fmt.Sprint(worker.Err())})
return true
})
// 先空两行
builder.WriteString("\n\n")
tb.Render()
}
// 如果下载速度为0, 剩余下载时间未知, 则用 - 代替
var leftStr string
left := status.TimeLeft()
if left < 0 {
leftStr = "-"
} else {
leftStr = left.String()
}
fmt.Fprintf(builder,dtu.PrintFormat, dtu.taskInfo.Id(),
converter.ConvertFileSize(status.Downloaded(), 2),
converter.ConvertFileSize(status.TotalSize(), 2),
converter.ConvertFileSize(status.SpeedsPerSecond(), 2),
status.TimeElapsed()/1e7*1e7, leftStr,
)
if !isComplete {
// 如果未完成下载, 就输出
fmt.Print(builder.String())
}
})
der.OnExecute(func() {
if dtu.Cfg.IsTest {
fmt.Printf("[%s] 测试下载开始\n\n", dtu.taskInfo.Id())
}
})
err = der.Execute()
isComplete = true
fmt.Print("\n")
if err != nil {
// 下载发生错误
if !dtu.Cfg.IsTest {
// 下载失败, 删去空文件
if info, infoErr := file.Stat(); infoErr == nil {
if info.Size() == 0 {
// 空文件, 应该删除
dtu.verboseInfof("[%s] remove empty file: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
removeErr := os.Remove(dtu.SavePath)
if removeErr != nil {
dtu.verboseInfof("[%s] remove file error: %s\n", dtu.taskInfo.Id(), removeErr)
}
}
}
}
return err
}
// 下载成功
if !dtu.Cfg.IsTest {
if dtu.IsExecutedPermission {
err = file.Chmod(0766)
if err != nil {
fmt.Printf("[%s] 警告, 加执行权限错误: %s\n", dtu.taskInfo.Id(), err)
}
}
fmt.Printf("[%s] 下载完成, 保存位置: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
} else {
fmt.Printf("[%s] 测试下载结束\n", dtu.taskInfo.Id())
}
return nil
}
//panHTTPClient 获取包含特定User-Agent的HTTPClient
func (dtu *DownloadTaskUnit) panHTTPClient() (client *requester.HTTPClient) {
client = pcsconfig.Config.PanHTTPClient()
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
// 去掉 Referer
if !pcsconfig.Config.EnableHTTPS {
req.Header.Del("Referer")
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
}
client.SetTimeout(20 * time.Minute)
client.SetKeepAlive(true)
return client
}
func (dtu *DownloadTaskUnit) handleError(result *taskframework.TaskUnitRunResult) {
switch value := result.Err.(type) {
case pcserror.Error: // pcserror 接口
switch value.GetErrType() {
case pcserror.ErrTypeRemoteError:
// 远程服务器错误
case 31045: // user not exists
fallthrough
case 31066: // file does not exist
result.NeedRetry = false
case 31626: // user is not authorized
//可能是User-Agent不对
//重试
fallthrough
default:
result.NeedRetry = true
}
case *os.PathError:
// 系统级别的错误, 可能是权限问题
result.NeedRetry = false
default:
// 其他错误, 需要重试
result.NeedRetry = true
}
}
func (dtu *DownloadTaskUnit) execPanDownload(dlink string, result *taskframework.TaskUnitRunResult, okPtr *bool) {
dtu.verboseInfof("[%s] 获取到下载链接: %s\n", dtu.taskInfo.Id(), dlink)
client := dtu.panHTTPClient()
err := dtu.download(dlink, client)
if err != nil {
result.ResultMessage = StrDownloadFailed
result.Err = err
dtu.handleError(result)
return
}
*okPtr = true
}
func (dtu *DownloadTaskUnit) locateDownload(result *taskframework.TaskUnitRunResult) (ok bool) {
rawDlinks, err := GetLocateDownloadLinks(dtu.PCS, dtu.PcsPath)
if err != nil {
result.ResultMessage = StrDownloadGetDlinkFailed
result.Err = err
dtu.handleError(result)
return
}
// 更新链接的协议
FixHTTPLinkURL(rawDlinks[0])
dlink := rawDlinks[0].String()
dtu.execPanDownload(dlink, result, &ok)
return
}
func (dtu *DownloadTaskUnit) pcsOrStreamingDownload(mode DownloadMode, result *taskframework.TaskUnitRunResult) (ok bool) {
dfunc := func(downloadURL string, jar http.CookieJar) error {
client := pcsconfig.Config.PCSHTTPClient()
client.SetCookiejar(jar)
client.SetKeepAlive(true)
client.SetTimeout(10 * time.Minute)
return dtu.download(downloadURL, client)
}
var err error
switch mode {
case DownloadModePCS:
err = dtu.PCS.DownloadFile(dtu.PcsPath, dfunc)
case DownloadModeStreaming:
err = dtu.PCS.DownloadStreamFile(dtu.PcsPath, dfunc)
default:
panic("unreachable")
}
if err != nil {
result.ResultMessage = StrDownloadFailed
result.Err = err
dtu.handleError(result)
return
}
return true // 下载成功
}
//checkFileValid 检测文件有效性
func (dtu *DownloadTaskUnit) checkFileValid(result *taskframework.TaskUnitRunResult) (ok bool) {
if dtu.Cfg.IsTest || dtu.NoCheck {
// 不检测文件有效性
return
}
if dtu.fileInfo.Size >= 128*converter.MB {
// 大文件, 输出一句提示消息
fmt.Printf("[%s] 开始检验文件有效性, 请稍候...\n", dtu.taskInfo.Id())
}
// 就在这里处理校验出错
err := CheckFileValid(dtu.SavePath, dtu.fileInfo)
if err != nil {
result.ResultMessage = StrDownloadChecksumFailed
result.Err = err
switch err {
case ErrDownloadNotSupportChecksum:
// 文件不支持校验
result.ResultMessage = "检验文件有效性"
result.Err = err
fmt.Printf("[%s] 检验文件有效性: %s\n", dtu.taskInfo.Id(), err)
return true
case ErrDownloadFileBanned:
// 违规文件
result.NeedRetry = false
return
case ErrDownloadChecksumFailed:
// 校验失败, 需要重新下载
result.NeedRetry = true
// 设置允许覆盖
dtu.IsOverwrite = true
return
default:
result.NeedRetry = false
return
}
}
fmt.Printf("[%s] 检验文件有效性成功: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
return true
}
func (dtu *DownloadTaskUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunResult) {
// 输出错误信息
if lastRunResult.Err == nil {
// result中不包含Err, 忽略输出
fmt.Printf("[%s] %s, 重试 %d/%d\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, dtu.taskInfo.Retry(), dtu.taskInfo.MaxRetry())
return
}
fmt.Printf("[%s] %s, %s, 重试 %d/%d\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err, dtu.taskInfo.Retry(), dtu.taskInfo.MaxRetry())
}
func (dtu *DownloadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunResult) {
}
func (dtu *DownloadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunResult) {
// 失败
if lastRunResult.Err == nil {
// result中不包含Err, 忽略输出
fmt.Printf("[%s] %s\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage)
return
}
fmt.Printf("[%s] %s, %s\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err)
}
func (dtu *DownloadTaskUnit) OnComplete(lastRunResult *taskframework.TaskUnitRunResult) {
}
func (dtu *DownloadTaskUnit) RetryWait() time.Duration {
return pcsfunctions.RetryWait(dtu.taskInfo.Retry())
}
func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
result = &taskframework.TaskUnitRunResult{}
// 获取文件信息
var err error
if dtu.fileInfo == nil || dtu.taskInfo.Retry() > 0 {
// 没有获取文件信息
// 如果是动态添加的下载任务, 是会写入文件信息的
// 如果该任务重试过, 则应该再获取一次文件信息
dtu.fileInfo, err = dtu.PCS.FilesDirectoriesMeta(dtu.PcsPath)
if err != nil {
// 如果不是未登录或文件不存在, 则不重试
result.ResultMessage = "获取下载路径信息错误"
result.Err = err
dtu.handleError(result)
return
}
}
// 输出文件信息
fmt.Print("\n")
fmt.Printf("[%s] ----\n%s\n", dtu.taskInfo.Id(), dtu.fileInfo.String())
// 如果是一个目录, 将子文件和子目录加入队列
if dtu.fileInfo.Isdir {
if !dtu.Cfg.IsTest { // 测试下载, 不建立空目录
os.MkdirAll(dtu.SavePath, 0777) // 首先在本地创建目录, 保证空目录也能被保存
}
// 获取该目录下的文件列表
fileList, err := dtu.PCS.FilesDirectoriesList(dtu.PcsPath, baidupcs.DefaultOrderOptions)
if err != nil {
result.ResultMessage = "获取目录信息错误"
result.Err = err
result.NeedRetry = true
return
}
for k := range fileList {
// 添加子任务
subUnit := *dtu
newCfg := *dtu.Cfg
subUnit.Cfg = &newCfg
subUnit.fileInfo = fileList[k] // 保存文件信息
subUnit.PcsPath = fileList[k].Path
subUnit.SavePath = filepath.Join(dtu.SavePath, fileList[k].Filename) // 保存位置
// 加入父队列
info := dtu.ParentTaskExecutor.Append(&subUnit, dtu.taskInfo.MaxRetry())
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), fileList[k].Path)
}
result.Succeed = true // 执行成功
return
}
fmt.Printf("[%s] 准备下载: %s\n", dtu.taskInfo.Id(), dtu.PcsPath)
if !dtu.Cfg.IsTest && !dtu.IsOverwrite && FileExist(dtu.SavePath) {
fmt.Printf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
result.Succeed = true // 执行成功
return
}
if !dtu.Cfg.IsTest {
// 不是测试下载, 输出下载路径
fmt.Printf("[%s] 将会下载到路径: %s\n\n", dtu.taskInfo.Id(), dtu.SavePath)
}
var ok bool
// 获取下载链接
switch dtu.DownloadMode {
case DownloadModeLocate:
ok = dtu.locateDownload(result)
case DownloadModePCS, DownloadModeStreaming:
ok = dtu.pcsOrStreamingDownload(dtu.DownloadMode, result)
}
if !ok {
// 以上执行不成功, 返回
return result
}
// 检测文件有效性
ok = dtu.checkFileValid(result)
if !ok {
// 校验不成功, 返回结果
return result
}
// 统计下载
dtu.DownloadStatistic.AddTotalSize(dtu.fileInfo.Size)
// 下载成功
result.Succeed = true
return
}

View File

@@ -0,0 +1,16 @@
package pcsdownload
import "errors"
var (
// ErrDownloadNotSupportChecksum 文件不支持校验
ErrDownloadNotSupportChecksum = errors.New("该文件不支持校验")
// ErrDownloadChecksumFailed 文件校验失败
ErrDownloadChecksumFailed = errors.New("该文件校验失败, 文件md5值与服务器记录的不匹配")
// ErrDownloadFileBanned 违规文件
ErrDownloadFileBanned = errors.New("该文件可能是违规文件, 不支持校验")
// ErrDlinkNotFound 未取得下载链接
ErrDlinkNotFound = errors.New("未取得下载链接")
// ErrShareInfoNotFound 未在已分享列表中找到分享信息
ErrShareInfoNotFound = errors.New("未在已分享列表中找到分享信息")
)

View File

@@ -0,0 +1,36 @@
package pcsdownload
import (
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/requester"
"net/http"
"strconv"
)
// IsSkipMd5Checksum 是否忽略某些校验
func IsSkipMd5Checksum(size int64, md5Str string) bool {
switch {
case size == 1749504 && md5Str == "48bb9b0361dc9c672f3dc7b3ffcfde97": //8秒温馨提示
fallthrough
case size == 120 && md5Str == "6c1b84914588d09a6e5ec43605557457": //温馨提示文字版
return true
}
return false
}
// BaiduPCSURLCheckFunc downloader 首次检查下载地址要执行的函数
func BaiduPCSURLCheckFunc(client *requester.HTTPClient, durl string) (contentLength int64, resp *http.Response, err error) {
resp, err = client.Req(http.MethodGet, durl, nil, map[string]string{
"Range": "bytes=0-" + strconv.FormatInt(baidupcs.MaxDownloadRangeSize-1, 10),
})
if err != nil {
if resp != nil {
resp.Body.Close()
}
return 0, nil, err
}
contentLengthStr := resp.Header.Get("x-bs-file-size")
contentLength, _ = strconv.ParseInt(contentLengthStr, 10, 64)
return contentLength, resp, nil
}

View File

@@ -0,0 +1,63 @@
package pcsdownload
import (
"encoding/hex"
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/internal/pcsconfig"
"github.com/iikira/BaiduPCS-Go/pcsutil/checksum"
"net/url"
"os"
)
// CheckFileValid 检测文件有效性
func CheckFileValid(filePath string, fileInfo *baidupcs.FileDirectory) error {
if len(fileInfo.BlockList) != 1 {
return ErrDownloadNotSupportChecksum
}
f := checksum.NewLocalFileChecksum(filePath, int(baidupcs.SliceMD5Size))
err := f.OpenPath()
if err != nil {
return err
}
defer f.Close()
err = f.Sum(checksum.CHECKSUM_MD5)
if err != nil {
return err
}
md5Str := hex.EncodeToString(f.MD5)
if md5Str != fileInfo.MD5 { // md5不一致
// 检测是否为违规文件
if IsSkipMd5Checksum(f.Length, md5Str) {
return ErrDownloadFileBanned
}
return ErrDownloadChecksumFailed
}
return nil
}
// FileExist 检查文件是否存在,
// 只有当文件存在, 文件大小不为0或断点续传文件不存在时, 才判断为存在
func FileExist(path string) bool {
if info, err := os.Stat(path); err == nil {
if info.Size() == 0 {
return false
}
if _, err = os.Stat(path + DownloadSuffix); err != nil {
return true
}
}
return false
}
//FixHTTPLinkURL 通过配置, 确定链接使用的协议(http,https)
func FixHTTPLinkURL(linkURL *url.URL) {
if pcsconfig.Config.EnableHTTPS {
if linkURL.Scheme == "http" {
linkURL.Scheme = "https"
}
}
}

View File

@@ -0,0 +1,14 @@
// Package pcsupload 上传包
package pcsupload
import (
"github.com/iikira/BaiduPCS-Go/pcsverbose"
)
const (
UploadingFileName = "pcs_uploading.json"
)
var (
pcsUploadVerbose = pcsverbose.New("PCSUPLOAD")
)

View File

@@ -0,0 +1,119 @@
package pcsupload
import (
"context"
"github.com/iikira/BaiduPCS-Go/baidupcs"
"github.com/iikira/BaiduPCS-Go/baidupcs/pcserror"
"github.com/iikira/BaiduPCS-Go/internal/pcsconfig"
"github.com/iikira/BaiduPCS-Go/requester"
"github.com/iikira/BaiduPCS-Go/requester/multipartreader"
"github.com/iikira/BaiduPCS-Go/requester/rio"
"github.com/iikira/BaiduPCS-Go/requester/uploader"
"io"
"net/http"
)
type (
PCSUpload struct {
pcs *baidupcs.BaiduPCS
targetPath string
}
EmptyReaderLen64 struct {
}
)
func (e EmptyReaderLen64) Read(p []byte) (n int, err error) {
return 0, io.EOF
}
func (e EmptyReaderLen64) Len() int64 {
return 0
}
func NewPCSUpload(pcs *baidupcs.BaiduPCS, targetPath string) uploader.MultiUpload {
return &PCSUpload{
pcs: pcs,
targetPath: targetPath,
}
}
func (pu *PCSUpload) lazyInit() {
if pu.pcs == nil {
pu.pcs = &baidupcs.BaiduPCS{}
}
}
// Precreate do nothing
func (pu *PCSUpload) Precreate() (err error) {
return nil
}
func (pu *PCSUpload) TmpFile(ctx context.Context, partseq int, partOffset int64, r rio.ReaderLen64) (checksum string, uperr error) {
pu.lazyInit()
var respErr *uploader.MultiError
checksum, pcsError := pu.pcs.UploadTmpFile(func(uploadURL string, jar http.CookieJar) (resp *http.Response, err error) {
client := pcsconfig.Config.PCSHTTPClient()
client.SetCookiejar(jar)
client.SetTimeout(0)
mr := multipartreader.NewMultipartReader()
mr.AddFormFile("uploadedfile", "", r)
mr.CloseMultipart()
doneChan := make(chan struct{}, 1)
go func() {
resp, err = client.Req(http.MethodPost, uploadURL, mr, nil)
doneChan <- struct{}{}
if resp != nil {
// 不可恢复的错误
switch resp.StatusCode {
case 400, 401, 403, 413:
respErr = &uploader.MultiError{
Terminated: true,
}
}
}
}()
select {
case <-ctx.Done(): // 取消
// 返回, 让那边关闭连接
return resp, ctx.Err()
case <-doneChan:
// return
}
return
})
if respErr != nil {
respErr.Err = pcsError
return checksum, respErr
}
return checksum, pcsError
}
func (pu *PCSUpload) CreateSuperFile(checksumList ...string) (err error) {
pu.lazyInit()
// 先在网盘目标位置, 上传一个空文件
// 防止出现file does not exist
pcsError := pu.pcs.Upload(pu.targetPath, func(uploadURL string, jar http.CookieJar) (resp *http.Response, err error) {
mr := multipartreader.NewMultipartReader()
mr.AddFormFile("file", "file", &EmptyReaderLen64{})
mr.CloseMultipart()
c := requester.NewHTTPClient()
c.SetCookiejar(jar)
return c.Req(http.MethodPost, uploadURL, mr, nil)
})
if pcsError != nil {
// 修改操作
pcsError.(*pcserror.PCSErrInfo).Operation = baidupcs.OperationUploadCreateSuperFile
return pcsError
}
return pu.pcs.UploadCreateSuperFile(false, pu.targetPath, checksumList...)
}

View File

@@ -0,0 +1,65 @@
package pcsupload
/*
import (
"github.com/iikira/BaiduPCS-Go/baidupcs"
)
type (
// PCSUpload2 新的上传方式
// TODO
PCSUpload2 struct {
pcs *baidupcs.BaiduPCS
targetPath string
uploadid string
}
)
func NewPCSUpload2(pcs *baidupcs.BaiduPCS, targetPath string) uploader.MultiUpload {
return &PCSUpload{
pcs: pcs,
targetPath: targetPath,
}
}
func (pu2 *PCSUpload2) lazyInit() {
if pu2.pcs == nil {
pu2.pcs = &baidupcs.BaiduPCS{}
}
}
// Precreate
func (pu2 *PCSUpload2) Precreate() (err error) {
return nil
}
func (pu2 *PCSUpload2) TmpFile(ctx context.Context, partseq int, partOffset int64, r rio.ReaderLen64) (checksum string, uperr error) {
pu2.lazyInit()
return pu.pcs.UploadTmpFile(func(uploadURL string, jar http.CookieJar) (resp *http.Response, err error) {
client := pcsconfig.Config.HTTPClient()
client.SetCookiejar(jar)
client.SetTimeout(0)
mr := multipartreader.NewMultipartReader()
mr.AddFormFile("uploadedfile", "", r)
mr.CloseMultipart()
doneChan := make(chan struct{}, 1)
go func() {
resp, err = client.Req("POST", uploadURL, mr, nil)
doneChan <- struct{}{}
}()
select {
case <-ctx.Done():
return resp, ctx.Err()
case <-doneChan:
// return
}
return
})
}
func (pu2 *PCSUpload2) CreateSuperFile(checksumList ...string) (err error) {
return nil
}
*/

View File

@@ -0,0 +1,198 @@
package pcsupload
import (
"errors"
"github.com/iikira/BaiduPCS-Go/internal/pcsconfig"
"github.com/iikira/BaiduPCS-Go/pcsutil/checksum"
"github.com/iikira/BaiduPCS-Go/pcsutil/converter"
"github.com/iikira/BaiduPCS-Go/pcsutil/jsonhelper"
"github.com/iikira/BaiduPCS-Go/requester/uploader"
"os"
"path/filepath"
"strings"
"time"
)
type (
// Uploading 未完成上传的信息
Uploading struct {
*checksum.LocalFileMeta
State *uploader.InstanceState `json:"state"`
}
// UploadingDatabase 未完成上传的数据库
UploadingDatabase struct {
UploadingList []*Uploading `json:"upload_state"`
Timestamp int64 `json:"timestamp"`
dataFile *os.File
}
)
// NewUploadingDatabase 初始化未完成上传的数据库, 从库中读取内容
func NewUploadingDatabase() (ud *UploadingDatabase, err error) {
file, err := os.OpenFile(filepath.Join(pcsconfig.GetConfigDir(), UploadingFileName), os.O_CREATE|os.O_RDWR, 0777)
if err != nil {
return nil, err
}
ud = &UploadingDatabase{
dataFile: file,
}
info, err := file.Stat()
if err != nil {
return nil, err
}
if info.Size() <= 0 {
return ud, nil
}
err = jsonhelper.UnmarshalData(file, ud)
if err != nil {
return nil, err
}
return ud, nil
}
// Save 保存内容
func (ud *UploadingDatabase) Save() error {
if ud.dataFile == nil {
return errors.New("dataFile is nil")
}
ud.Timestamp = time.Now().Unix()
var (
builder = &strings.Builder{}
err = jsonhelper.MarshalData(builder, ud)
)
if err != nil {
panic(err)
}
err = ud.dataFile.Truncate(int64(builder.Len()))
if err != nil {
return err
}
str := builder.String()
_, err = ud.dataFile.WriteAt(converter.ToBytes(str), 0)
if err != nil {
return err
}
return nil
}
// UpdateUploading 更新正在上传
func (ud *UploadingDatabase) UpdateUploading(meta *checksum.LocalFileMeta, state *uploader.InstanceState) {
if meta == nil {
return
}
meta.CompleteAbsPath()
for k, uploading := range ud.UploadingList {
if uploading.LocalFileMeta == nil {
continue
}
if uploading.LocalFileMeta.EqualLengthMD5(meta) || uploading.LocalFileMeta.Path == meta.Path {
ud.UploadingList[k].State = state
return
}
}
ud.UploadingList = append(ud.UploadingList, &Uploading{
LocalFileMeta: meta,
State: state,
})
}
func (ud *UploadingDatabase) deleteIndex(k int) {
ud.UploadingList = append(ud.UploadingList[:k], ud.UploadingList[k+1:]...)
}
// Delete 删除
func (ud *UploadingDatabase) Delete(meta *checksum.LocalFileMeta) bool {
if meta == nil {
return false
}
meta.CompleteAbsPath()
for k, uploading := range ud.UploadingList {
if uploading.LocalFileMeta == nil {
continue
}
if uploading.LocalFileMeta.EqualLengthMD5(meta) || uploading.LocalFileMeta.Path == meta.Path {
ud.deleteIndex(k)
return true
}
}
return false
}
// Search 搜索
func (ud *UploadingDatabase) Search(meta *checksum.LocalFileMeta) *uploader.InstanceState {
if meta == nil {
return nil
}
meta.CompleteAbsPath()
ud.clearModTimeChange()
for _, uploading := range ud.UploadingList {
if uploading.LocalFileMeta == nil {
continue
}
if uploading.LocalFileMeta.EqualLengthMD5(meta) {
return uploading.State
}
if uploading.LocalFileMeta.Path == meta.Path {
// 移除旧的信息
// 目前只是比较了文件大小
if meta.Length != uploading.LocalFileMeta.Length {
ud.Delete(meta)
return nil
}
// 覆盖数据
meta.MD5 = uploading.LocalFileMeta.MD5
meta.SliceMD5 = uploading.LocalFileMeta.SliceMD5
return uploading.State
}
}
return nil
}
func (ud *UploadingDatabase) clearModTimeChange() {
for i := 0; i < len(ud.UploadingList); i++ {
uploading := ud.UploadingList[i]
if uploading.LocalFileMeta == nil {
continue
}
if uploading.ModTime == -1 { // 忽略
continue
}
info, err := os.Stat(uploading.LocalFileMeta.Path)
if err != nil {
ud.deleteIndex(i)
i--
pcsUploadVerbose.Warnf("clear invalid file path: %s, err: %s\n", uploading.LocalFileMeta.Path, err)
continue
}
if uploading.LocalFileMeta.ModTime != info.ModTime().Unix() {
ud.deleteIndex(i)
i--
pcsUploadVerbose.Infof("clear modified file path: %s\n", uploading.LocalFileMeta.Path)
continue
}
}
}
// Close 关闭数据库
func (ud *UploadingDatabase) Close() error {
return ud.dataFile.Close()
}

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