Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b368060ffb | |||
| 504d38db90 | |||
| 4a0ad141dc | |||
| a7a386e9a6 | |||
| 6506acba9a | |||
| 1e6004d367 | |||
| 9796673138 | |||
| b506bcc84d | |||
| 989abc71ee | |||
| ebed4ff62f | |||
| 465d8fa4e9 | |||
| e9ae974017 | |||
| 54dd4e766b | |||
| 4727b9696d | |||
| 4769d17281 | |||
| 03f3d37fb8 | |||
| d1a577810c | |||
| 670b5c5dad | |||
| 9d084610e5 | |||
| 9962e63c81 | |||
| f7b8d26964 | |||
| 280421fd88 | |||
| 02b6d529d0 | |||
| 561c50956f | |||
| 6a39dd78f6 | |||
| a5ff0780b7 | |||
| 1d2342efb5 | |||
| 2e3916be7e | |||
| 68b3a4aba6 | |||
| 82c62a2d51 | |||
| a02638272f | |||
| 87d3770e5b | |||
| 8ae323eaa6 | |||
| 21770fec89 | |||
| 291fbd54c8 | |||
| 281ba818cd | |||
| df53c7f44b | |||
| 5cab8cc6a5 | |||
| 25202861c5 | |||
| 3fcbbe9b40 | |||
| ae3be4ce6f | |||
| ae81b59926 | |||
| c0da3de6c6 | |||
| 134531d64a | |||
| 0064a23f64 | |||
| 425dddf92b | |||
| 31aaadfcc1 | |||
| 1f10f1e982 | |||
| a9f9f18f09 | |||
| 5e90f96577 | |||
| 5b008b935d | |||
| bdc0a15bdc | |||
| 26c7201f4e | |||
| 7da4fedc54 | |||
| 957a1a1bd1 | |||
| 92133d185e | |||
| 239b794f8b | |||
| b8f35e8647 | |||
| c465277a8d | |||
| 281552eefa | |||
| d26d4d273e | |||
| f531c7bc70 | |||
| 443eac7589 | |||
| 6aecb98595 | |||
| 62c50726a5 | |||
| a7034e300f | |||
| c86afbd764 | |||
| d83f87942c | |||
| b79cd7bafb | |||
| 4c0c8a4ff9 | |||
| bb231dd45e | |||
| bec492fe65 | |||
| 69b76a15ca | |||
| 3bdc27ce66 | |||
| 9dd2482b58 | |||
| e8b87a02dc | |||
| 787aa53ce7 | |||
| 4a623b443c | |||
| 136dfb082f | |||
| e4d73d8b42 | |||
| 47123f2b86 | |||
| 9d6dfd0d0a | |||
| 3c0da7fc1d | |||
| 2fe5cc5414 | |||
| e261e04978 | |||
| e5f63c60e9 | |||
| c350cadba4 | |||
| 4e8c4d6e9c | |||
| a282aabbe7 | |||
| bbff32c8c6 |
@@ -4,14 +4,17 @@
|
||||
# 特色
|
||||
1. 多平台支持, 支持 Windows, macOS, linux(x86/x64/arm), android, iOS 等
|
||||
2. 阿里云盘多用户支持
|
||||
3. 支持同步盘,资源库,相册网盘无缝切换
|
||||
3. 支持同步盘,资源库无缝切换
|
||||
4. [下载](docs/manual.md#下载文件目录)网盘内文件, 支持多个文件或目录下载, 支持断点续传和单文件并行下载。支持软链接(符号链接)文件。
|
||||
5. [上传](docs/manual.md#上传文件目录)本地文件, 支持多个文件或目录上传,支持排除指定文件夹/文件(正则表达式)功能。支持软链接(符号链接)文件。
|
||||
6. [同步备份功能](docs/manual.md#同步备份功能)支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份保持本地文件和网盘文件同步。常用于嵌入式或者NAS等设备,支持docker镜像部署。
|
||||
7. 命令和文件路径输入支持Tab键自动补全,路径支持通配符匹配模式
|
||||
8. 支持阿里云ECS环境下使用内网链接上传/下载,速度更快(只支持阿里经典网络,最高可达100MB/s),还可以节省公网带宽流量(配置transfer_url_type=2即可)
|
||||
9. 支持[JavaScript插件](docs/manual.md#JavaScript插件),你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求
|
||||
10. 支持个人相册的相关操作,支持批量下载相册所有照片、视频文件到本地
|
||||
8. 支持[JavaScript插件](docs/manual.md#JavaScript插件),你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求
|
||||
9. 支持个人相册的相关操作,支持批量下载相册所有照片、视频文件到本地
|
||||
|
||||
# 阿里云盘VIP会员推荐码
|
||||
如果大家有打算开通阿里云盘VIP会员,可以使用阿里云盘APP扫描我们的推荐码进行开通。
|
||||

|
||||
|
||||
# 目录
|
||||
- [关于](#关于)
|
||||
@@ -38,7 +41,6 @@
|
||||
- [同步备份文件](#同步备份文件)
|
||||
- [更多命令](#更多命令)
|
||||
- [常见问题](#常见问题)
|
||||
- [如何获取RefreshToken](#如何获取refreshtoken)
|
||||
- [如何开启Debug调试日志](#如何开启debug调试日志)
|
||||
- [第一步](#第一步)
|
||||
- [第二步](#第二步)
|
||||
@@ -55,15 +57,16 @@
|
||||
2. arm64 : 适用64位ARM系统
|
||||
3. 386 / x86 : 适用32系统,包括Intel和AMD的CPU系统
|
||||
4. amd64 / x64 : 适用64位系统,包括Intel和AMD的CPU系统
|
||||
5. mips : 适用MIPS指令集的CPU,例如国产龙芯CPU
|
||||
6. macOS amd64适用Intel CPU的机器,macOS arm64目前主要是适用苹果M1芯片的机器
|
||||
5. mips : 适用MIPS指令集的CPU,例如中标麒麟的系统。MIPS要注意大小端字序的不同对应的版本也不同。
|
||||
6. macOS amd64适用Intel CPU的机器,macOS arm64目前主要是适用苹果M系列芯片的机器
|
||||
7. iOS arm64适用iPhone手机,并且必须是越狱的手机才能正常运行
|
||||
8. loong64 : 适用于LoongArch64架构的机器,例如国产龙芯CPU
|
||||
|
||||
参考例子:
|
||||
```shell
|
||||
wget https://github.com/tickstep/aliyunpan/releases/download/v0.2.9/aliyunpan-v0.2.9-linux-amd64.zip
|
||||
unzip aliyunpan-v0.2.9-linux-amd64.zip
|
||||
cd aliyunpan-v0.2.9-linux-amd64
|
||||
wget https://github.com/tickstep/aliyunpan/releases/download/v0.3.2/aliyunpan-v0.3.2-linux-amd64.zip
|
||||
unzip aliyunpan-v0.3.2-linux-amd64.zip
|
||||
cd aliyunpan-v0.3.2-linux-amd64
|
||||
./aliyunpan
|
||||
```
|
||||
|
||||
@@ -108,21 +111,30 @@ winget install tickstep.aliyunpan --silent
|
||||
|
||||
## docker安装
|
||||
### sync同步盘
|
||||
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份三种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
备份功能支持以下三种模式:
|
||||
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地两种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
备份功能支持以下模式:
|
||||
1. 备份本地文件,即上传本地文件到网盘,始终保持本地文件有一个完整的备份在网盘
|
||||
2. 备份云盘文件,即下载网盘文件到本地,始终保持网盘的文件有一个完整的备份在本地
|
||||
3. 双向备份,保持网盘文件和本地文件严格一致
|
||||
|
||||
备份功能支持指定备份策略:
|
||||
1. exclusive,排他备份文件(一比一镜像备份),目标目录多余的文件会被删除。保证备份的源目录,和目标目录文件一比一备份。源目录文件如果文件被删除,则对应的目标目录的文件也会被删除。
|
||||
2. increment,增量备份文件,目标目录多余的文件不会被删除。只会把源目录修改的文件,新增的文件备份到目标目录。如果源目录有文件删除,或者目标目录有其他文件新增是不会被删除。
|
||||
|
||||
同步的基本逻辑如下所示,一次循环包括:扫描-对比-执行,一共三个环节。
|
||||

|
||||
|
||||
```
|
||||
docker run -d --name=aliyunpan-sync --restart=always -v "<your local dir>:/home/app/data" -e TZ="Asia/Shanghai" -e ALIYUNPAN_REFRESH_TOKEN="<your refreshToken>" -e ALIYUNPAN_PAN_DIR="<your drive pan dir>" -e ALIYUNPAN_SYNC_MODE="upload" -e ALIYUNPAN_TASK_STEP="sync" tickstep/aliyunpan-sync:v0.2.9
|
||||
|
||||
docker run -d --name=aliyunpan-sync --restart=always -v "<your aliyunpan_config.json>:/home/app/config/aliyunpan_config.json" -v "<your local dir>:/home/app/data" -e ALIYUNPAN_PAN_DIR="<your drive pan dir>" -e ALIYUNPAN_SYNC_MODE="upload" -e ALIYUNPAN_SYNC_POLICY="increment" -e ALIYUNPAN_SYNC_DRIVE="backup" -e ALIYUNPAN_SYNC_LOG="true" tickstep/aliyunpan-sync:v0.3.2
|
||||
|
||||
<your local dir>:本地目录绝对路径,例如:/tickstep/Documents/设计文档
|
||||
ALIYUNPAN_PAN_DIR:云盘目录
|
||||
ALIYUNPAN_REFRESH_TOKEN:RefreshToken
|
||||
ALIYUNPAN_SYNC_MODE:备份模式,支持三种: upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)
|
||||
ALIYUNPAN_TASK_STEP:任务步骤, 支持两种: scan(只扫描并建立同步数据库),sync(正常启动同步任务)。如果你同步目录文件非常多,首次运行最好先跑一次scan步骤,然后再正常启动文件同步任务
|
||||
<your aliyunpan_config.json>: 用户已经登录成功并保存好的aliyunpan_config.json凭据文件
|
||||
<your local dir>:本地目标目录,绝对路径,例如:/tickstep/Documents/设计文档
|
||||
ALIYUNPAN_PAN_DIR:云盘目标目录,绝对路径
|
||||
ALIYUNPAN_SYNC_MODE:备份模式,支持: upload(备份本地文件到云盘),download(备份云盘文件到本地)
|
||||
ALIYUNPAN_SYNC_POLICY:备份策略,支持:exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)
|
||||
ALIYUNPAN_SYNC_DRIVE: 网盘,支持:backup(备份盘), resource(资源盘)
|
||||
ALIYUNPAN_SYNC_LOG: 同步日志,true-开启同步日志显示,false-关闭同步日志
|
||||
```
|
||||
docker-compose启动方式请查看:[docker-compose.yml](https://github.com/tickstep/aliyunpan/blob/main/docs/manual.md#Docker%E8%BF%90%E8%A1%8C)
|
||||
更详情文档请参考dockerhub网址:[tickstep/aliyunpan-sync](https://hub.docker.com/r/tickstep/aliyunpan-sync)
|
||||
|
||||
# 如何使用
|
||||
@@ -186,11 +198,12 @@ aliyunpan > help
|
||||
```
|
||||
|
||||
### 登录
|
||||
需要先登录,已经登录过的可以跳过此步。
|
||||
RefreshToken获取教程请查看:[如何获取RefreshToken](#如何获取RefreshToken)
|
||||
需要先登录,已经登录过的可以跳过此步。登录是在浏览器上进行,你需要进行两次登录。
|
||||
```shell
|
||||
aliyunpan > login -RefreshToken=32994cd2c43...4d505fa79
|
||||
|
||||
aliyunpan > login
|
||||
请在浏览器打开以下链接进行登录,链接有效时间为5分钟。
|
||||
注意:你需要进行一次授权一次扫码的两次登录。
|
||||
https://openapi.alipan.com/oauth/authorize?client_id=cf9f70e8fc61430f8ec5ab5cadf31375&redirect_uri=https%3A%2F%2Fapi.tickstep.com%2Fauth%2Ftickstep%2Faliyunpan%2Ftoken%2Fopenapi%2F8206f0.....fb5db6b40336%2Fauth&scope=user:base,file:all:read,file:all:write
|
||||
阿里云盘登录成功: tickstep
|
||||
aliyunpan:/ tickstep$
|
||||
```
|
||||
@@ -202,7 +215,6 @@ aliyunpan:/ tickstep(备份盘)$ drive
|
||||
# DRIVE ID 网盘名称
|
||||
1 11519221 备份盘
|
||||
2 1311893110 资源库
|
||||
3 61104421 相册
|
||||
|
||||
输入要切换的网盘 # 值 > 2
|
||||
切换到网盘:资源库
|
||||
@@ -248,10 +260,6 @@ aliyunpan:/ tickstep$ download IMG_0106.JPG
|
||||
下载结束, 时间: 4秒, 数据总量: 1.48MB
|
||||
aliyunpan:/ tickstep$
|
||||
```
|
||||
下载支持两种链接类型:1-默认类型 2-阿里ECS环境类型
|
||||
在普通网络下,下载速度可以达到10MB/s,在阿里ECS(必须是"经典网络"类型的机器)环境下,下载速度单文件可以轻松达到20MB/s,多文件可以达到100MB/s
|
||||

|
||||

|
||||
|
||||
### 上传文件
|
||||
```shell
|
||||
@@ -270,23 +278,13 @@ aliyunpan:/ tickstep$ upload /Users/tickstep/Downloads/apt.zip /tmp
|
||||
|
||||
上传结束, 时间: 18秒, 数据总量: 21.00MB
|
||||
```
|
||||
上传支持两种链接类型:1-默认类型 2-阿里ECS环境类型
|
||||
在阿里ECS(必须是"经典网络"类型的机器)环境下,上传速度单文件可以轻松达到30MB/s,多文件可以达到100MB/s
|
||||

|
||||
|
||||
### 同步备份文件
|
||||
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份三种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
|
||||
***注意:如果同步目录下有非常多的文件,最好在首次备份前先运行一次scan任务,等scan任务完成并建立起同步数据库后,再正常启动同步任务。这样同步任务可以更加快速同步并且能有效避免同步重复文件。***
|
||||
|
||||
|
||||
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份三种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
|
||||
例如:将本地目录 `/tickstep/Documents/设计文档` 中的文件备份上传到云盘目录 `/备份盘/我的文档`
|
||||
```shell
|
||||
首次运行建议先扫描并构建同步数据库,如下:
|
||||
aliyunpan:/ tickstep$ sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload" -step scan
|
||||
|
||||
然后再正常启动同步任务,如下:
|
||||
aliyunpan:/ tickstep$ sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload"
|
||||
aliyunpan:/ tickstep$ sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload" -drive "backup"
|
||||
|
||||
启动同步备份进程
|
||||
备份配置文件:(使用命令行配置)
|
||||
@@ -307,18 +305,6 @@ aliyunpan:/ tickstep$ sync start -ldir "/tickstep/Documents/设计文档" -pdir
|
||||
更多更详细的命令请查看手册:[命令手册](docs/manual.md)。
|
||||
|
||||
# 常见问题
|
||||
## 如何获取RefreshToken
|
||||
需要通过浏览器获取refresh_token。这里以Chrome浏览器为例,其他浏览器类似。
|
||||
打开 [阿里云盘网页](https://www.aliyundrive.com/drive) 并进行登录,然后F12按键打开浏览器调试菜单,按照下面步骤进行
|
||||

|
||||
|
||||
或者直接在控制台输入以下命令获取
|
||||
```
|
||||
JSON.parse(localStorage.getItem("token")).refresh_token
|
||||
```
|
||||

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

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

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

|
||||
Web网页端
|
||||
@@ -353,6 +340,6 @@ Web网页端
|
||||
|
||||
# 鸣谢
|
||||
本项目大量借鉴了以下相关项目的功能&成果
|
||||
> [tickstep/cloudpan189-go](https://github.com/tickstep/cloudpan189-go)
|
||||
> [hacdias/webdav](https://github.com/hacdias/webdav)
|
||||
> [tickstep/aliyunpan-api](https://github.com/tickstep/aliyunpan-api)
|
||||
> [tickstep/cloudpan189-go](https://github.com/tickstep/cloudpan189-go)
|
||||
> [kazutoiris/ali_ecc](https://github.com/kazutoiris/ali_ecc)
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity version="0.2.9.0" processorArchitecture="*" name="com.tickstep.aliyunpan" type="win32"/>
|
||||
<assemblyIdentity version="0.3.2.0" processorArchitecture="*" name="com.tickstep.aliyunpan" type="win32"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 92 KiB |
@@ -62,7 +62,7 @@ IOSBuild() {
|
||||
cd "$output/$1"
|
||||
export CC=/usr/local/go/misc/ios/clangwrap.sh GOOS=ios GOARCH=arm64 GOARM=7 CGO_ENABLED=1
|
||||
$go build -ldflags "-X main.Version=$version -s -w" -o $name github.com/tickstep/aliyunpan
|
||||
jtool --sign --inplace --ent ../../entitlements.xml $name
|
||||
jtool2 --sign --inplace --ent ../../entitlements.xml $name
|
||||
cd ../..
|
||||
RicePack $1 $name
|
||||
Pack $1 "ios"
|
||||
@@ -112,7 +112,7 @@ Build $name-$version"-darwin-macos-arm64" darwin arm64
|
||||
############### Windows ###############
|
||||
Build $name-$version"-windows-x86" windows 386
|
||||
Build $name-$version"-windows-x64" windows amd64
|
||||
Build $name-$version"-windows-arm" windows arm
|
||||
Build $name-$version"-windows-arm64" windows arm64
|
||||
|
||||
############### Linux ###############
|
||||
# x64/x86
|
||||
|
||||
@@ -48,9 +48,6 @@ HEALTHCHECK --start-period=5s --interval=10s --timeout=5s --retries=3 CMD /bin/s
|
||||
ENV ALIYUNPAN_DOCKER=1
|
||||
ENV ALIYUNPAN_CONFIG_DIR=/home/app/config
|
||||
|
||||
ENV ALIYUNPAN_REFRESH_TOKEN=""
|
||||
ENV ALIYUNPAN_TRANSFER_URL_TYPE=0
|
||||
|
||||
ENV ALIYUNPAN_DOWNLOAD_PARALLEL=2
|
||||
ENV ALIYUNPAN_UPLOAD_PARALLEL=2
|
||||
ENV ALIYUNPAN_DOWNLOAD_BLOCK_SIZE=1024
|
||||
@@ -58,10 +55,12 @@ ENV ALIYUNPAN_UPLOAD_BLOCK_SIZE=10240
|
||||
ENV ALIYUNPAN_LOCAL_DIR=/home/app/data
|
||||
ENV ALIYUNPAN_PAN_DIR=/sync_drive
|
||||
ENV ALIYUNPAN_SYNC_MODE=upload
|
||||
ENV ALIYUNPAN_SYNC_POLICY=increment
|
||||
ENV ALIYUNPAN_SYNC_DRIVE=backup
|
||||
ENV ALIYUNPAN_SYNC_CYCLE=infinity
|
||||
ENV ALIYUNPAN_SYNC_PRIORITY=time
|
||||
ENV ALIYUNPAN_SYNC_LOG=false
|
||||
ENV ALIYUNPAN_LOCAL_DELAY_TIME=3
|
||||
ENV ALIYUNPAN_TASK_STEP=sync
|
||||
ENV ALIYUNPAN_DEVICE_ID=""
|
||||
|
||||
# 运行
|
||||
|
||||
@@ -15,11 +15,12 @@ else
|
||||
"syncTaskList": [
|
||||
{
|
||||
"name": "阿里云盘备份",
|
||||
"id": "5b2d7c10-e927-4e72-8f9d-5abb3bb04814",
|
||||
"id": "5b2d7c10-e927-4e72-8f9d-5abb3bb04815",
|
||||
"localFolderPath": "$ALIYUNPAN_LOCAL_DIR",
|
||||
"panFolderPath": "$ALIYUNPAN_PAN_DIR",
|
||||
"mode": "$ALIYUNPAN_SYNC_MODE",
|
||||
"priority": "$ALIYUNPAN_SYNC_PRIORITY",
|
||||
"policy": "$ALIYUNPAN_SYNC_POLICY",
|
||||
"driveName": "$ALIYUNPAN_SYNC_DRIVE",
|
||||
"lastSyncTime": "2022-06-12 19:28:20"
|
||||
}
|
||||
]
|
||||
@@ -30,13 +31,13 @@ fi
|
||||
sleep 2s
|
||||
|
||||
# device-id
|
||||
if [[ -z $ALIYUNPAN_DEVICE_ID ]];
|
||||
then
|
||||
echo "the program use random device id"
|
||||
else
|
||||
echo "set device id"
|
||||
./aliyunpan config set -device_id ${ALIYUNPAN_DEVICE_ID}
|
||||
fi
|
||||
#if [[ -z $ALIYUNPAN_DEVICE_ID ]];
|
||||
#then
|
||||
# echo "the program use random device id"
|
||||
#else
|
||||
# echo "set device id"
|
||||
# ./aliyunpan config set -device_id ${ALIYUNPAN_DEVICE_ID}
|
||||
#fi
|
||||
|
||||
# check login already or not
|
||||
./aliyunpan who
|
||||
@@ -44,12 +45,10 @@ if [ $? -eq 0 ]
|
||||
then
|
||||
echo "cache token is valid, not need to re-login"
|
||||
else
|
||||
echo "login use refresh token: ${ALIYUNPAN_REFRESH_TOKEN}"
|
||||
./aliyunpan login -RefreshToken=${ALIYUNPAN_REFRESH_TOKEN}
|
||||
echo "token is invalid, please use the valid aliyunpan_config.json file and retry"
|
||||
# ./aliyunpan login -RefreshToken=${ALIYUNPAN_REFRESH_TOKEN}
|
||||
fi
|
||||
|
||||
./aliyunpan config set -transfer_url_type ${ALIYUNPAN_TRANSFER_URL_TYPE}
|
||||
|
||||
if [ "$ALIYUNPAN_SYNC_LOG" = "true" ]
|
||||
then
|
||||
./aliyunpan config set -file_record_config 1
|
||||
@@ -57,4 +56,4 @@ else
|
||||
./aliyunpan config set -file_record_config 2
|
||||
fi
|
||||
|
||||
./aliyunpan sync start -dp ${ALIYUNPAN_DOWNLOAD_PARALLEL} -up ${ALIYUNPAN_UPLOAD_PARALLEL} -dbs ${ALIYUNPAN_DOWNLOAD_BLOCK_SIZE} -ubs ${ALIYUNPAN_UPLOAD_BLOCK_SIZE} -log ${ALIYUNPAN_SYNC_LOG} -ldt ${ALIYUNPAN_LOCAL_DELAY_TIME} -step ${ALIYUNPAN_TASK_STEP}
|
||||
./aliyunpan sync start -dp ${ALIYUNPAN_DOWNLOAD_PARALLEL} -up ${ALIYUNPAN_UPLOAD_PARALLEL} -dbs ${ALIYUNPAN_DOWNLOAD_BLOCK_SIZE} -ubs ${ALIYUNPAN_UPLOAD_BLOCK_SIZE} -log ${ALIYUNPAN_SYNC_LOG} -ldt ${ALIYUNPAN_LOCAL_DELAY_TIME} -cycle ${ALIYUNPAN_SYNC_CYCLE}
|
||||
|
||||
@@ -6,17 +6,17 @@ services:
|
||||
container_name: aliyunpan-sync
|
||||
restart: always
|
||||
volumes:
|
||||
# (必须)映射的本地目录
|
||||
- ./data:/home/app/data:rw
|
||||
# (可选)可以指定JS插件sync_handler.js用于过滤文件,详见插件说明
|
||||
#- ./plugin/js/sync_handler.js:/home/app/config/plugin/js/sync_handler.js
|
||||
# (推荐)挂载sync_drive同步数据库到本地,这样即使容器销毁,同步数据库还可以用于以后使用
|
||||
#- ./sync_drive:/home/app/config/sync_drive
|
||||
# (必须)映射token凭据文件
|
||||
- /your/file/path/for/aliyunpan_config.json:/home/app/config/aliyunpan_config.json
|
||||
environment:
|
||||
# 时区,东8区
|
||||
- TZ=Asia/Shanghai
|
||||
# refresh token
|
||||
- ALIYUNPAN_REFRESH_TOKEN=8bf31a...7628a8ad5a0975bf2d9
|
||||
# 上传下载链接类型:1-默认 2-阿里ECS环境
|
||||
- ALIYUNPAN_TRANSFER_URL_TYPE=1
|
||||
# 下载文件并发数
|
||||
- ALIYUNPAN_DOWNLOAD_PARALLEL=2
|
||||
# 上传文件并发数
|
||||
@@ -25,17 +25,17 @@ services:
|
||||
- ALIYUNPAN_DOWNLOAD_BLOCK_SIZE=1024
|
||||
# 上传数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
|
||||
- ALIYUNPAN_UPLOAD_BLOCK_SIZE=10240
|
||||
# 指定网盘文件夹作为备份目录,不要指定根目录
|
||||
# 指定网盘文件夹作为备份目标目录,不要指定根目录
|
||||
- ALIYUNPAN_PAN_DIR=/my_sync_dir
|
||||
# 备份模式:upload(备份本地文件到云盘), download(备份云盘文件到本地), sync(双向同步备份,慎用!!!)
|
||||
# 备份模式:upload(备份本地文件到云盘), download(备份云盘文件到本地)
|
||||
- ALIYUNPAN_SYNC_MODE=upload
|
||||
# 优先级,只对双向同步备份模式有效。选项支持三种: time-时间优先,local-本地优先,pan-网盘优先
|
||||
- ALIYUNPAN_SYNC_PRIORITY=time
|
||||
# 备份策略: exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)
|
||||
- ALIYUNPAN_SYNC_POLICY=increment
|
||||
# 备份周期, 支持两种: infinity(永久循环备份),onetime(只运行一次备份)
|
||||
- ALIYUNPAN_SYNC_CYCLE=infinity
|
||||
# 网盘:backup(备份盘), resource(资源盘)
|
||||
- ALIYUNPAN_SYNC_DRIVE=backup
|
||||
# 是否显示文件备份过程日志,true-显示,false-不显示
|
||||
- ALIYUNPAN_SYNC_LOG=true
|
||||
# 本地文件修改检测延迟间隔,单位秒。如果本地文件会被频繁修改,例如录制视频文件,配置好该时间可以避免上传未录制好的文件
|
||||
- ALIYUNPAN_LOCAL_DELAY_TIME=3
|
||||
# 任务步骤, 支持两种: scan(只扫描并建立同步数据库),sync(正常启动同步任务)
|
||||
- ALIYUNPAN_TASK_STEP=sync
|
||||
# 登录设备的ID,默认为空,程序会自动生成一个ID
|
||||
- ALIYUNPAN_DEVICE_ID=
|
||||
- ALIYUNPAN_LOCAL_DELAY_TIME=3
|
||||
@@ -1,59 +0,0 @@
|
||||
# alpine:3.15.0
|
||||
|
||||
# 参数
|
||||
ARG DOCKER_IMAGE_HASH
|
||||
|
||||
FROM alpine@sha256:$DOCKER_IMAGE_HASH
|
||||
LABEL author="tickstep"
|
||||
LABEL email="tickstep@outlook.com"
|
||||
LABEL version="1.0"
|
||||
LABEL description="webdav file server for aliyun cloud drive"
|
||||
|
||||
# 时区
|
||||
ENV TZ=Asia/Shanghai
|
||||
# 手动下载tzdata安装包,注意要下载对应架构的: https://dl-cdn.alpinelinux.org/alpine/v3.15/community/
|
||||
RUN apk add -U tzdata
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
# 创建运行目录
|
||||
RUN mkdir -p /home/app
|
||||
VOLUME /home/app
|
||||
WORKDIR /home/app
|
||||
RUN cd /home/app
|
||||
|
||||
# 创建配置文件目录
|
||||
RUN mkdir -p /home/app/config
|
||||
|
||||
# 复制文件
|
||||
COPY ./docker/webdav/app.sh /home/app/app.sh
|
||||
RUN chmod +x /home/app/app.sh
|
||||
COPY ./docker/webdav/health_check.sh /home/app/health_check.sh
|
||||
RUN chmod +x /home/app/health_check.sh
|
||||
|
||||
COPY ./out/binary_files/aliyunpan /home/app
|
||||
RUN mkdir -p /home/app/config/plugin
|
||||
COPY ./out/binary_files/plugin /home/app/config/plugin
|
||||
RUN mkdir -p /home/app/config/sync_drive
|
||||
COPY ./out/binary_files/sync_drive /home/app/config/sync_drive
|
||||
#RUN chmod +x /home/app/aliyunpan
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --start-period=5s --interval=10s --timeout=5s --retries=3 CMD /bin/sh /home/app/health_check.sh
|
||||
|
||||
# 端口
|
||||
EXPOSE 23077
|
||||
|
||||
# 环境变量
|
||||
ENV ALIYUNPAN_DOCKER=1
|
||||
ENV ALIYUNPAN_CONFIG_DIR=/home/app/config
|
||||
ENV ALIYUNPAN_AUTH_USER=admin
|
||||
ENV ALIYUNPAN_AUTH_PASSWORD=admin
|
||||
ENV ALIYUNPAN_WEBDAV_MODE=rw
|
||||
ENV ALIYUNPAN_PAN_DRIVE=File
|
||||
ENV ALIYUNPAN_PAN_DIR=/
|
||||
ENV ALIYUNPAN_REFRESH_TOKEN=""
|
||||
ENV ALIYUNPAN_TRANSFER_URL_TYPE=0
|
||||
ENV ALIYUNPAN_BLOCK_SIZE=10240
|
||||
ENV ALIYUNPAN_DEVICE_ID=""
|
||||
|
||||
ENTRYPOINT ./app.sh
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/bin/sh
|
||||
cd /home/app
|
||||
chmod +x ./aliyunpan
|
||||
|
||||
# device-id
|
||||
if [[ -z $ALIYUNPAN_DEVICE_ID ]];
|
||||
then
|
||||
echo "the program use random device id"
|
||||
else
|
||||
echo "set device id"
|
||||
./aliyunpan config set -device_id ${ALIYUNPAN_DEVICE_ID}
|
||||
fi
|
||||
|
||||
# check login already or not
|
||||
./aliyunpan who
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
echo "cache token is valid, not need to re-login"
|
||||
else
|
||||
echo "login use refresh token: ${ALIYUNPAN_REFRESH_TOKEN}"
|
||||
./aliyunpan login -RefreshToken=${ALIYUNPAN_REFRESH_TOKEN}
|
||||
fi
|
||||
|
||||
./aliyunpan config set -transfer_url_type ${ALIYUNPAN_TRANSFER_URL_TYPE}
|
||||
./aliyunpan webdav start -ip "0.0.0.0" -port 23077 -webdav_user "${ALIYUNPAN_AUTH_USER}" -webdav_password "${ALIYUNPAN_AUTH_PASSWORD}" -webdav_mode "${ALIYUNPAN_WEBDAV_MODE}" -pan_drive "${ALIYUNPAN_PAN_DRIVE}" -pan_dir_path "${ALIYUNPAN_PAN_DIR}" -bs ${ALIYUNPAN_BLOCK_SIZE}
|
||||
@@ -1,33 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
webdav:
|
||||
image: tickstep/aliyunpan-webdav:v0.1.6
|
||||
container_name: aliyunpan-webdav
|
||||
restart: always
|
||||
ports:
|
||||
- 23077:23077
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
# refresh token
|
||||
- ALIYUNPAN_REFRESH_TOKEN=4dd40e8b165...42b8833e4a507b6
|
||||
# webdav 登录用户名
|
||||
- ALIYUNPAN_AUTH_USER=admin
|
||||
# webdav 登录密码
|
||||
- ALIYUNPAN_AUTH_PASSWORD=admin
|
||||
# webdav 网盘模式,支持:rw-读写,ro-只读
|
||||
- ALIYUNPAN_WEBDAV_MODE=rw
|
||||
# 指定网盘类型为文件,可选: File-文件 Album-相册
|
||||
- ALIYUNPAN_PAN_DRIVE=File
|
||||
# 指定网盘文件夹作为webdav服务根目录
|
||||
- ALIYUNPAN_PAN_DIR=/
|
||||
# 上传下载链接类型:1-默认 2-阿里ECS环境
|
||||
- ALIYUNPAN_TRANSFER_URL_TYPE=1
|
||||
# 上传数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
|
||||
- ALIYUNPAN_BLOCK_SIZE=10240
|
||||
# 登录设备的ID,默认为空,程序会自动生成一个ID
|
||||
- ALIYUNPAN_DEVICE_ID=
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 检查webdav的服务端口是否存在
|
||||
netstat -luntp | awk '{print $4}' | grep 23077
|
||||
if [ $? == 0 ]
|
||||
then
|
||||
echo $?
|
||||
exit 0
|
||||
else
|
||||
echo $?
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
* [获取当前帐号](#获取当前帐号)
|
||||
* [切换阿里云盘帐号](#切换阿里云盘帐号)
|
||||
* [退出阿里云盘帐号](#退出阿里云盘帐号)
|
||||
* [刷新Token](#刷新Token)
|
||||
* [切换网盘(备份盘/资源库/相册)](#切换网盘)
|
||||
* [切换网盘(备份盘/资源库)](#切换网盘)
|
||||
* [获取网盘配额](#获取网盘配额)
|
||||
* [切换工作目录](#切换工作目录)
|
||||
* [输出工作目录](#输出工作目录)
|
||||
@@ -24,9 +23,9 @@
|
||||
* [重命名文件/目录](#重命名文件目录)
|
||||
* [分享文件/目录](#分享文件目录)
|
||||
+ [设置分享文件/目录](#设置分享文件目录)
|
||||
+ [创建快传链接](#创建快传链接)
|
||||
+ [列出已分享文件/目录](#列出已分享文件目录)
|
||||
+ [取消分享文件/目录](#取消分享文件目录)
|
||||
+ [分享秒传链接](#分享秒传链接)
|
||||
* [同步备份功能](#同步备份功能)
|
||||
+ [常用命令说明](#常用命令说明)
|
||||
+ [备份配置文件说明](#备份配置文件说明)
|
||||
@@ -45,8 +44,7 @@
|
||||
+ [5.Token刷新失败发送外部通知](#5.Token刷新失败发送外部通知)
|
||||
* [显示和修改程序配置项](#显示和修改程序配置项)
|
||||
- [常见问题Q&A](#常见问题Q&A)
|
||||
* [1. 如何获取RefreshToken](#1-如何获取RefreshToken)
|
||||
* [2. 如何开启Debug调试日志](#2-如何开启Debug调试日志)
|
||||
* [1. 如何开启Debug调试日志](#1-如何开启Debug调试日志)
|
||||
|
||||
# 命令列表及说明
|
||||
## 注意
|
||||
@@ -86,25 +84,32 @@ aliyunpan help login
|
||||
## 登录阿里云盘帐号
|
||||
|
||||
### 登录
|
||||
当前支持使用RefreshToken进行登录。RefreshToken请参考 [1. 如何获取RefreshToken](#1-如何获取RefreshToken) 获取
|
||||
当前支持在浏览器进行登录,请输入以下命令按照步骤进行即可。
|
||||
```
|
||||
aliyunpan login
|
||||
```
|
||||
|
||||
### 例子
|
||||
```
|
||||
按照引导步骤登录
|
||||
aliyunpan login
|
||||
请输入RefreshToken, 回车键提交 > 626a27b6193f4c5ca6ef0.......
|
||||
|
||||
命令行指定RefreshToken登录
|
||||
aliyunpan login -RefreshToken=626a27b6193f4c5ca6ef0.......
|
||||
|
||||
使用二维码方式进行登录,按照引导步骤进行
|
||||
aliyunpan login -QrCode
|
||||
aliyunpan > login
|
||||
请在浏览器打开以下链接进行登录,链接有效时间为5分钟。
|
||||
注意:你需要进行一次授权一次扫码的两次登录。
|
||||
https://openapi.alipan.com/oauth/authorize?client_id=cf9f70e8fc61430f8ec5ab5cadf31375&redirect_uri=https%3A%2F%2Fapi.tickstep.com%2Fauth%2Ftickstep%2Faliyunpan%2Ftoken%2Fopenapi%2F8206f0.....fb5db6b40336%2Fauth&scope=user:base,file:all:read,file:all:write
|
||||
阿里云盘登录成功: tickstep
|
||||
aliyunpan:/ tickstep$
|
||||
```
|
||||
目前登录全部采用网页登录,由于 aliyunpan 融合了阿里官方Open接口+网页Web端接口,所以你需要进行两次登录。输入 login 命令
|
||||

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

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

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

|
||||
|
||||
## 列出帐号列表
|
||||
|
||||
```
|
||||
@@ -140,25 +145,8 @@ aliyunpan logout
|
||||
|
||||
程序会进一步确认退出帐号, 防止误操作.
|
||||
|
||||
## 刷新Token
|
||||
由于阿里云盘的RefreshToken是会过期的,为了延长最大过期时间,需要定期刷新Token,建议每小时刷新一次。
|
||||
调用该命令可以自动刷新RefreshToken并保存到配置文件中,但是有一个前提,即Token必须还没有过期,如果Token已经过期是无法刷新的则只能重新登录。
|
||||
```
|
||||
刷新当前登录用户
|
||||
aliyunpan token update
|
||||
|
||||
刷新所有登录的用户
|
||||
aliyunpan token update -mode 2
|
||||
```
|
||||
|
||||
如果你的aliyunpan工具是在Linux中运行,则建议你使用crontab定时任务进行Token自动刷新,例如
|
||||
```
|
||||
每小时执行一次Token刷新任务
|
||||
*/60 * * * * /<your path>/aliyunpan token update -mode 2
|
||||
```
|
||||
|
||||
## 切换网盘
|
||||
程序默认工作在文件网盘下,如需切换到相册网盘,可以使用本命令进行切换。
|
||||
程序默认工作在文件网盘下,如需切换到资源库网盘,可以使用本命令进行切换。
|
||||
```
|
||||
aliyunpan drive <driveId>
|
||||
```
|
||||
@@ -226,11 +214,6 @@ aliyunpan ll /我的文档
|
||||
```
|
||||
|
||||
## 下载文件/目录
|
||||
下载支持两种链接类型:1-默认类型 2-阿里ECS环境类型
|
||||
在普通网络下,下载速度可以达到10MB/s,在阿里ECS(必须是"经典网络"类型的机器)环境下,下载速度单文件可以轻松达到20MB/s,多文件可以达到100MB/s
|
||||

|
||||

|
||||
|
||||
```
|
||||
aliyunpan download <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
|
||||
aliyunpan d <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
|
||||
@@ -274,10 +257,6 @@ aliyunpan d /我的文档
|
||||
自动跳过下载重名的文件!
|
||||
|
||||
## 上传文件/目录
|
||||
上传支持两种链接类型:1-默认类型 2-阿里ECS环境类型
|
||||
在阿里ECS(必须是"经典网络"类型的机器)环境下,上传速度单文件可以轻松达到30MB/s,多文件可以达到100MB/s
|
||||

|
||||
|
||||
```
|
||||
aliyunpan upload <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>
|
||||
aliyunpan u <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>
|
||||
@@ -311,22 +290,6 @@ aliyunpan upload -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视
|
||||
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
|
||||
```
|
||||
|
||||
## 手动秒传上传文件
|
||||
通过秒传链接上传文件到网盘,秒传链接可以通过share命令获取
|
||||
```
|
||||
aliyunpan rapidupload <秒传链接1> <秒传链接2> <秒传链接3> ...
|
||||
```
|
||||
|
||||
### 例子:
|
||||
```
|
||||
# 如果秒传成功, 则保存到网盘路径 /file.dmg
|
||||
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|"
|
||||
|
||||
# 如果秒传成功, 则保存到网盘路径 /pan_folder/file.dmg
|
||||
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder"
|
||||
|
||||
```
|
||||
|
||||
## 创建目录
|
||||
```
|
||||
aliyunpan mkdir <目录>
|
||||
@@ -410,55 +373,44 @@ aliyunpan share
|
||||
```
|
||||
|
||||
### 设置分享文件/目录
|
||||
阿里目前之支持少数文件类型的分享,不支持的文件分享会提示分享失败
|
||||
阿里目前只支持少数文件类型的分享,不支持的文件分享会提示分享失败
|
||||
```
|
||||
aliyunpan share set <文件/目录1> <文件/目录2> ...
|
||||
aliyunpan share s <文件/目录1> <文件/目录2> ...
|
||||
aliyunpan share set -mode 1 <文件/目录1> <文件/目录2> ...
|
||||
```
|
||||
|
||||
### 创建快传链接
|
||||
阿里的快传支持大部分文件的共享,例如zip压缩包,按照如下方式可以创建快传链接
|
||||
```
|
||||
aliyunpan share set -mode 3 <文件/目录1> <文件/目录2> ...
|
||||
```
|
||||
|
||||
### 列出已分享文件/目录
|
||||
```
|
||||
aliyunpan share list
|
||||
aliyunpan share l
|
||||
```
|
||||
|
||||
### 取消分享文件/目录
|
||||
```
|
||||
aliyunpan share cancel <shareid_1> <shareid_2> ...
|
||||
aliyunpan share c <shareid_1> <shareid_2> ...
|
||||
```
|
||||
目前只支持通过分享id (shareid) 来取消分享.
|
||||
|
||||
|
||||
### 分享秒传链接
|
||||
秒传链接支持所有类型的文件分享,可以突破阿里的分享限制。得到的链接可以使用import或者rapidupload命令保存到自己网盘中。
|
||||
秒传链接只支持文件分享,不支持文件夹。如果指定文件夹会创建文件夹下所有文件的秒传链接。
|
||||
```
|
||||
aliyunpan share mc <文件/目录1> <文件/目录2> ...
|
||||
|
||||
例子
|
||||
# 创建文件 1.mp4 的秒传链接
|
||||
aliyunpan share mc 1.mp4
|
||||
|
||||
# 创建文件 1.mp4 的秒传链接,但链接隐藏相对路径
|
||||
aliyunpan share mc -hp 1.mp4
|
||||
|
||||
# 创建文件夹 share_folder 下面所有文件的秒传链接
|
||||
aliyunpan share mc share_folder/
|
||||
```
|
||||
|
||||
## 同步备份功能
|
||||
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份三种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地两种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
指定本地目录和对应的一个网盘目录,以备份文件。网盘目录必须和本地目录独占使用,不要用作其他用途,不然备份可能会有问题。
|
||||
|
||||
备份功能支持以下三种模式:
|
||||
|
||||
备份功能支持以下模式:
|
||||
1. 备份本地文件,即上传本地文件到网盘,始终保持本地文件有一个完整的备份在网盘
|
||||
2. 备份云盘文件,即下载网盘文件到本地,始终保持网盘的文件有一个完整的备份在本地
|
||||
3. 双向备份,保持网盘文件和本地文件严格一致
|
||||
|
||||
备份功能一般用于NAS等系统,进行文件备份。比如备份照片,就可以使用这个功能定期备份照片到云盘,十分好用。
|
||||
|
||||
***如果你同步目录下有非常多的文件,最好在首次备份前先运行一次scan任务,等scan任务完成并建立起同步数据库后,再正常启动同步任务。这样同步任务可以更加快速并且能有效避免同步重复文件。***
|
||||
|
||||
备份功能支持指定备份策略:
|
||||
1. exclusive,排他备份文件,目标目录多余的文件会被删除。保证备份的源目录,和目标目录文件一比一备份。源目录文件如果文件被删除,则对应的目标目录的文件也会被删除。
|
||||
2. increment,增量备份文件,目标目录多余的文件不会被删除。只会把源目录修改的文件,新增的文件备份到目标目录。如果源目录有文件删除,或者目标目录有其他文件新增是不会被删除。
|
||||
|
||||
备份功能一般用于NAS等系统,进行文件备份。比如备份照片,就可以使用这个功能定期备份照片到云盘。
|
||||
|
||||
同步的基本逻辑如下所示,一次循环包括:扫描-对比-执行,一共三个环节。
|
||||

|
||||
|
||||
### 常用命令说明
|
||||
```
|
||||
@@ -483,10 +435,6 @@ aliyunpan sync start
|
||||
|
||||
使用配置文件启动同步备份服务,并配置下载并发为2,上传并发为1,下载分片大小为256KB,上传分片大小为1MB
|
||||
aliyunpan sync start -dp 2 -up 1 -dbs 256 -ubs 1024
|
||||
|
||||
当你本地同步目录文件非常多,或者云盘同步目录文件非常多,为了后期更快更精准同步文件,可以先进行文件扫描并构建同步数据库,然后再正常启动同步任务。如下所示:
|
||||
aliyunpan sync start -step scan
|
||||
aliyunpan sync start
|
||||
```
|
||||
|
||||
### 备份配置文件说明
|
||||
@@ -502,13 +450,15 @@ aliyunpan sync start
|
||||
"name": "设计文档备份",
|
||||
"localFolderPath": "D:/tickstep/Documents/设计文档",
|
||||
"panFolderPath": "/备份盘/我的文档",
|
||||
"mode": "upload"
|
||||
"mode": "upload",
|
||||
"driveName": "backup"
|
||||
},
|
||||
{
|
||||
"name": "手机图片备份",
|
||||
"localFolderPath": "D:/tickstep/Photos/手机图片",
|
||||
"panFolderPath": "/备份盘/手机图片",
|
||||
"mode": "upload"
|
||||
"mode": "upload",
|
||||
"driveName": "resource"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -517,7 +467,8 @@ aliyunpan sync start
|
||||
name - 任务名称
|
||||
localFolderPath - 本地目录
|
||||
panFolderPath - 网盘目录
|
||||
mode - 模式,支持三种: upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)
|
||||
mode - 模式,支持: upload(备份本地文件到云盘),download(备份云盘文件到本地)
|
||||
driveName - 网盘,支持:backup(备份盘), resource(资源盘)
|
||||
```
|
||||
|
||||
### 命令行启动
|
||||
@@ -525,12 +476,13 @@ mode - 模式,支持三种: upload(备份本地文件到云盘),download(备
|
||||
|
||||
```
|
||||
使用命令行配置启动同步备份服务,将本地目录 /tickstep/Documents/设计文档 中的文件备份上传到云盘目录 /备份盘/我的文档
|
||||
./aliyunpan sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload"
|
||||
./aliyunpan sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload" -drive "backup"
|
||||
|
||||
参数说明
|
||||
ldir:本地目录
|
||||
pdir:云盘目录
|
||||
mode:备份模式,支持:upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)
|
||||
mode:备份模式,支持:upload(备份本地文件到云盘),download(备份云盘文件到本地)
|
||||
drive - 网盘,支持:backup(备份盘), resource(资源盘)
|
||||
|
||||
--------------------------------------------------------------
|
||||
正常会有以下的输出:
|
||||
@@ -560,15 +512,9 @@ cd /path/to/aliyunpan/folder
|
||||
|
||||
chmod +x ./aliyunpan
|
||||
|
||||
# 指定refresh token用于登录
|
||||
./aliyunpan login -RefreshToken=9078907....adg9087
|
||||
|
||||
# 上传下载链接类型:1-默认 2-阿里ECS环境
|
||||
./aliyunpan config set -transfer_url_type 1
|
||||
|
||||
# 指定配置参数并进行启动
|
||||
# 支持的模式:upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)
|
||||
./aliyunpan sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload"
|
||||
# 支持的模式:upload(备份本地文件到云盘),download(备份云盘文件到本地)
|
||||
./aliyunpan sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload" -drive "backup"
|
||||
```
|
||||
|
||||
增加脚本执行权限
|
||||
@@ -599,8 +545,8 @@ $ nohup ./sync.sh >/dev/null 2>&1 &
|
||||
<log mode="roll"></log>
|
||||
</service>
|
||||
```
|
||||
4. 将alisync.exe和alisync.xml存放在你PC上aliyunpan.exe工具所在目录,例如:
|
||||

|
||||
4. 将alisync.exe和alisync.xml存放在你PC上aliyunpan.exe工具所在目录,例如:
|
||||

|
||||
5. CMD命令行启动程序
|
||||
```
|
||||
# 安装服务(只需要第一次安装,后面不用再安装)
|
||||
@@ -628,14 +574,15 @@ D:\Program Files\aliyunpan>alisync stop
|
||||
|
||||
1. 直接运行
|
||||
```
|
||||
docker run -d --name=aliyunpan-sync --restart=always -v "<your local dir>:/home/app/data" -e TZ="Asia/Shanghai" -e ALIYUNPAN_REFRESH_TOKEN="<your refreshToken>" -e ALIYUNPAN_PAN_DIR="<your drive pan dir>" -e ALIYUNPAN_SYNC_MODE="upload" -e ALIYUNPAN_TASK_STEP="sync" tickstep/aliyunpan-sync:<tag>
|
||||
|
||||
docker run -d --name=aliyunpan-sync --restart=always -v "<your aliyunpan_config.json>:/home/app/config/aliyunpan_config.json" -v "<your local dir>:/home/app/data" -e ALIYUNPAN_PAN_DIR="<your drive pan dir>" -e ALIYUNPAN_SYNC_MODE="upload" -e ALIYUNPAN_SYNC_POLICY="increment" -e ALIYUNPAN_SYNC_DRIVE="backup" -e ALIYUNPAN_SYNC_LOG="true" tickstep/aliyunpan-sync:<tag>
|
||||
|
||||
<your local dir>:本地目录绝对路径,例如:/tickstep/Documents/设计文档
|
||||
ALIYUNPAN_PAN_DIR:云盘目录
|
||||
ALIYUNPAN_REFRESH_TOKEN:RefreshToken
|
||||
ALIYUNPAN_SYNC_MODE:备份模式,支持三种: upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)
|
||||
ALIYUNPAN_TASK_STEP:任务步骤, 支持两种: scan(只扫描并建立同步数据库),sync(正常启动同步任务)。如果你同步目录文件非常多,首次运行最好先跑一次scan步骤,然后再正常启动文件同步任务
|
||||
<your aliyunpan_config.json>: 用户已经登录成功并保存好的aliyunpan_config.json凭据文件
|
||||
<your local dir>:本地目标目录,绝对路径,例如:/tickstep/Documents/设计文档
|
||||
ALIYUNPAN_PAN_DIR:云盘目标目录,绝对路径
|
||||
ALIYUNPAN_SYNC_MODE:备份模式,支持: upload(备份本地文件到云盘),download(备份云盘文件到本地)
|
||||
ALIYUNPAN_SYNC_POLICY:备份策略,支持:exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)
|
||||
ALIYUNPAN_SYNC_DRIVE: 网盘,支持:backup(备份盘), resource(资源盘)
|
||||
ALIYUNPAN_SYNC_LOG: 同步日志,true-开启同步日志显示,false-关闭同步日志
|
||||
```
|
||||
|
||||
2. docker-compose运行
|
||||
@@ -649,18 +596,17 @@ services:
|
||||
container_name: aliyunpan-sync
|
||||
restart: always
|
||||
volumes:
|
||||
# 指定本地备份目录绝对路径:/tickstep/Documents/设计文档
|
||||
- /tickstep/Documents/设计文档:/home/app/data:rw
|
||||
# (可选)可以指定JS插件sync_handler.js用于过滤文件,详见下面的插件说明
|
||||
# (必须)映射的本地目录
|
||||
- ./data:/home/app/data:rw
|
||||
# (可选)可以指定JS插件sync_handler.js用于过滤文件,详见插件说明
|
||||
#- ./plugin/js/sync_handler.js:/home/app/config/plugin/js/sync_handler.js
|
||||
# (推荐)挂载sync_drive同步数据库到本地
|
||||
# (推荐)挂载sync_drive同步数据库到本地,这样即使容器销毁,同步数据库还可以用于以后使用
|
||||
#- ./sync_drive:/home/app/config/sync_drive
|
||||
# (必须)映射token凭据文件
|
||||
- /your/file/path/for/aliyunpan_config.json:/home/app/config/aliyunpan_config.json
|
||||
environment:
|
||||
# 时区,东8区
|
||||
- TZ=Asia/Shanghai
|
||||
# refresh token
|
||||
- ALIYUNPAN_REFRESH_TOKEN=41804446a...bf7f069cab2
|
||||
# 上传下载链接类型:1-默认 2-阿里ECS环境
|
||||
- ALIYUNPAN_TRANSFER_URL_TYPE=1
|
||||
# 下载文件并发数
|
||||
- ALIYUNPAN_DOWNLOAD_PARALLEL=2
|
||||
# 上传文件并发数
|
||||
@@ -669,18 +615,20 @@ services:
|
||||
- ALIYUNPAN_DOWNLOAD_BLOCK_SIZE=1024
|
||||
# 上传数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
|
||||
- ALIYUNPAN_UPLOAD_BLOCK_SIZE=10240
|
||||
# 指定网盘文件夹作为备份目录,不要指定根目录
|
||||
- ALIYUNPAN_PAN_DIR=/备份盘/我的文档
|
||||
# 备份模式:upload(备份本地文件到云盘), download(备份云盘文件到本地), sync(双向同步备份)
|
||||
# 指定网盘文件夹作为备份目标目录,不要指定根目录
|
||||
- ALIYUNPAN_PAN_DIR=/my_sync_dir
|
||||
# 备份模式:upload(备份本地文件到云盘), download(备份云盘文件到本地)
|
||||
- ALIYUNPAN_SYNC_MODE=upload
|
||||
# 优先级,只对双向同步备份模式有效。选项支持三种: time-时间优先,local-本地优先,pan-网盘优先
|
||||
- ALIYUNPAN_SYNC_PRIORITY=time
|
||||
# 备份策略: exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)
|
||||
- ALIYUNPAN_SYNC_POLICY=increment
|
||||
# 备份周期, 支持两种: infinity(永久循环备份),onetime(只运行一次备份)
|
||||
- ALIYUNPAN_SYNC_CYCLE=infinity
|
||||
# 网盘:backup(备份盘), resource(资源盘)
|
||||
- ALIYUNPAN_SYNC_DRIVE=backup
|
||||
# 是否显示文件备份过程日志,true-显示,false-不显示
|
||||
- ALIYUNPAN_SYNC_LOG=true
|
||||
# 本地文件修改检测延迟间隔,单位秒。如果本地文件会被频繁修改,例如录制视频文件,配置好该时间可以避免上传未录制好的文件
|
||||
- ALIYUNPAN_LOCAL_DELAY_TIME=3
|
||||
# 任务步骤, 支持两种: scan(只扫描并建立同步数据库),sync(正常启动同步任务)
|
||||
- ALIYUNPAN_TASK_STEP=sync
|
||||
```
|
||||
|
||||
3. sync_handler.js插件说明
|
||||
@@ -853,8 +801,8 @@ function syncScanPanFilePrepareCallback(context, params) {
|
||||
7.下载的文件进行改名,但是网盘的文件保持不变
|
||||
8.下载的文件路径进行更改,但是网盘的文件保持不变
|
||||
9.下载文件完成后,通过HTTP通知其他服务
|
||||
10.同步备份功能,支持过滤本地文件,或者过滤云盘文件。定制上传或者下载需要同步的文件
|
||||
11.Token刷新失败或者过期,发送外部通知(HTTP&邮件)
|
||||
10.同步备份功能,支持过滤本地文件,或者过滤云盘文件。定制上传或者下载需要同步的文件
|
||||
11.Token刷新失败或者过期,发送外部通知(HTTP&邮件)
|
||||
|
||||
### 如何使用
|
||||
JS插件的样本文件默认存放在程序所在的plugin/js文件夹下,分为下载(download_handler.js.sample)、上传(upload_handler.js.sample)、同步备份(sync_handler.js.sample)、用户Token(token_handler.js.sample)插件。
|
||||
@@ -1215,25 +1163,10 @@ aliyunpan config set -max_download_parallel 15
|
||||
|
||||
# 组合设置
|
||||
aliyunpan config set -max_download_parallel 15 -savedir D:/Downloads
|
||||
|
||||
# 设置使用阿里云内部URL链接,专供阿里云ECS环境使用
|
||||
# 开启内部URL链接可以使用阿里云ECS私网带宽流量,而不用使用宝贵的公网带宽流量,如果你在阿里ECS环境中使用本工具,建议开启
|
||||
aliyunpan config set -transfer_url_type 2
|
||||
```
|
||||
|
||||
# 常见问题Q&A
|
||||
## 1 如何获取RefreshToken
|
||||
需要通过浏览器获取refresh_token。这里以Chrome浏览器为例,其他浏览器类似。
|
||||
打开 [阿里云盘网页](https://www.aliyundrive.com/drive) 并进行登录,然后F12按键打开浏览器调试菜单,按照下面步骤进行
|
||||

|
||||
|
||||
或者直接在控制台输入以下命令获取
|
||||
```
|
||||
JSON.parse(localStorage.getItem("token")).refresh_token
|
||||
```
|
||||

|
||||
|
||||
## 2 如何开启Debug调试日志
|
||||
## 1 如何开启Debug调试日志
|
||||
当需要定位问题,或者提交issue的时候抓取log,则需要开启debug日志。步骤如下:
|
||||
|
||||
### 第一步
|
||||
|
||||
@@ -13,12 +13,10 @@ require (
|
||||
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce
|
||||
github.com/peterh/liner v1.2.1
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/tickstep/aliyunpan-api v0.1.8
|
||||
github.com/tickstep/aliyunpan-api v0.2.1
|
||||
github.com/tickstep/bolt v1.3.4
|
||||
github.com/tickstep/library-go v0.1.1
|
||||
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -32,6 +30,7 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/russross/blackfriday v1.5.2 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
)
|
||||
|
||||
@@ -98,8 +98,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tickstep/aliyunpan-api v0.1.8 h1:heK04T2F1K5eDSHDqHJ2yHA6RSBqyN/4yqYjHClbJvI=
|
||||
github.com/tickstep/aliyunpan-api v0.1.8/go.mod h1:SN+N5Vh3lLq3dVimlxKxviCl50gLGXZO96nHHc3qd2o=
|
||||
github.com/tickstep/aliyunpan-api v0.2.1 h1:bPJGnLR2uwWZ5xNInH2xvVRynlLN3fNGk7CZeM7+TbY=
|
||||
github.com/tickstep/aliyunpan-api v0.2.1/go.mod h1:SN+N5Vh3lLq3dVimlxKxviCl50gLGXZO96nHHc3qd2o=
|
||||
github.com/tickstep/bolt v1.3.4 h1:UN1txRsauOfR8a5mUWm5So0ng233PsIMVR5/+3WowCE=
|
||||
github.com/tickstep/bolt v1.3.4/go.mod h1:Y24RwCywbOsKAyHiVcJ4K2QQfED+q67x7PEOH9OG7Q4=
|
||||
github.com/tickstep/library-go v0.1.0/go.mod h1:uAHeNOIpoywCzlaeLrWmmRSupn03m9kJVZKOEmuarmA=
|
||||
@@ -117,8 +117,6 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
@@ -78,6 +79,10 @@ func CmdAlbum() cli.Command {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumList()
|
||||
return nil
|
||||
},
|
||||
@@ -102,6 +107,10 @@ func CmdAlbum() cli.Command {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumCreate(c.Args().Get(0), c.Args().Get(1))
|
||||
return nil
|
||||
},
|
||||
@@ -127,6 +136,10 @@ func CmdAlbum() cli.Command {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumDelete(c.Args())
|
||||
return nil
|
||||
},
|
||||
@@ -149,6 +162,10 @@ func CmdAlbum() cli.Command {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumRename(c.Args().Get(0), c.Args().Get(1))
|
||||
return nil
|
||||
},
|
||||
@@ -171,6 +188,10 @@ func CmdAlbum() cli.Command {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumListFile(c.Args().Get(0))
|
||||
return nil
|
||||
},
|
||||
@@ -193,6 +214,10 @@ func CmdAlbum() cli.Command {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
if len(subArgs) < 2 {
|
||||
fmt.Println("请指定移除的文件")
|
||||
@@ -223,6 +248,10 @@ func CmdAlbum() cli.Command {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
if len(subArgs) < 2 {
|
||||
fmt.Println("请指定增加的文件")
|
||||
@@ -251,6 +280,10 @@ func CmdAlbum() cli.Command {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
if len(subArgs) == 0 {
|
||||
fmt.Println("请指定下载的相簿名称")
|
||||
@@ -303,7 +336,7 @@ func CmdAlbum() cli.Command {
|
||||
|
||||
func RunAlbumList() {
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().AlbumListGetAll(&aliyunpan.AlbumListParam{})
|
||||
records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{})
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return
|
||||
@@ -326,7 +359,7 @@ func RunAlbumCreate(name, description string) {
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
_, err := activeUser.PanClient().AlbumCreate(&aliyunpan.AlbumCreateParam{
|
||||
_, err := activeUser.PanClient().WebapiPanClient().AlbumCreate(&aliyunpan_web.AlbumCreateParam{
|
||||
Name: name,
|
||||
Description: description,
|
||||
})
|
||||
@@ -344,7 +377,7 @@ func RunAlbumDelete(nameList []string) {
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().AlbumListGetAll(&aliyunpan.AlbumListParam{})
|
||||
records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{})
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return
|
||||
@@ -354,7 +387,7 @@ func RunAlbumDelete(nameList []string) {
|
||||
for i, name := range nameList {
|
||||
if name == record.Name {
|
||||
nameList = append(nameList[:i], nameList[i+1:]...)
|
||||
_, err := activeUser.PanClient().AlbumDelete(&aliyunpan.AlbumDeleteParam{
|
||||
_, err := activeUser.PanClient().WebapiPanClient().AlbumDelete(&aliyunpan_web.AlbumDeleteParam{
|
||||
AlbumId: record.AlbumId,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -369,8 +402,8 @@ func RunAlbumDelete(nameList []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func getAlbumFromName(activeUser *config.PanUser, name string) *aliyunpan.AlbumEntity {
|
||||
records, err := activeUser.PanClient().AlbumListGetAll(&aliyunpan.AlbumListParam{})
|
||||
func getAlbumFromName(activeUser *config.PanUser, name string) *aliyunpan_web.AlbumEntity {
|
||||
records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{})
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return nil
|
||||
@@ -399,7 +432,7 @@ func RunAlbumRename(name, newName string) {
|
||||
if record == nil {
|
||||
return
|
||||
}
|
||||
_, err := activeUser.PanClient().AlbumEdit(&aliyunpan.AlbumEditParam{
|
||||
_, err := activeUser.PanClient().WebapiPanClient().AlbumEdit(&aliyunpan_web.AlbumEditParam{
|
||||
AlbumId: record.AlbumId,
|
||||
Description: record.Description,
|
||||
Name: newName,
|
||||
@@ -424,7 +457,7 @@ func RunAlbumListFile(name string) {
|
||||
return
|
||||
}
|
||||
|
||||
fileList, er := activeUser.PanClient().AlbumListFileGetAll(&aliyunpan.AlbumListFileParam{
|
||||
fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{
|
||||
AlbumId: record.AlbumId,
|
||||
})
|
||||
if er != nil {
|
||||
@@ -450,14 +483,14 @@ func RunAlbumRmFile(name string, nameList []string) {
|
||||
return
|
||||
}
|
||||
|
||||
fileList, er := activeUser.PanClient().AlbumListFileGetAll(&aliyunpan.AlbumListFileParam{
|
||||
fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{
|
||||
AlbumId: album.AlbumId,
|
||||
})
|
||||
if er != nil {
|
||||
fmt.Printf("获取相簿文件列表失败:%s\n", er)
|
||||
return
|
||||
}
|
||||
param := &aliyunpan.AlbumDeleteFileParam{
|
||||
param := &aliyunpan_web.AlbumDeleteFileParam{
|
||||
AlbumId: album.AlbumId,
|
||||
DriveFileList: []aliyunpan.FileBatchActionParam{},
|
||||
}
|
||||
@@ -480,7 +513,7 @@ func RunAlbumRmFile(name string, nameList []string) {
|
||||
return
|
||||
}
|
||||
// delete file
|
||||
_, e := activeUser.PanClient().AlbumDeleteFile(param)
|
||||
_, e := activeUser.PanClient().WebapiPanClient().AlbumDeleteFile(param)
|
||||
if e != nil {
|
||||
fmt.Printf("删除相簿文件失败:%s\n", e)
|
||||
return
|
||||
@@ -513,13 +546,13 @@ func RunAlbumAddFile(albumName string, filePathList []string, filterOption Album
|
||||
}
|
||||
|
||||
fmt.Printf("正在获取增加的文件信息,该操作可能会非常耗费时间,请耐心等待...\n")
|
||||
param := &aliyunpan.AlbumAddFileParam{
|
||||
param := &aliyunpan_web.AlbumAddFileParam{
|
||||
AlbumId: album.AlbumId,
|
||||
DriveFileList: []aliyunpan.FileBatchActionParam{},
|
||||
}
|
||||
for k := range paths {
|
||||
filePath := paths[k]
|
||||
fileInfo, apierr := activeUser.PanClient().FileInfoByPath(activeUser.ActiveDriveId, filePath)
|
||||
fileInfo, apierr := activeUser.PanClient().WebapiPanClient().FileInfoByPath(activeUser.ActiveDriveId, filePath)
|
||||
if apierr != nil {
|
||||
fmt.Printf("获取文件信息失败: %s\n", filePath)
|
||||
continue
|
||||
@@ -531,7 +564,7 @@ func RunAlbumAddFile(albumName string, filePathList []string, filterOption Album
|
||||
}
|
||||
} else {
|
||||
// folder
|
||||
activeUser.PanClient().FilesDirectoriesRecurseList(activeUser.ActiveDriveId, fileInfo.Path, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
|
||||
activeUser.PanClient().WebapiPanClient().FilesDirectoriesRecurseList(activeUser.ActiveDriveId, fileInfo.Path, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
|
||||
if apiError != nil {
|
||||
logger.Verbosef("%s\n", apiError)
|
||||
return true
|
||||
@@ -553,7 +586,7 @@ func RunAlbumAddFile(albumName string, filePathList []string, filterOption Album
|
||||
return
|
||||
}
|
||||
// add file
|
||||
_, e := activeUser.PanClient().AlbumAddFile(param)
|
||||
_, e := activeUser.PanClient().WebapiPanClient().AlbumAddFile(param)
|
||||
if e != nil {
|
||||
fmt.Printf("增加相簿文件失败:%s\n", e)
|
||||
return
|
||||
@@ -584,9 +617,9 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
activeUser.PanClient().EnableCache()
|
||||
activeUser.PanClient().ClearCache()
|
||||
defer activeUser.PanClient().DisableCache()
|
||||
activeUser.PanClient().WebapiPanClient().EnableCache()
|
||||
activeUser.PanClient().WebapiPanClient().ClearCache()
|
||||
defer activeUser.PanClient().WebapiPanClient().DisableCache()
|
||||
// pan token expired checker
|
||||
continueFlag := int32(0)
|
||||
atomic.StoreInt32(&continueFlag, 0)
|
||||
@@ -596,7 +629,7 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
go func(flag *int32) {
|
||||
for atomic.LoadInt32(flag) == 0 {
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
if RefreshWebTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
logger.Verboseln("update access token for download task")
|
||||
}
|
||||
}
|
||||
@@ -619,7 +652,6 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
MaxRate: config.Config.MaxDownloadRate,
|
||||
InstanceStateStorageFormat: downloader.InstanceStateStorageFormatJSON,
|
||||
ShowProgress: options.ShowProgress,
|
||||
UseInternalUrl: config.Config.TransferUrlType == 2,
|
||||
ExcludeNames: options.ExcludeNames,
|
||||
}
|
||||
if cfg.CacheSize == 0 {
|
||||
@@ -681,7 +713,7 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
continue
|
||||
}
|
||||
// 获取相簿下的所有文件
|
||||
fileList, er := activeUser.PanClient().AlbumListFileGetAll(&aliyunpan.AlbumListFileParam{
|
||||
fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{
|
||||
AlbumId: record.AlbumId,
|
||||
})
|
||||
if er != nil {
|
||||
@@ -714,8 +746,10 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
GlobalSpeedsStat: globalSpeedsStat,
|
||||
FileRecorder: nil,
|
||||
}
|
||||
|
||||
// TODO: 相册下载需要重构
|
||||
// 设置相簿文件信息
|
||||
unit.SetFileInfo(pandownload.AlbumFileSource, f)
|
||||
//unit.SetFileInfo(pandownload.AlbumFileSource, f)
|
||||
|
||||
// 设置储存的路径
|
||||
if options.SaveTo != "" {
|
||||
|
||||
@@ -15,7 +15,6 @@ package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
@@ -96,24 +95,32 @@ func RunChangeDirectory(driveId, targetPath string) {
|
||||
user := config.Config.ActiveUser()
|
||||
targetPath = user.PathJoin(driveId, targetPath)
|
||||
|
||||
//targetPathInfo, err := user.PanClient().FileInfoByPath(driveId, targetPath)
|
||||
files, err := matchPathByShellPattern(driveId, targetPath)
|
||||
// 获取目标路径文件信息
|
||||
targetPathInfo, err := user.PanClient().OpenapiPanClient().FileInfoByPath(driveId, targetPath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
var targetPathInfo *aliyunpan.FileEntity
|
||||
if len(files) == 1 {
|
||||
targetPathInfo = files[0]
|
||||
} else {
|
||||
for _, f := range files {
|
||||
if f.IsFolder() {
|
||||
targetPathInfo = f
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// 适配通配符路径获取目标文件信息(弃用,容易触发风控)
|
||||
//files, err := matchPathByShellPattern(driveId, targetPath)
|
||||
//if err != nil {
|
||||
// fmt.Println(err)
|
||||
// return
|
||||
//}
|
||||
//
|
||||
//var targetPathInfo *aliyunpan.FileEntity
|
||||
//if len(files) == 1 {
|
||||
// targetPathInfo = files[0]
|
||||
//} else {
|
||||
// for _, f := range files {
|
||||
// if f.IsFolder() {
|
||||
// targetPathInfo = f
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
if targetPathInfo == nil {
|
||||
fmt.Println("路径不存在")
|
||||
return
|
||||
|
||||
@@ -18,26 +18,13 @@ import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdutil"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/library/crypto"
|
||||
"github.com/tickstep/library-go/getip"
|
||||
"github.com/urfave/cli"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
)
|
||||
|
||||
type (
|
||||
// 秒传数据项
|
||||
RapidUploadItem struct {
|
||||
FileSha1 string
|
||||
FileSize int64
|
||||
FilePath string // 绝对路径,包含文件名
|
||||
}
|
||||
)
|
||||
type ()
|
||||
|
||||
const (
|
||||
cryptoDescription = `
|
||||
@@ -58,7 +45,7 @@ const (
|
||||
var ErrBadArgs = errors.New("参数错误")
|
||||
var ErrNotLogined = errors.New("未登录账号")
|
||||
|
||||
func GetActivePanClient() *aliyunpan.PanClient {
|
||||
func GetActivePanClient() *config.PanClient {
|
||||
return config.Config.ActiveUser().PanClient()
|
||||
}
|
||||
|
||||
@@ -74,85 +61,6 @@ func parseDriveId(c *cli.Context) string {
|
||||
return driveId
|
||||
}
|
||||
|
||||
// newRapidUploadItem 通过解析秒传链接创建秒传实体
|
||||
func newRapidUploadItem(rapidUploadShareLink string) (*RapidUploadItem, error) {
|
||||
if strings.IndexAny(rapidUploadShareLink, "aliyunpan://") != 0 {
|
||||
return nil, fmt.Errorf("秒传链接格式错误: %s", rapidUploadShareLink)
|
||||
}
|
||||
|
||||
// 格式:aliyunpan://文件名|sha1|文件大小|<相对路径>
|
||||
rapidUploadShareLinkStr := strings.Replace(rapidUploadShareLink, "aliyunpan://", "", 1)
|
||||
|
||||
item := &RapidUploadItem{}
|
||||
parts := strings.Split(rapidUploadShareLinkStr, "|")
|
||||
|
||||
if len(parts) < 4 {
|
||||
return nil, fmt.Errorf("秒传链接格式错误: %s", rapidUploadShareLink)
|
||||
}
|
||||
|
||||
// hash
|
||||
if len(parts[1]) == 0 {
|
||||
return nil, fmt.Errorf("文件sha1错误: %s", rapidUploadShareLink)
|
||||
}
|
||||
item.FileSha1 = strings.TrimSpace(parts[1])
|
||||
|
||||
// size
|
||||
if size, e := strconv.ParseInt(parts[2], 10, 64); e == nil {
|
||||
item.FileSize = size
|
||||
} else {
|
||||
return nil, fmt.Errorf("文件大小错误: %s", rapidUploadShareLink)
|
||||
}
|
||||
|
||||
// path
|
||||
relativePath, _ := url.QueryUnescape(parts[3])
|
||||
item.FilePath = path.Join(relativePath, parts[0])
|
||||
|
||||
// result
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func newRapidUploadItemFromFileEntity(fileEntity *aliyunpan.FileEntity) *RapidUploadItem {
|
||||
if fileEntity == nil {
|
||||
return nil
|
||||
}
|
||||
return &RapidUploadItem{
|
||||
FileSha1: fileEntity.ContentHash,
|
||||
FileSize: fileEntity.FileSize,
|
||||
FilePath: fileEntity.Path,
|
||||
}
|
||||
}
|
||||
|
||||
// 创建秒传链接
|
||||
// 链接格式说明:aliyunpan://文件名|sha1|文件大小|<相对路径>
|
||||
// "相对路径" 可以为空,为空代表存储到网盘根目录
|
||||
func (r *RapidUploadItem) createRapidUploadLink(hideRelativePath bool) string {
|
||||
fullLink := &strings.Builder{}
|
||||
|
||||
p := r.FilePath
|
||||
p = strings.ReplaceAll(p, "\\", "/")
|
||||
|
||||
fileName := path.Base(p)
|
||||
dirPath := path.Dir(p)
|
||||
|
||||
// 去掉开头/
|
||||
if strings.Index(dirPath, "/") == 0 {
|
||||
dirPath = dirPath[1:]
|
||||
}
|
||||
// 相对路径编码
|
||||
dirPath = url.QueryEscape(dirPath)
|
||||
|
||||
// 隐藏相对路径
|
||||
if hideRelativePath {
|
||||
dirPath = ""
|
||||
}
|
||||
|
||||
// 拼接
|
||||
fmt.Fprintf(fullLink, "aliyunpan://%s|%s|%d|%s",
|
||||
fileName, strings.ToUpper(r.FileSha1), r.FileSize, dirPath)
|
||||
|
||||
return fullLink.String()
|
||||
}
|
||||
|
||||
func CmdConfig() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "config",
|
||||
@@ -213,10 +121,6 @@ func CmdConfig() cli.Command {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if c.IsSet("transfer_url_type") {
|
||||
config.Config.TransferUrlType = c.Int("transfer_url_type")
|
||||
}
|
||||
|
||||
if c.IsSet("savedir") {
|
||||
config.Config.SaveDir = c.String("savedir")
|
||||
}
|
||||
@@ -268,11 +172,6 @@ func CmdConfig() cli.Command {
|
||||
Name: "max_upload_rate",
|
||||
Usage: "限制最大上传速度, 0代表不限制",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "transfer_url_type",
|
||||
Usage: "上传下载URL类别,1-默认,2-阿里云ECS",
|
||||
Value: 1,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "savedir",
|
||||
Usage: "下载文件的储存目录",
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRapidUploadItem_createRapidUploadLink(t *testing.T) {
|
||||
item := &RapidUploadItem{
|
||||
FileSha1: "752FCCBFB2436A6FFCA3B287831D4FAA5654B07E",
|
||||
FileSize: 7005440,
|
||||
FilePath: "/dgsdg/rtt5/我的文件夹/file我的文件.dmg",
|
||||
}
|
||||
fmt.Println(item.createRapidUploadLink(false))
|
||||
|
||||
}
|
||||
|
||||
func TestRapidUploadItem_newRapidUploadItem(t *testing.T) {
|
||||
link := "aliyunpan://file我的文件.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|dgsdg%2Frtt5%2F%E6%88%91%E7%9A%84%E6%96%87%E4%BB%B6%E5%A4%B9"
|
||||
item,_ := newRapidUploadItem(link)
|
||||
fmt.Println(item)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func CmdCp() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "cp",
|
||||
Usage: "复制文件/目录",
|
||||
UsageText: `
|
||||
aliyunpan cp <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>`,
|
||||
Description: `
|
||||
同网盘内复制文件或者目录。支持通配符匹配复制文件,通配符当前只能匹配文件名,不能匹配文件路径。
|
||||
|
||||
示例:
|
||||
|
||||
将 /我的资源/1.mp4 复制到 根目录 /
|
||||
aliyunpan cp /我的资源/1.mp4 /
|
||||
|
||||
将 /我的资源 目录下所有的.png文件 复制到 /我的图片 目录下面,使用通配符匹配
|
||||
aliyunpan cp /我的资源/*.png /我的图片
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() <= 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
RunCopy(parseDriveId(c), c.Args()...)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunCopy 执行复制文件/目录
|
||||
func RunCopy(driveId string, paths ...string) {
|
||||
activeUser := GetActiveUser()
|
||||
cacheCleanPaths := []string{}
|
||||
opFileList, targetFile, _, err := getFileInfo(driveId, paths...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if targetFile == nil {
|
||||
fmt.Println("目标文件不存在")
|
||||
return
|
||||
}
|
||||
if opFileList == nil || len(opFileList) == 0 {
|
||||
fmt.Println("没有有效的文件可复制")
|
||||
return
|
||||
}
|
||||
cacheCleanPaths = append(cacheCleanPaths, targetFile.Path)
|
||||
|
||||
failedCopyFiles := []*aliyunpan.FileEntity{}
|
||||
successCopyFiles := []*aliyunpan.FileEntity{}
|
||||
for _, mfi := range opFileList {
|
||||
_, er := activeUser.PanClient().OpenapiPanClient().FileCopy(&aliyunpan.FileCopyParam{
|
||||
DriveId: driveId,
|
||||
FileId: mfi.FileId,
|
||||
ToParentFileId: targetFile.FileId,
|
||||
})
|
||||
if er == nil {
|
||||
successCopyFiles = append(successCopyFiles, mfi)
|
||||
} else {
|
||||
failedCopyFiles = append(failedCopyFiles, mfi)
|
||||
}
|
||||
}
|
||||
|
||||
if len(failedCopyFiles) > 0 {
|
||||
fmt.Println("以下文件复制失败:")
|
||||
for _, f := range failedCopyFiles {
|
||||
fmt.Println(f.Path)
|
||||
}
|
||||
fmt.Println("")
|
||||
}
|
||||
if len(successCopyFiles) > 0 {
|
||||
pnt := func() {
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "文件/目录"})
|
||||
for k, rs := range successCopyFiles {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), rs.Path})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
fmt.Println("操作成功, 以下文件已复制到目标目录: ", targetFile.Path)
|
||||
pnt()
|
||||
activeUser.DeleteCache(cacheCleanPaths)
|
||||
} else {
|
||||
fmt.Println("无法复制文件,请稍后重试")
|
||||
}
|
||||
}
|
||||
@@ -25,15 +25,12 @@ import (
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -49,7 +46,6 @@ type (
|
||||
NoCheck bool
|
||||
ShowProgress bool
|
||||
DriveId string
|
||||
UseInternalUrl bool // 是否使用内置链接
|
||||
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
|
||||
}
|
||||
|
||||
@@ -219,23 +215,24 @@ func downloadPrintFormat(load int) string {
|
||||
// RunDownload 执行下载网盘内文件
|
||||
func RunDownload(paths []string, options *DownloadOptions) {
|
||||
activeUser := GetActiveUser()
|
||||
activeUser.PanClient().EnableCache()
|
||||
activeUser.PanClient().ClearCache()
|
||||
defer activeUser.PanClient().DisableCache()
|
||||
// pan token expired checker
|
||||
continueFlag := int32(0)
|
||||
atomic.StoreInt32(&continueFlag, 0)
|
||||
defer func() {
|
||||
atomic.StoreInt32(&continueFlag, 1)
|
||||
}()
|
||||
go func(flag *int32) {
|
||||
for atomic.LoadInt32(flag) == 0 {
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
logger.Verboseln("update access token for download task")
|
||||
}
|
||||
}
|
||||
}(&continueFlag)
|
||||
activeUser.PanClient().OpenapiPanClient().EnableCache()
|
||||
activeUser.PanClient().OpenapiPanClient().ClearCache()
|
||||
defer activeUser.PanClient().OpenapiPanClient().DisableCache()
|
||||
|
||||
//// pan token expired checker
|
||||
//continueFlag := int32(0)
|
||||
//atomic.StoreInt32(&continueFlag, 0)
|
||||
//defer func() {
|
||||
// atomic.StoreInt32(&continueFlag, 1)
|
||||
//}()
|
||||
//go func(flag *int32) {
|
||||
// for atomic.LoadInt32(flag) == 0 {
|
||||
// time.Sleep(time.Duration(1) * time.Minute)
|
||||
// if RefreshWebTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
// logger.Verboseln("update access token for download task")
|
||||
// }
|
||||
// }
|
||||
//}(&continueFlag)
|
||||
|
||||
if options == nil {
|
||||
options = &DownloadOptions{}
|
||||
@@ -258,7 +255,6 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
||||
MaxRate: config.Config.MaxDownloadRate,
|
||||
InstanceStateStorageFormat: downloader.InstanceStateStorageFormatJSON,
|
||||
ShowProgress: options.ShowProgress,
|
||||
UseInternalUrl: config.Config.TransferUrlType == 2,
|
||||
ExcludeNames: options.ExcludeNames,
|
||||
}
|
||||
if cfg.CacheSize == 0 {
|
||||
@@ -299,7 +295,6 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("\n[*] 注意:由于阿里云盘接口的限制,当前不支持>100M单个文件的下载。")
|
||||
fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 下载缓存为: %s\n\n", options.Parallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
||||
|
||||
var (
|
||||
|
||||
@@ -27,9 +27,9 @@ import (
|
||||
func CmdDrive() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "drive",
|
||||
Usage: "切换网盘(备份盘/资源库/相册)",
|
||||
Usage: "切换网盘(备份盘/资源库)",
|
||||
Description: `
|
||||
切换已登录账号的阿里云盘的工作网盘(备份盘/资源库/相册)
|
||||
切换已登录账号的阿里云盘的工作网盘(备份盘/资源库)
|
||||
如果运行该条命令没有提供参数, 程序将会列出所有的网盘列表, 供选择切换.
|
||||
|
||||
示例:
|
||||
@@ -125,6 +125,9 @@ func getDriveOptionList() (config.DriveInfoList, string) {
|
||||
tb.SetHeader([]string{"#", "drive_id", "网盘名称"})
|
||||
|
||||
for k, info := range driveList {
|
||||
if info.IsAlbumDrive() {
|
||||
continue
|
||||
}
|
||||
tb.Append([]string{strconv.Itoa(k + 1), info.DriveId, info.DriveName})
|
||||
}
|
||||
tb.Render()
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CmdExport() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "export",
|
||||
Usage: "导出文件/目录元数据",
|
||||
UsageText: cmder.App().Name + " export <网盘文件/目录的路径1> <文件/目录2> <文件/目录3> ... <本地保存文件路径>",
|
||||
Description: `
|
||||
导出指定文件/目录下面的所有文件的元数据信息,并保存到指定的本地文件里面。导出的文件元信息可以使用 import 命令(秒传文件功能)导入到网盘中。
|
||||
支持多个文件或目录的导出.
|
||||
|
||||
示例:
|
||||
|
||||
导出 /我的资源/1.mp4 元数据到文件 /Users/tickstep/Downloads/export_files.txt
|
||||
aliyunpan export /我的资源/1.mp4 /Users/tickstep/Downloads/export_files.txt
|
||||
|
||||
导出 /我的资源 整个目录 元数据到文件 /Users/tickstep/Downloads/export_files.txt
|
||||
aliyunpan export /我的资源 /Users/tickstep/Downloads/export_files.txt
|
||||
|
||||
导出 网盘 整个目录 元数据到文件 /Users/tickstep/Downloads/export_files.txt
|
||||
aliyunpan export / /Users/tickstep/Downloads/export_files.txt
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() < 2 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
subArgs := c.Args()
|
||||
RunExportFiles(parseDriveId(c), c.Bool("ow"), subArgs[:len(subArgs)-1], subArgs[len(subArgs)-1])
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "ow",
|
||||
Usage: "overwrite, 覆盖已存在的导出文件",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunExportFiles(driveId string, overwrite bool, panPaths []string, saveLocalFilePath string) {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
panClient := activeUser.PanClient()
|
||||
|
||||
lfi, _ := os.Stat(saveLocalFilePath)
|
||||
realSaveFilePath := saveLocalFilePath
|
||||
if lfi != nil {
|
||||
if lfi.IsDir() {
|
||||
realSaveFilePath = path.Join(saveLocalFilePath, "export_file_") + strconv.FormatInt(time.Now().Unix(), 10) + ".txt"
|
||||
} else {
|
||||
if !overwrite {
|
||||
fmt.Println("导出文件已存在")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// create file
|
||||
localDir := path.Dir(saveLocalFilePath)
|
||||
dirFs, _ := os.Stat(localDir)
|
||||
if dirFs != nil {
|
||||
if !dirFs.IsDir() {
|
||||
fmt.Println("指定的保存文件路径不合法")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
er := os.MkdirAll(localDir, 0755)
|
||||
if er != nil {
|
||||
fmt.Println("创建本地文件夹出错")
|
||||
return
|
||||
}
|
||||
}
|
||||
realSaveFilePath = saveLocalFilePath
|
||||
}
|
||||
|
||||
totalCount := 0
|
||||
saveFile, err := os.OpenFile(realSaveFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, panPath := range panPaths {
|
||||
panPath = activeUser.PathJoin(driveId, panPath)
|
||||
panClient.FilesDirectoriesRecurseList(driveId, panPath, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
|
||||
if apiError != nil {
|
||||
logger.Verbosef("%s\n", apiError)
|
||||
return true
|
||||
}
|
||||
|
||||
// 只需要存储文件即可
|
||||
if !fd.IsFolder() {
|
||||
item := newRapidUploadItemFromFileEntity(fd)
|
||||
jstr := item.createRapidUploadLink(false)
|
||||
if len(jstr) <= 0 {
|
||||
logger.Verboseln("create rapid upload link err")
|
||||
return false
|
||||
}
|
||||
saveFile.WriteString(jstr + "\n")
|
||||
totalCount += 1
|
||||
time.Sleep(time.Duration(100) * time.Millisecond)
|
||||
fmt.Printf("\r导出文件数量: %d", totalCount)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// close and save
|
||||
if err := saveFile.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("\r导出文件总数量: %d\n", totalCount)
|
||||
fmt.Printf("导出文件保存路径: %s\n", realSaveFilePath)
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
dirFileListData struct {
|
||||
Dir *aliyunpan.MkdirResult
|
||||
FileList aliyunpan.FileList
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultSaveToPanPath = "/aliyunpan"
|
||||
)
|
||||
|
||||
func CmdImport() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "import",
|
||||
Usage: "导入文件",
|
||||
UsageText: cmder.App().Name + " export <本地元数据文件路径>",
|
||||
Description: `
|
||||
导入文件中记录的元数据文件到网盘。保存到网盘的文件会使用文件元数据记录的路径位置,如果没有指定云盘目录(saveto)则默认导入到目录 aliyunpan 中。
|
||||
导入的文件可以使用 export 命令获得。
|
||||
|
||||
导入文件每一行是一个文件元数据,样例如下:
|
||||
aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder
|
||||
|
||||
示例:
|
||||
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项
|
||||
aliyunpan import /Users/tickstep/Downloads/export_files.txt
|
||||
|
||||
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项并保存到目录 /my2021 中
|
||||
aliyunpan import -saveto=/my2021 /Users/tickstep/Downloads/export_files.txt
|
||||
|
||||
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项并保存到网盘根目录 / 中
|
||||
aliyunpan import -saveto=/ /Users/tickstep/Downloads/export_files.txt
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() < 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
saveTo := ""
|
||||
if c.String("saveto") != "" {
|
||||
saveTo = filepath.Clean(c.String("saveto"))
|
||||
}
|
||||
|
||||
subArgs := c.Args()
|
||||
RunImportFiles(parseDriveId(c), c.Bool("ow"), saveTo, subArgs[0])
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "ow",
|
||||
Usage: "overwrite, 覆盖已存在的网盘文件",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "saveto",
|
||||
Usage: "将文件保存到指定的目录",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunImportFiles(driveId string, overwrite bool, panSavePath, localFilePath string) {
|
||||
lfi, _ := os.Stat(localFilePath)
|
||||
if lfi != nil {
|
||||
if lfi.IsDir() {
|
||||
fmt.Println("请指定导入文件")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// create file
|
||||
fmt.Println("导入文件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if panSavePath == "" {
|
||||
// use default
|
||||
panSavePath = DefaultSaveToPanPath
|
||||
}
|
||||
|
||||
fmt.Println("导入的文件会存储到目录:" + panSavePath)
|
||||
|
||||
importFile, err := os.OpenFile(localFilePath, os.O_RDONLY, 0755)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
defer importFile.Close()
|
||||
|
||||
fileData, err := ioutil.ReadAll(importFile)
|
||||
if err != nil {
|
||||
fmt.Println("读取文件出错")
|
||||
return
|
||||
}
|
||||
fileText := string(fileData)
|
||||
if len(fileText) == 0 {
|
||||
fmt.Println("文件为空")
|
||||
return
|
||||
}
|
||||
fileText = strings.TrimSpace(fileText)
|
||||
fileLines := strings.Split(fileText, "\n")
|
||||
importFileItems := []RapidUploadItem{}
|
||||
for _, line := range fileLines {
|
||||
line = strings.TrimSpace(line)
|
||||
if item, e := newRapidUploadItem(line); e != nil {
|
||||
fmt.Println(e)
|
||||
continue
|
||||
} else {
|
||||
item.FilePath = strings.ReplaceAll(path.Join(panSavePath, item.FilePath), "\\", "/")
|
||||
importFileItems = append(importFileItems, *item)
|
||||
}
|
||||
}
|
||||
if len(importFileItems) == 0 {
|
||||
fmt.Println("没有可以导入的文件项目")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("正在准备导入...")
|
||||
dirMap := prepareMkdir(driveId, importFileItems)
|
||||
|
||||
fmt.Println("正在导入...")
|
||||
successImportFiles := []RapidUploadItem{}
|
||||
failedImportFiles := []RapidUploadItem{}
|
||||
for _, item := range importFileItems {
|
||||
fmt.Printf("正在处理导入: %s\n", item.FilePath)
|
||||
result, abort := processOneImport(driveId, overwrite, dirMap, item)
|
||||
if abort {
|
||||
fmt.Println("导入任务终止了")
|
||||
break
|
||||
}
|
||||
if result {
|
||||
successImportFiles = append(successImportFiles, item)
|
||||
} else {
|
||||
failedImportFiles = append(failedImportFiles, item)
|
||||
}
|
||||
time.Sleep(time.Duration(200) * time.Millisecond)
|
||||
}
|
||||
if len(failedImportFiles) > 0 {
|
||||
fmt.Println("\n以下文件导入失败")
|
||||
for _, f := range failedImportFiles {
|
||||
fmt.Printf("%s %s\n", f.FileSha1, f.FilePath)
|
||||
}
|
||||
fmt.Println("")
|
||||
}
|
||||
fmt.Printf("导入结果, 成功 %d, 失败 %d\n", len(successImportFiles), len(failedImportFiles))
|
||||
}
|
||||
|
||||
func processOneImport(driveId string, isOverwrite bool, dirMap map[string]*dirFileListData, item RapidUploadItem) (result, abort bool) {
|
||||
panClient := config.Config.ActiveUser().PanClient()
|
||||
panDir, fileName := path.Split(item.FilePath)
|
||||
dataItem := dirMap[path.Dir(panDir)]
|
||||
if isOverwrite {
|
||||
// 标记覆盖旧同名文件
|
||||
// 检查同名文件是否存在
|
||||
var efi *aliyunpan.FileEntity = nil
|
||||
for _, fileItem := range dataItem.FileList {
|
||||
if !fileItem.IsFolder() && fileItem.FileName == fileName {
|
||||
efi = fileItem
|
||||
break
|
||||
}
|
||||
}
|
||||
if efi != nil && efi.FileId != "" {
|
||||
// existed, delete it
|
||||
fdr, err := panClient.FileDelete([]*aliyunpan.FileBatchActionParam{
|
||||
{
|
||||
DriveId: driveId,
|
||||
FileId: efi.FileId,
|
||||
},
|
||||
})
|
||||
if err != nil || fdr == nil || !fdr[0].Success {
|
||||
fmt.Println("无法删除文件,请稍后重试")
|
||||
return false, false
|
||||
}
|
||||
time.Sleep(time.Duration(500) * time.Millisecond)
|
||||
fmt.Println("检测到同名文件,已移动到回收站")
|
||||
}
|
||||
}
|
||||
|
||||
appCreateUploadFileParam := &aliyunpan.CreateFileUploadParam{
|
||||
DriveId: driveId,
|
||||
Name: fileName,
|
||||
Size: item.FileSize,
|
||||
ContentHash: item.FileSha1,
|
||||
ParentFileId: dataItem.Dir.FileId,
|
||||
}
|
||||
uploadOpEntity, apierr := panClient.CreateUploadFile(appCreateUploadFileParam)
|
||||
if apierr != nil {
|
||||
fmt.Println("创建秒传任务失败:" + apierr.Error())
|
||||
return false, true
|
||||
}
|
||||
|
||||
if uploadOpEntity.RapidUpload {
|
||||
logger.Verboseln("秒传成功, 保存到网盘路径: ", path.Join(panDir, uploadOpEntity.FileName))
|
||||
} else {
|
||||
fmt.Println("失败,文件未曾上传,无法秒传")
|
||||
return false, false
|
||||
}
|
||||
return true, false
|
||||
}
|
||||
|
||||
func prepareMkdir(driveId string, importFileItems []RapidUploadItem) map[string]*dirFileListData {
|
||||
panClient := config.Config.ActiveUser().PanClient()
|
||||
resultMap := map[string]*dirFileListData{}
|
||||
for _, item := range importFileItems {
|
||||
var apierr *apierror.ApiError
|
||||
var rs *aliyunpan.MkdirResult
|
||||
panDir := path.Dir(item.FilePath)
|
||||
if resultMap[panDir] != nil {
|
||||
continue
|
||||
}
|
||||
if panDir != "/" {
|
||||
rs, apierr = panClient.MkdirRecursive(driveId, "", "", 0, strings.Split(path.Clean(panDir), "/"))
|
||||
if apierr != nil || rs.FileId == "" {
|
||||
logger.Verboseln("创建云盘文件夹失败")
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
rs = &aliyunpan.MkdirResult{}
|
||||
rs.FileId = aliyunpan.DefaultRootParentFileId
|
||||
}
|
||||
dataItem := &dirFileListData{}
|
||||
dataItem.Dir = rs
|
||||
|
||||
// files
|
||||
param := &aliyunpan.FileListParam{}
|
||||
param.DriveId = driveId
|
||||
param.ParentFileId = rs.FileId
|
||||
allFileInfo, err1 := panClient.FileListGetAll(param, 0)
|
||||
if err1 != nil {
|
||||
logger.Verboseln("获取文件信息出错")
|
||||
continue
|
||||
}
|
||||
dataItem.FileList = allFileInfo
|
||||
|
||||
resultMap[panDir] = dataItem
|
||||
time.Sleep(time.Duration(500) * time.Millisecond)
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/library/collection"
|
||||
"github.com/urfave/cli"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CmdLocateUrl() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "locate",
|
||||
Usage: "获取文件下载链接",
|
||||
UsageText: cmder.App().Name + " locate <文件/目录1> <文件/目录2> <文件/目录3> ...",
|
||||
Description: `
|
||||
获取文件下载直链,支持文件和文件夹。下载链接有效时间为4个小时。
|
||||
导出的下载链接可以使用任何支持http下载的工具进行下载,如果是视频文件还可以使用支持在线流播放的视频软件进行在线播放。
|
||||
|
||||
注意:由于阿里云盘网页端有防盗链设置所以不能直接使用网页Token登录,你必须使用手机扫码二维码登录(命令:login -QrCode),否则获取的直链无法正常下载,会提示 403 Forbidden 下载被禁止。
|
||||
|
||||
示例:
|
||||
获取 /我的资源/1.mp4 下载直链
|
||||
aliyunpan locate /我的资源/1.mp4
|
||||
|
||||
获取 /我的资源 目录下面所有mp4文件的下载直链,使用通配符
|
||||
aliyunpan locate /我的资源/*.mp4
|
||||
|
||||
获取 /我的资源 目录下面所有文件的下载直链并保存到本地文件 /Volumes/Downloads/file_url.txt 中
|
||||
aliyunpan locate -saveto "/Volumes/Downloads/file_url.txt" /我的资源
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() == 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
saveFilePath := ""
|
||||
if c.IsSet("saveto") {
|
||||
saveFilePath = c.String("saveto")
|
||||
}
|
||||
RunLocateUrl(parseDriveId(c), c.Args(), saveFilePath)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "saveto",
|
||||
Usage: "导出链接成文件并保存到指定的位置",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunLocateUrl 执行下载网盘内文件
|
||||
func RunLocateUrl(driveId string, paths []string, saveFilePath string) {
|
||||
useInternalUrl := config.Config.TransferUrlType == 2
|
||||
activeUser := GetActiveUser()
|
||||
activeUser.PanClient().EnableCache()
|
||||
activeUser.PanClient().ClearCache()
|
||||
defer activeUser.PanClient().DisableCache()
|
||||
|
||||
paths, err := makePathAbsolute(driveId, paths...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fileEntityQueue := collection.NewFifoQueue()
|
||||
sb := &strings.Builder{}
|
||||
failedList := []string{}
|
||||
for _, p := range paths {
|
||||
fileList, err1 := matchPathByShellPattern(driveId, p)
|
||||
if err1 != nil {
|
||||
failedList = append(failedList, p)
|
||||
continue
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
// 文件不存在
|
||||
failedList = append(failedList, p)
|
||||
continue
|
||||
}
|
||||
for _, f := range fileList {
|
||||
// 匹配的文件
|
||||
fileEntityQueue.Push(f)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
item := fileEntityQueue.Pop()
|
||||
if item == nil {
|
||||
break
|
||||
}
|
||||
fileInfo := item.(*aliyunpan.FileEntity)
|
||||
if fileInfo.IsFolder() { // 文件夹,获取下面所有文件
|
||||
allFilesInFolder, er := activeUser.PanClient().FileListGetAll(&aliyunpan.FileListParam{
|
||||
DriveId: driveId,
|
||||
ParentFileId: fileInfo.FileId,
|
||||
}, 300)
|
||||
if er != nil {
|
||||
failedList = append(failedList, fileInfo.Path)
|
||||
continue
|
||||
}
|
||||
for _, f := range allFilesInFolder {
|
||||
f.Path = fileInfo.Path + "/" + f.FileName
|
||||
if f.IsFolder() {
|
||||
// for next term
|
||||
fileEntityQueue.Push(f)
|
||||
continue
|
||||
}
|
||||
durl, apierr := activeUser.PanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
|
||||
DriveId: driveId,
|
||||
FileId: f.FileId,
|
||||
})
|
||||
if apierr != nil {
|
||||
failedList = append(failedList, f.Path)
|
||||
continue
|
||||
}
|
||||
url := durl.Url
|
||||
if useInternalUrl {
|
||||
url = durl.InternalUrl
|
||||
}
|
||||
if saveFilePath != "" {
|
||||
fmt.Printf("获取文件下载链接:%s\n", f.Path)
|
||||
fmt.Fprintf(sb, "\n文件:%s\n%s\n", f.Path, url)
|
||||
} else {
|
||||
fmt.Printf("\n文件:%s\n%s\n", f.Path, url)
|
||||
}
|
||||
}
|
||||
} else { // 文件
|
||||
durl, apierr := activeUser.PanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
|
||||
DriveId: driveId,
|
||||
FileId: fileInfo.FileId,
|
||||
})
|
||||
if apierr != nil {
|
||||
failedList = append(failedList, fileInfo.Path)
|
||||
continue
|
||||
}
|
||||
url := durl.Url
|
||||
if useInternalUrl {
|
||||
url = durl.InternalUrl
|
||||
}
|
||||
if saveFilePath != "" {
|
||||
fmt.Printf("获取文件下载链接:%s\n", fileInfo.Path)
|
||||
fmt.Fprintf(sb, "\n文件:%s\n%s\n", fileInfo.Path, url)
|
||||
} else {
|
||||
fmt.Printf("\n文件:%s\n%s\n", fileInfo.Path, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if saveFilePath != "" {
|
||||
// save file
|
||||
if e := ioutil.WriteFile(saveFilePath, []byte(sb.String()), 0777); e == nil {
|
||||
fmt.Printf("保存文件成功:%s\n", saveFilePath)
|
||||
} else {
|
||||
fmt.Printf("保存文件失败:%s\n", saveFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
// 输出失败的文件列表
|
||||
if len(failedList) > 0 {
|
||||
pnt := func() {
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "文件/目录"})
|
||||
for k, f := range failedList {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), f})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
fmt.Printf("\n\n以下文件获取直链失败: \n")
|
||||
pnt()
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,13 @@ package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdliner"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
_ "github.com/tickstep/library-go/requester"
|
||||
"github.com/urfave/cli"
|
||||
"time"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CmdLogin() cli.Command {
|
||||
@@ -34,55 +33,36 @@ func CmdLogin() cli.Command {
|
||||
1.常规登录,按提示一步一步来即可
|
||||
aliyunpan login
|
||||
|
||||
2.直接指定RefreshToken进行登录
|
||||
aliyunpan login -RefreshToken=8B12CBBCE89CA8DFC3445985B63B511B5E7EC7...
|
||||
|
||||
3.使用二维码方式进行登录
|
||||
aliyunpan login -QrCode
|
||||
`,
|
||||
Category: "阿里云盘账号",
|
||||
Before: ReloadConfigFunc, // 每次进行登录动作的时候需要调用刷新配置
|
||||
After: SaveConfigFunc, // 登录完成需要调用保存配置
|
||||
Action: func(c *cli.Context) error {
|
||||
refreshTokenStr := ""
|
||||
if refreshTokenStr == "" {
|
||||
refreshTokenStr = c.String("RefreshToken")
|
||||
}
|
||||
useQrCode := c.Bool("QrCode")
|
||||
|
||||
tokenId := ""
|
||||
webToken := aliyunpan.WebLoginToken{}
|
||||
refreshToken := ""
|
||||
ticketId := ""
|
||||
openToken := &config.PanClientToken{}
|
||||
webToken := &config.PanClientToken{}
|
||||
var err error
|
||||
tokenId, refreshToken, webToken, err = RunLogin(useQrCode, refreshTokenStr)
|
||||
ticketId, openToken, webToken, err = RunLogin()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
cloudUser, err := config.SetupUserByCookie(&webToken, config.Config.DeviceId, config.Config.DeviceName)
|
||||
cloudUser, err := config.SetupUserByCookie(openToken, webToken,
|
||||
ticketId, "",
|
||||
config.Config.DeviceId, config.Config.DeviceName,
|
||||
config.Config.ClientId, config.Config.ClientSecret)
|
||||
if cloudUser == nil {
|
||||
fmt.Println("登录失败: ", err)
|
||||
return nil
|
||||
}
|
||||
cloudUser.RefreshToken = refreshToken
|
||||
cloudUser.TokenId = tokenId
|
||||
cloudUser.TicketId = ticketId
|
||||
config.Config.SetActiveUser(cloudUser)
|
||||
fmt.Println("阿里云盘登录成功: ", cloudUser.Nickname)
|
||||
return nil
|
||||
},
|
||||
// 命令的附加options参数说明,使用 help panlogin 命令即可查看
|
||||
Flags: []cli.Flag{
|
||||
// aliyunpan panlogin -RefreshToken=8B12CBBCE89CA8DFC3445985B63B511B5E7EC7...
|
||||
cli.StringFlag{
|
||||
Name: "RefreshToken",
|
||||
Usage: "使用RefreshToken Cookie来登录帐号",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "QrCode",
|
||||
Usage: "使用二维码登录",
|
||||
},
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +97,7 @@ func CmdLogout() cli.Command {
|
||||
}
|
||||
|
||||
// 云端注销登录
|
||||
//if _, e := activeUser.PanClient().DeviceLogout(); e != nil {
|
||||
//if _, e := activeUser.PanClient().WebapiPanClient().DeviceLogout(); e != nil {
|
||||
// fmt.Printf("登出设备失败,请手动在网页端进行登出: %s\n", e.String())
|
||||
//}
|
||||
|
||||
@@ -139,55 +119,46 @@ func CmdLogout() cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func RunLogin(useQrCodeLogin bool, refreshToken string) (tokenId, refreshTokenStr string, webToken aliyunpan.WebLoginToken, error error) {
|
||||
if useQrCodeLogin {
|
||||
h := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
|
||||
qrCodeUrlResult, err := h.GetQRCodeLoginUrl("")
|
||||
if err != nil {
|
||||
fmt.Println("二维码登录错误:", err)
|
||||
return "", "", aliyunpan.WebLoginToken{}, err
|
||||
}
|
||||
fmt.Printf("请在浏览器打开以下链接进行扫码登录,链接有效时间为5分钟\n%s\n\n", qrCodeUrlResult.TokenUrl+"&deviceId="+config.Config.DeviceId)
|
||||
func RunLogin() (ticketId string, openapiToken, webapiToken *config.PanClientToken, error error) {
|
||||
h := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
|
||||
|
||||
// handler waiting
|
||||
line := cmdliner.NewLiner()
|
||||
var qrCodeLoginResult *panlogin.QRCodeLoginResult
|
||||
queryResult := true
|
||||
defer line.Close()
|
||||
// web login request
|
||||
qrCodeUrlResult, err := h.GetQRCodeLoginUrl("")
|
||||
if err != nil {
|
||||
fmt.Println("登录出错:", err)
|
||||
return "", nil, nil, err
|
||||
}
|
||||
ticketId = qrCodeUrlResult.TokenId
|
||||
loginUrl := &strings.Builder{}
|
||||
if global.IsSupportNoneOpenApiCommands {
|
||||
// 兼容以前的版本
|
||||
fmt.Fprintf(loginUrl, "https://openapi.alipan.com/oauth/authorize?client_id=%s&redirect_uri=https%%3A%%2F%%2Fapi.tickstep.com%%2Fauth%%2Ftickstep%%2Faliyunpan%%2Ftoken%%2Fopenapi%%2F%s%%2Fauth&scope=user:base,file:all:read,file:all:write",
|
||||
config.Config.ClientId, ticketId)
|
||||
} else {
|
||||
fmt.Fprintf(loginUrl, "https://openapi.alipan.com/oauth/authorize?client_id=%s&redirect_uri=https%%3A%%2F%%2Fapi.tickstep.com%%2Fauth%%2Ftickstep%%2Faliyunpan%%2Ftoken%%2Fopenapi%%2F%s%%2Fauth2&scope=user:base,file:all:read,file:all:write",
|
||||
config.Config.ClientId, ticketId)
|
||||
}
|
||||
fmt.Printf("请在浏览器打开以下链接进行登录,链接有效时间为5分钟。\n注意:你需要进行一次授权一次扫码的两次登录。\n%s\n\n", loginUrl)
|
||||
|
||||
go func() {
|
||||
for queryResult {
|
||||
time.Sleep(3 * time.Second)
|
||||
qr, er := h.GetQRCodeLoginResult(qrCodeUrlResult.TokenId)
|
||||
if er != nil {
|
||||
continue
|
||||
}
|
||||
logger.Verboseln(qr)
|
||||
if qr.QrCodeStatus == "CONFIRMED" {
|
||||
// login successfully
|
||||
qrCodeLoginResult = qr
|
||||
break
|
||||
} else if qr.QrCodeStatus == "EXPIRED" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
// handler waiting
|
||||
line := cmdliner.NewLiner()
|
||||
defer line.Close()
|
||||
line.State.Prompt("请在浏览器里面完成扫码登录,然后再按Enter键继续...")
|
||||
|
||||
line.State.Prompt("请在浏览器里面完成扫码登录,然后再按Enter键继续...")
|
||||
if qrCodeLoginResult == nil {
|
||||
queryResult = false
|
||||
return "", "", aliyunpan.WebLoginToken{}, fmt.Errorf("二维码登录失败")
|
||||
}
|
||||
|
||||
tokenStr, er := h.ParseSecureRefreshToken("", qrCodeLoginResult.SecureRefreshToken)
|
||||
if er != nil {
|
||||
fmt.Println("解析Token错误:", er)
|
||||
return "", "", aliyunpan.WebLoginToken{}, er
|
||||
}
|
||||
refreshToken = tokenStr
|
||||
tokenId = qrCodeUrlResult.TokenId
|
||||
// get login token
|
||||
comToken, er := h.GetLoginToken(ticketId)
|
||||
if er != nil {
|
||||
return ticketId, nil, nil, fmt.Errorf("登录失败,请稍后尝试重新登录")
|
||||
}
|
||||
|
||||
refreshTokenStr, webToken, error = DoLoginHelper(refreshToken)
|
||||
return
|
||||
return ticketId,
|
||||
&config.PanClientToken{
|
||||
AccessToken: comToken.Openapi.AccessToken,
|
||||
Expired: comToken.Openapi.Expired,
|
||||
},
|
||||
&config.PanClientToken{
|
||||
AccessToken: comToken.Webapi.AccessToken,
|
||||
Expired: comToken.Webapi.Expired,
|
||||
},
|
||||
nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -141,22 +141,31 @@ func RunLs(driveId, targetPath string, lsOptions *LsOptions,
|
||||
activeUser := config.Config.ActiveUser()
|
||||
targetPath = activeUser.PathJoin(driveId, targetPath)
|
||||
|
||||
files, err := matchPathByShellPattern(driveId, targetPath)
|
||||
// 获取目标路径文件信息
|
||||
targetPathInfo, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, targetPath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
var targetPathInfo *aliyunpan.FileEntity
|
||||
if len(files) == 1 {
|
||||
targetPathInfo = files[0]
|
||||
} else {
|
||||
for _, f := range files {
|
||||
if f.IsFolder() {
|
||||
targetPathInfo = f
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 适配通配符路径获取目标文件信息(弃用,容易触发风控)
|
||||
//files, err := matchPathByShellPattern(driveId, targetPath)
|
||||
//if err != nil {
|
||||
// fmt.Println(err)
|
||||
// return
|
||||
//}
|
||||
//var targetPathInfo *aliyunpan.FileEntity
|
||||
//if len(files) == 1 {
|
||||
// targetPathInfo = files[0]
|
||||
//} else {
|
||||
// for _, f := range files {
|
||||
// if f.IsFolder() {
|
||||
// targetPathInfo = f
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
if targetPathInfo == nil {
|
||||
fmt.Println("目录路径不存在")
|
||||
return
|
||||
@@ -169,7 +178,7 @@ func RunLs(driveId, targetPath string, lsOptions *LsOptions,
|
||||
fileListParam.OrderBy = orderBy
|
||||
fileListParam.OrderDirection = orderDirection
|
||||
if targetPathInfo.IsFolder() {
|
||||
fileResult, err1 := activeUser.PanClient().FileListGetAll(fileListParam, 200)
|
||||
fileResult, err1 := activeUser.PanClient().OpenapiPanClient().FileListGetAll(fileListParam, 200)
|
||||
if err1 != nil {
|
||||
fmt.Println(err1)
|
||||
return
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -56,7 +56,7 @@ func RunMkdir(driveId, name string) {
|
||||
fullpath := activeUser.PathJoin(driveId, name)
|
||||
rs := &aliyunpan.MkdirResult{}
|
||||
err := apierror.NewFailedApiError("")
|
||||
rs, err = activeUser.PanClient().Mkdir(driveId, "", fullpath)
|
||||
rs, err = activeUser.PanClient().OpenapiPanClient().MkdirByFullPath(driveId, fullpath)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("创建文件夹失败:" + err.Error())
|
||||
|
||||
@@ -85,49 +85,44 @@ func RunMove(driveId string, paths ...string) {
|
||||
cacheCleanPaths = append(cacheCleanPaths, targetFile.Path)
|
||||
|
||||
failedMoveFiles := []*aliyunpan.FileEntity{}
|
||||
moveFileParamList := []*aliyunpan.FileMoveParam{}
|
||||
fileId2FileEntity := map[string]*aliyunpan.FileEntity{}
|
||||
successMoveFiles := []*aliyunpan.FileEntity{}
|
||||
for _, mfi := range opFileList {
|
||||
fileId2FileEntity[mfi.FileId] = mfi
|
||||
moveFileParamList = append(moveFileParamList,
|
||||
&aliyunpan.FileMoveParam{
|
||||
DriveId: driveId,
|
||||
FileId: mfi.FileId,
|
||||
ToDriveId: driveId,
|
||||
ToParentFileId: targetFile.FileId,
|
||||
})
|
||||
cacheCleanPaths = append(cacheCleanPaths, path.Dir(mfi.Path))
|
||||
}
|
||||
fmr, er := activeUser.PanClient().FileMove(moveFileParamList)
|
||||
|
||||
for _, rs := range fmr {
|
||||
if !rs.Success {
|
||||
failedMoveFiles = append(failedMoveFiles, fileId2FileEntity[rs.FileId])
|
||||
fmr, er := activeUser.PanClient().OpenapiPanClient().FileMove(&aliyunpan.FileMoveParam{
|
||||
DriveId: driveId,
|
||||
FileId: mfi.FileId,
|
||||
ToDriveId: driveId,
|
||||
ToParentFileId: targetFile.FileId,
|
||||
})
|
||||
if er != nil || !fmr.Success {
|
||||
failedMoveFiles = append(failedMoveFiles, mfi)
|
||||
} else {
|
||||
successMoveFiles = append(successMoveFiles, mfi)
|
||||
}
|
||||
cacheCleanPaths = append(cacheCleanPaths, path.Dir(mfi.Path))
|
||||
}
|
||||
|
||||
if len(failedMoveFiles) > 0 {
|
||||
fmt.Println("以下文件移动失败:")
|
||||
for _, f := range failedMoveFiles {
|
||||
fmt.Println(f.FileName)
|
||||
fmt.Println(f.Path)
|
||||
}
|
||||
fmt.Println("")
|
||||
}
|
||||
if er == nil {
|
||||
if len(successMoveFiles) > 0 {
|
||||
pnt := func() {
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "文件/目录"})
|
||||
for k, rs := range fmr {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), fileId2FileEntity[rs.FileId].Path})
|
||||
for k, rs := range successMoveFiles {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), rs.Path})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
fmt.Println("操作成功, 以下文件已移动到目标目录: ", targetFile.Path)
|
||||
pnt()
|
||||
activeUser.DeleteCache(cacheCleanPaths)
|
||||
} else {
|
||||
fmt.Println("无法移动文件,请稍后重试")
|
||||
}
|
||||
activeUser.DeleteCache(cacheCleanPaths)
|
||||
}
|
||||
|
||||
func getFileInfo(driveId string, paths ...string) (opFileList []*aliyunpan.FileEntity, targetFile *aliyunpan.FileEntity, failedPaths []string, error error) {
|
||||
@@ -138,7 +133,7 @@ func getFileInfo(driveId string, paths ...string) (opFileList []*aliyunpan.FileE
|
||||
// the last one is the target file path
|
||||
targetFilePath := path.Clean(paths[len(paths)-1])
|
||||
absolutePath := activeUser.PathJoin(driveId, targetFilePath)
|
||||
targetFile, err := activeUser.PanClient().FileInfoByPath(driveId, absolutePath)
|
||||
targetFile, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, absolutePath)
|
||||
if err != nil || !targetFile.IsFolder() {
|
||||
return nil, nil, nil, fmt.Errorf("指定目标文件夹不存在")
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func CmdQuota() cli.Command {
|
||||
}
|
||||
|
||||
func RunGetQuotaInfo() (quotaInfo *QuotaInfo, error error) {
|
||||
user, err := GetActivePanClient().GetUserInfo()
|
||||
user, err := GetActivePanClient().OpenapiPanClient().GetUserInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -17,8 +17,10 @@ import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
@@ -60,6 +62,14 @@ func CmdRecycle() cli.Command {
|
||||
Usage: "列出回收站文件列表",
|
||||
UsageText: cmder.App().Name + " recycle list",
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunRecycleList(parseDriveId(c))
|
||||
return nil
|
||||
},
|
||||
@@ -78,6 +88,14 @@ func CmdRecycle() cli.Command {
|
||||
UsageText: cmder.App().Name + " recycle restore <file_id 1> <file_id 2> <file_id 3> ...",
|
||||
Description: `根据文件/目录的 fs_id, 还原回收站指定的文件或目录`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
if c.NArg() <= 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
@@ -100,6 +118,14 @@ func CmdRecycle() cli.Command {
|
||||
UsageText: cmder.App().Name + " recycle delete [-all] <file_id 1> <file_id 2> <file_id 3> ...",
|
||||
Description: `根据文件/目录的 file_id 或 -all 参数, 删除回收站指定的文件或目录或清空回收站`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
if c.Bool("all") {
|
||||
// 清空回收站
|
||||
RunRecycleClear(parseDriveId(c))
|
||||
@@ -132,7 +158,7 @@ func CmdRecycle() cli.Command {
|
||||
// RunRecycleList 执行列出回收站文件列表
|
||||
func RunRecycleList(driveId string) {
|
||||
panClient := GetActivePanClient()
|
||||
fdl, err := panClient.RecycleBinFileListGetAll(&aliyunpan.RecycleBinFileListParam{
|
||||
fdl, err := panClient.WebapiPanClient().RecycleBinFileListGetAll(&aliyunpan_web.RecycleBinFileListParam{
|
||||
DriveId: driveId,
|
||||
Limit: 100,
|
||||
})
|
||||
@@ -174,7 +200,7 @@ func RunRecycleRestore(driveId string, fidStrList ...string) {
|
||||
return
|
||||
}
|
||||
|
||||
rbfr, err := panClient.RecycleBinFileRestore(restoreFileList)
|
||||
rbfr, err := panClient.WebapiPanClient().RecycleBinFileRestore(restoreFileList)
|
||||
if rbfr != nil && len(rbfr) > 0 {
|
||||
fmt.Printf("还原文件成功\n")
|
||||
return
|
||||
@@ -203,7 +229,7 @@ func RunRecycleDelete(driveId string, fidStrList ...string) {
|
||||
return
|
||||
}
|
||||
|
||||
rbfr, err := panClient.RecycleBinFileDelete(deleteFileList)
|
||||
rbfr, err := panClient.WebapiPanClient().RecycleBinFileDelete(deleteFileList)
|
||||
if rbfr != nil && len(rbfr) > 0 {
|
||||
fmt.Printf("彻底删除文件成功\n")
|
||||
return
|
||||
@@ -220,7 +246,7 @@ func RunRecycleClear(driveId string) {
|
||||
panClient := GetActivePanClient()
|
||||
|
||||
// 提交清空回收站异步任务
|
||||
r, err := panClient.RecycleBinFileClear(&aliyunpan.RecycleBinFileClearParam{
|
||||
r, err := panClient.WebapiPanClient().RecycleBinFileClear(&aliyunpan_web.RecycleBinFileClearParam{
|
||||
DriveId: driveId,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -229,7 +255,7 @@ func RunRecycleClear(driveId string) {
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
ar, err1 := panClient.AsyncTaskQueryStatus(&aliyunpan.AsyncTaskQueryStatusParam{
|
||||
ar, err1 := panClient.WebapiPanClient().AsyncTaskQueryStatus(&aliyunpan_web.AsyncTaskQueryStatusParam{
|
||||
AsyncTaskId: r.AsyncTaskId,
|
||||
})
|
||||
if err1 != nil {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -136,14 +136,14 @@ func RunRename(driveId string, oldName string, newName string) {
|
||||
}
|
||||
|
||||
fileId := ""
|
||||
r, err := GetActivePanClient().FileInfoByPath(driveId, activeUser.PathJoin(driveId, oldName))
|
||||
r, err := GetActivePanClient().OpenapiPanClient().FileInfoByPath(driveId, activeUser.PathJoin(driveId, oldName))
|
||||
if err != nil {
|
||||
fmt.Printf("原文件不存在: %s, %s\n", oldName, err)
|
||||
return
|
||||
}
|
||||
fileId = r.FileId
|
||||
|
||||
b, e := activeUser.PanClient().FileRename(driveId, fileId, path.Base(newName))
|
||||
b, e := activeUser.PanClient().OpenapiPanClient().FileRename(driveId, fileId, path.Base(newName))
|
||||
if e != nil {
|
||||
fmt.Println(e.Err)
|
||||
return
|
||||
@@ -234,7 +234,7 @@ func RunRenameBatch(skipConfirm bool, driveId string, expression, replacement, f
|
||||
|
||||
// 重命名
|
||||
for _, file := range files {
|
||||
b, e := activeUser.PanClient().FileRename(driveId, file.file.FileId, file.newFileName)
|
||||
b, e := activeUser.PanClient().OpenapiPanClient().FileRename(driveId, file.file.FileId, file.newFileName)
|
||||
if e != nil {
|
||||
fmt.Println(e.Err)
|
||||
return
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -78,8 +78,7 @@ func RunRemove(driveId string, paths ...string) {
|
||||
|
||||
cacheCleanDirs := []string{}
|
||||
failedRmPaths := make([]string, 0, len(paths))
|
||||
delFileInfos := []*aliyunpan.FileBatchActionParam{}
|
||||
fileId2FileEntity := map[string]*aliyunpan.FileEntity{}
|
||||
successDelFileEntity := []*aliyunpan.FileEntity{}
|
||||
|
||||
for _, p := range paths {
|
||||
absolutePath := path.Clean(activeUser.PathJoin(driveId, p))
|
||||
@@ -95,28 +94,27 @@ func RunRemove(driveId string, paths ...string) {
|
||||
}
|
||||
for _, f := range fileList {
|
||||
// 删除匹配的文件
|
||||
delFileInfos = append(delFileInfos, &aliyunpan.FileBatchActionParam{
|
||||
fdr, err := activeUser.PanClient().OpenapiPanClient().FileDelete(&aliyunpan.FileBatchActionParam{
|
||||
DriveId: driveId,
|
||||
FileId: f.FileId,
|
||||
})
|
||||
fileId2FileEntity[f.FileId] = f
|
||||
if err != nil || !fdr.Success {
|
||||
failedRmPaths = append(failedRmPaths, absolutePath)
|
||||
} else {
|
||||
successDelFileEntity = append(successDelFileEntity, f)
|
||||
}
|
||||
cacheCleanDirs = append(cacheCleanDirs, path.Dir(f.Path))
|
||||
}
|
||||
}
|
||||
|
||||
// delete
|
||||
successDelFileEntity := []*aliyunpan.FileEntity{}
|
||||
fdr, err := activeUser.PanClient().FileDelete(delFileInfos)
|
||||
if fdr != nil {
|
||||
for _, item := range fdr {
|
||||
if !item.Success {
|
||||
failedRmPaths = append(failedRmPaths, fileId2FileEntity[item.FileId].Path)
|
||||
} else {
|
||||
successDelFileEntity = append(successDelFileEntity, fileId2FileEntity[item.FileId])
|
||||
}
|
||||
// output
|
||||
if len(failedRmPaths) > 0 {
|
||||
fmt.Println("以下文件删除失败:")
|
||||
for _, fp := range failedRmPaths {
|
||||
fmt.Println(fp)
|
||||
}
|
||||
fmt.Println("")
|
||||
}
|
||||
|
||||
pnt := func() {
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "文件/目录"})
|
||||
@@ -130,9 +128,4 @@ func RunRemove(driveId string, paths ...string) {
|
||||
pnt()
|
||||
activeUser.DeleteCache(cacheCleanDirs)
|
||||
}
|
||||
|
||||
if len(successDelFileEntity) == 0 && err != nil {
|
||||
fmt.Println("无法删除文件,请稍后重试")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func CmdSave() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "save",
|
||||
Usage: "保存分享文件/目录",
|
||||
UsageText: `
|
||||
aliyunpan save <分享链接> (<提取码>) <目标目录>`,
|
||||
Description: `
|
||||
注意: 保存大量文件时, 命令完成后可能还需要额外等待一段时间; 分享的根目录下如果包含大量文件或文件夹, 可能存在不稳定的情况
|
||||
|
||||
示例:
|
||||
|
||||
将 公开分享 保存到 根目录 /
|
||||
aliyunpan save ABCD1234wxyz /
|
||||
aliyunpan save https://www.alipan.com/s/ABCD1234wxyz /
|
||||
|
||||
将 私密分享 保存到 指定目录 /资源分享
|
||||
aliyunpan save ABCD1234wxyz akd1 /资源分享
|
||||
aliyunpan save https://www.alipan.com/s/ABCD1234wxyz akd1 /资源分享
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() <= 1 || c.NArg() > 3 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunSave(parseDriveId(c), c.Args()...)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunSave 保存分享的文件
|
||||
func RunSave(driveId string, args ...string) {
|
||||
activeUser := GetActiveUser()
|
||||
|
||||
targetFilePath := path.Clean(args[len(args)-1])
|
||||
absolutePath := activeUser.PathJoin(driveId, targetFilePath)
|
||||
targetFile, err := activeUser.PanClient().WebapiPanClient().FileInfoByPath(driveId, absolutePath)
|
||||
if err != nil || !targetFile.IsFolder() {
|
||||
fmt.Println("指定目标文件夹不存在")
|
||||
return
|
||||
}
|
||||
fmt.Println("保存文件至:", targetFilePath)
|
||||
|
||||
shareID := args[0]
|
||||
if i := strings.Index(shareID, "alipan.com/s/"); i > 0 {
|
||||
shareID = shareID[i+13:]
|
||||
}
|
||||
sharePwd := ""
|
||||
if len(args) == 3 {
|
||||
sharePwd = args[1]
|
||||
}
|
||||
|
||||
token, err := activeUser.PanClient().WebapiPanClient().GetShareToken(shareID, sharePwd)
|
||||
if err != nil {
|
||||
fmt.Println("读取分享链接失败:", err)
|
||||
return
|
||||
}
|
||||
|
||||
list, err := activeUser.PanClient().WebapiPanClient().GetListByShare(token.ShareToken, shareID, "")
|
||||
if err != nil {
|
||||
fmt.Println("读取分享文件列表失败:", err)
|
||||
return
|
||||
}
|
||||
for list.NextMarker != "" {
|
||||
list2, err := activeUser.PanClient().WebapiPanClient().GetListByShare(token.ShareToken, shareID, "")
|
||||
if err != nil {
|
||||
fmt.Println("读取分享文件列表失败:", err)
|
||||
return
|
||||
}
|
||||
list.Items = append(list.Items, list2.Items...)
|
||||
list.NextMarker = list2.NextMarker
|
||||
}
|
||||
|
||||
var params []*aliyunpan_web.FileSaveParam
|
||||
files := make(map[string]*aliyunpan_web.ListByShareItem)
|
||||
for _, item := range list.Items {
|
||||
if item.FileExtension != "" {
|
||||
fmt.Println(" ", item.Name)
|
||||
} else {
|
||||
fmt.Println(" ", item.Name+"/")
|
||||
}
|
||||
files[item.FileID] = item
|
||||
|
||||
params = append(params, &aliyunpan_web.FileSaveParam{
|
||||
ShareID: shareID,
|
||||
FileId: item.FileID,
|
||||
AutoRename: true,
|
||||
ToDriveId: driveId,
|
||||
ToParentFileId: targetFile.FileId,
|
||||
})
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
result, err := activeUser.PanClient().WebapiPanClient().FileCopy(token.ShareToken, params)
|
||||
if err != nil {
|
||||
fmt.Println("保存分享文件失败:", err)
|
||||
return
|
||||
}
|
||||
var ids []string
|
||||
var failedSaveFileIds []string
|
||||
tasks := make(map[string]string)
|
||||
for _, item := range result {
|
||||
if item.AsyncTaskId == "" {
|
||||
if item.Status != 201 {
|
||||
failedSaveFileIds = append(failedSaveFileIds, item.FileId)
|
||||
}
|
||||
} else {
|
||||
tasks[item.AsyncTaskId] = item.FileId
|
||||
ids = append(ids, item.AsyncTaskId)
|
||||
}
|
||||
}
|
||||
if ids != nil {
|
||||
result2, err := activeUser.PanClient().WebapiPanClient().AsyncTaskGet(token.ShareToken, ids)
|
||||
if err != nil {
|
||||
fmt.Println("读取保存结果失败:", err)
|
||||
}
|
||||
|
||||
for _, item := range result2 {
|
||||
if !item.Success {
|
||||
failedSaveFileIds = append(failedSaveFileIds, tasks[item.AsyncTaskId])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if failedSaveFileIds != nil {
|
||||
fmt.Println("以下文件保存失败:")
|
||||
for _, id := range failedSaveFileIds {
|
||||
if v, ok := files[id]; ok {
|
||||
fmt.Println(v.Name)
|
||||
} else {
|
||||
fmt.Println(id)
|
||||
}
|
||||
}
|
||||
fmt.Println("")
|
||||
}
|
||||
fmt.Println("操作成功, 分享文件已保存到目标目录: ", targetFile.Path)
|
||||
}
|
||||
@@ -18,10 +18,10 @@ import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path"
|
||||
@@ -51,19 +51,19 @@ func CmdShare() cli.Command {
|
||||
示例:
|
||||
|
||||
创建文件 1.mp4 的分享链接
|
||||
aliyunpan share set 1.mp4
|
||||
aliyunpan share set -mode 1 1.mp4
|
||||
|
||||
创建 /我的视频/ 目录下所有mp4文件的分享链接,支持通配符
|
||||
aliyunpan share set /我的视频/*.mp4
|
||||
aliyunpan share set -mode 1 /我的视频/*.mp4
|
||||
|
||||
创建文件 1.mp4 的分享链接,并指定分享密码为2333
|
||||
aliyunpan share set -sharePwd 2333 1.mp4
|
||||
aliyunpan share set -mode 1 -sharePwd 2333 1.mp4
|
||||
|
||||
创建文件 1.mp4 的分享链接,并指定有效期为1天
|
||||
aliyunpan share set -time 1 1.mp4
|
||||
aliyunpan share set -mode 1 -time 1 1.mp4
|
||||
|
||||
创建文件 1.mp4 的快传链接
|
||||
aliyunpan share set -mode 3 1.mp4
|
||||
aliyunpan share set 1.mp4
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() < 1 {
|
||||
@@ -74,6 +74,10 @@ func CmdShare() cli.Command {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
et := ""
|
||||
timeFlag := "0"
|
||||
if c.IsSet("time") {
|
||||
@@ -144,6 +148,14 @@ func CmdShare() cli.Command {
|
||||
Usage: "列出已分享文件/目录",
|
||||
UsageText: cmder.App().Name + " share list",
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunShareList()
|
||||
return nil
|
||||
},
|
||||
@@ -156,6 +168,14 @@ func CmdShare() cli.Command {
|
||||
UsageText: cmder.App().Name + " share cancel <shareid_1> <shareid_2> ...",
|
||||
Description: `目前只支持通过分享id (shareid) 来取消分享.`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
if c.NArg() < 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
@@ -179,6 +199,14 @@ func CmdShare() cli.Command {
|
||||
aliyunpan share export -option 2 "d:\myfoler\share_list.csv"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
if c.NArg() < 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
@@ -199,51 +227,6 @@ func CmdShare() cli.Command {
|
||||
},
|
||||
},
|
||||
},
|
||||
// {
|
||||
// Name: "mc",
|
||||
// Aliases: []string{},
|
||||
// Usage: "创建秒传链接",
|
||||
// UsageText: cmder.App().Name + " share mc <文件/目录1> <文件/目录2> ...",
|
||||
// Description: `
|
||||
//创建文件秒传链接,秒传链接只能是文件,如果是文件夹则会创建文件夹包含的所有文件的秒传链接。秒传链接可以通过RapidUpload命令或者Import命令进行导入到自己的网盘。
|
||||
//示例:
|
||||
// 创建文件 1.mp4 的秒传链接
|
||||
// aliyunpan share mc 1.mp4
|
||||
//
|
||||
// 创建文件 1.mp4 的秒传链接,但链接隐藏相对路径
|
||||
// aliyunpan share mc -hp 1.mp4
|
||||
//
|
||||
// 创建文件夹 share_folder 下面所有文件的秒传链接
|
||||
// aliyunpan share mc share_folder/
|
||||
//`,
|
||||
// Action: func(c *cli.Context) error {
|
||||
// if c.NArg() < 1 {
|
||||
// cli.ShowCommandHelp(c, c.Command.Name)
|
||||
// return nil
|
||||
// }
|
||||
// if config.Config.ActiveUser() == nil {
|
||||
// fmt.Println("未登录账号")
|
||||
// return nil
|
||||
// }
|
||||
// hp := false
|
||||
// if c.IsSet("hp") {
|
||||
// hp = c.Bool("hp")
|
||||
// }
|
||||
// RunShareMc(parseDriveId(c), hp, c.Args())
|
||||
// return nil
|
||||
// },
|
||||
// Flags: []cli.Flag{
|
||||
// cli.StringFlag{
|
||||
// Name: "driveId",
|
||||
// Usage: "网盘ID",
|
||||
// Value: "",
|
||||
// },
|
||||
// cli.BoolFlag{
|
||||
// Name: "hp",
|
||||
// Usage: "hide path, 隐藏相对目录",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -286,7 +269,7 @@ func RunShareSet(modeFlag, driveId string, paths []string, expiredTime string, s
|
||||
|
||||
if modeFlag == "3" {
|
||||
// 快传
|
||||
r, err1 := panClient.FastShareLinkCreate(aliyunpan.FastShareCreateParam{
|
||||
r, err1 := panClient.WebapiPanClient().FastShareLinkCreate(aliyunpan_web.FastShareCreateParam{
|
||||
DriveId: driveId,
|
||||
FileIdList: fidList,
|
||||
})
|
||||
@@ -303,7 +286,7 @@ func RunShareSet(modeFlag, driveId string, paths []string, expiredTime string, s
|
||||
fmt.Printf("链接:%s\n", r.ShareUrl)
|
||||
} else {
|
||||
// 分享
|
||||
r, err1 := panClient.ShareLinkCreate(aliyunpan.ShareCreateParam{
|
||||
r, err1 := panClient.WebapiPanClient().ShareLinkCreate(aliyunpan_web.ShareCreateParam{
|
||||
DriveId: driveId,
|
||||
SharePwd: sharePwd,
|
||||
Expiration: expiredTime,
|
||||
@@ -330,7 +313,7 @@ func RunShareSet(modeFlag, driveId string, paths []string, expiredTime string, s
|
||||
// RunShareList 执行列出分享列表
|
||||
func RunShareList() {
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().ShareLinkList(activeUser.UserId)
|
||||
records, err := activeUser.PanClient().WebapiPanClient().ShareLinkList(activeUser.UserId)
|
||||
if err != nil {
|
||||
fmt.Printf("获取分享列表失败: %s\n", err)
|
||||
return
|
||||
@@ -378,7 +361,7 @@ func RunShareCancel(shareIdList []string) {
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
r, err := activeUser.PanClient().ShareLinkCancel(shareIdList)
|
||||
r, err := activeUser.PanClient().WebapiPanClient().ShareLinkCancel(shareIdList)
|
||||
if err != nil {
|
||||
fmt.Printf("取消分享操作失败: %s\n", err)
|
||||
return
|
||||
@@ -393,7 +376,7 @@ func RunShareCancel(shareIdList []string) {
|
||||
|
||||
func RunShareExport(option, saveFilePath string) {
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().ShareLinkList(activeUser.UserId)
|
||||
records, err := activeUser.PanClient().WebapiPanClient().ShareLinkList(activeUser.UserId)
|
||||
if err != nil {
|
||||
fmt.Printf("获取分享列表失败: %s\n", err)
|
||||
return
|
||||
@@ -458,36 +441,3 @@ func ExportCsv(savePath string, data [][]string) bool {
|
||||
w.Flush()
|
||||
return true
|
||||
}
|
||||
|
||||
// 创建秒传链接
|
||||
func RunShareMc(driveId string, hideRelativePath bool, panPaths []string) {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
panClient := activeUser.PanClient()
|
||||
|
||||
totalCount := 0
|
||||
for _, panPath := range panPaths {
|
||||
panPath = activeUser.PathJoin(driveId, panPath)
|
||||
panClient.FilesDirectoriesRecurseList(driveId, panPath, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
|
||||
if apiError != nil {
|
||||
logger.Verbosef("%s\n", apiError)
|
||||
return true
|
||||
}
|
||||
|
||||
// 只需要文件即可
|
||||
if !fd.IsFolder() {
|
||||
item := newRapidUploadItemFromFileEntity(fd)
|
||||
jstr := item.createRapidUploadLink(hideRelativePath)
|
||||
if len(jstr) <= 0 {
|
||||
logger.Verboseln("create rapid upload link err")
|
||||
return false
|
||||
}
|
||||
// print
|
||||
fmt.Println(jstr)
|
||||
totalCount += 1
|
||||
time.Sleep(time.Duration(100) * time.Millisecond)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
fmt.Printf("\n秒传链接总数量: %d\n", totalCount)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"github.com/tickstep/aliyunpan/internal/syncdrive"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
@@ -27,7 +28,6 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -37,16 +37,14 @@ func CmdSync() cli.Command {
|
||||
Usage: "同步备份功能(Beta)",
|
||||
UsageText: cmder.App().Name + " sync",
|
||||
Description: `
|
||||
备份功能。支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份三种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
备份功能。支持备份本地文件到云盘,备份云盘文件到本地两种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
指定本地目录和对应的一个网盘目录,以备份文件。网盘目录必须和本地目录独占使用,不要用作其他用途,不然备份可能会有问题。
|
||||
|
||||
备份功能支持以下三种模式:
|
||||
备份功能支持以下模式:
|
||||
1. upload
|
||||
备份本地文件,即上传本地文件到网盘,始终保持本地文件有一个完整的备份在网盘
|
||||
2. download
|
||||
备份云盘文件,即下载网盘文件到本地,始终保持网盘的文件有一个完整的备份在本地
|
||||
3. sync(慎用!!!双向备份过程会删除文件)
|
||||
双向备份,保持网盘文件和本地文件严格一致
|
||||
|
||||
请输入以下命令查看如何配置和启动:
|
||||
aliyunpan sync start -h
|
||||
@@ -74,7 +72,8 @@ func CmdSync() cli.Command {
|
||||
"localFolderPath": "D:/tickstep/Documents/设计文档",
|
||||
"panFolderPath": "/sync_drive/我的文档",
|
||||
"mode": "upload",
|
||||
"priority": "time"
|
||||
"policy": "increment",
|
||||
"driveName": "backup"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -82,36 +81,30 @@ func CmdSync() cli.Command {
|
||||
name - 任务名称
|
||||
localFolderPath - 本地目录
|
||||
panFolderPath - 网盘目录
|
||||
mode - 模式,支持三种: upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)
|
||||
priority - 优先级,只对双向同步备份模式有效。选项支持三种: time-时间优先,local-本地优先,pan-网盘优先
|
||||
mode - 备份模式,支持两种: upload(备份本地文件到云盘),download(备份云盘文件到本地)
|
||||
policy - 备份策略, 支持两种: exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)
|
||||
driveName - 网盘名称,backup(备份盘),resource(资源盘)
|
||||
|
||||
例子:
|
||||
1. 查看帮助
|
||||
aliyunpan sync start -h
|
||||
|
||||
2. 使用命令行配置启动同步备份服务,将本地目录 D:\tickstep\Documents\设计文档 中的文件备份上传到云盘目录 /sync_drive/我的文档
|
||||
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "upload"
|
||||
2. 使用命令行配置启动同步备份服务,将本地目录 D:\tickstep\Documents\设计文档 中的文件备份上传到"备份盘"的云盘目录 /sync_drive/我的文档
|
||||
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "upload" -drive "backup"
|
||||
|
||||
3. 使用命令行配置启动同步备份服务,将云盘目录 /sync_drive/我的文档 中的文件备份下载到本地目录 D:\tickstep\Documents\设计文档
|
||||
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "download"
|
||||
|
||||
4. 使用命令行配置启动同步备份服务,将云盘目录 /sync_drive/我的文档 和本地目录 D:\tickstep\Documents\设计文档 的文件进行双向同步
|
||||
同时配置同步优先选项为本地文件优先,并显示同步过程的日志
|
||||
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "sync" -pri "local" -log true
|
||||
|
||||
5. 使用命令行配置启动同步备份服务,将本地目录 D:\tickstep\Documents\设计文档 中的文件备份到云盘目录 /sync_drive/我的文档
|
||||
4. 使用命令行配置启动同步备份服务,将本地目录 D:\tickstep\Documents\设计文档 中的文件备份到云盘目录 /sync_drive/我的文档
|
||||
同时配置下载并发为2,上传并发为1,下载分片大小为256KB,上传分片大小为1MB
|
||||
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "upload" -dp 2 -up 1 -dbs 256 -ubs 1024
|
||||
|
||||
6. 使用配置文件启动同步备份服务,使用配置文件可以支持同时启动多个备份任务。配置文件必须存在,否则启动失败。
|
||||
5. 使用配置文件启动同步备份服务,使用配置文件可以支持同时启动多个备份任务。配置文件必须存在,否则启动失败。
|
||||
aliyunpan sync start
|
||||
|
||||
7. 使用配置文件启动同步备份服务,并配置下载并发为2,上传并发为1,下载分片大小为256KB,上传分片大小为1MB
|
||||
6. 使用配置文件启动同步备份服务,并配置下载并发为2,上传并发为1,下载分片大小为256KB,上传分片大小为1MB
|
||||
aliyunpan sync start -dp 2 -up 1 -dbs 256 -ubs 1024
|
||||
|
||||
8. 当你本地同步目录文件非常多,或者云盘同步目录文件非常多,为了后期更快更精准同步文件,可以先进行文件扫描并构建同步数据库,然后再正常启动同步任务。如下所示:
|
||||
aliyunpan sync start -step scan
|
||||
aliyunpan sync start
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
@@ -155,27 +148,22 @@ priority - 优先级,只对双向同步备份模式有效。选项支持三种
|
||||
uploadBlockSize = aliyunpan.DefaultChunkSize
|
||||
}
|
||||
|
||||
opt := c.String("pri")
|
||||
var syncOpt syncdrive.SyncPriorityOption = syncdrive.SyncPriorityTimestampFirst
|
||||
if opt == "local" {
|
||||
syncOpt = syncdrive.SyncPriorityLocalFirst
|
||||
} else if opt == "pan" {
|
||||
syncOpt = syncdrive.SyncPriorityPanFirst
|
||||
} else {
|
||||
syncOpt = syncdrive.SyncPriorityTimestampFirst
|
||||
}
|
||||
|
||||
// 任务类型
|
||||
step := syncdrive.StepSyncFile
|
||||
stepVar := c.String("step")
|
||||
if stepVar == "scan" {
|
||||
step = syncdrive.StepScanFile
|
||||
}
|
||||
//opt := c.String("pri")
|
||||
//if opt == "local" {
|
||||
// syncOpt = syncdrive.SyncPriorityLocalFirst
|
||||
//} else if opt == "pan" {
|
||||
// syncOpt = syncdrive.SyncPriorityPanFirst
|
||||
//} else {
|
||||
// syncOpt = syncdrive.SyncPriorityTimestampFirst
|
||||
//}
|
||||
|
||||
var task *syncdrive.SyncTask
|
||||
localDir := c.String("ldir")
|
||||
panDir := c.String("pdir")
|
||||
mode := c.String("mode")
|
||||
policy := c.String("policy")
|
||||
driveName := c.String("drive")
|
||||
if localDir != "" && panDir != "" {
|
||||
// make path absolute
|
||||
if !utils.IsLocalAbsPath(localDir) {
|
||||
@@ -203,39 +191,57 @@ priority - 优先级,只对双向同步备份模式有效。选项支持三种
|
||||
task = &syncdrive.SyncTask{}
|
||||
task.LocalFolderPath = path.Clean(strings.ReplaceAll(localDir, "\\", "/"))
|
||||
task.PanFolderPath = panDir
|
||||
task.Mode = syncdrive.UploadOnly
|
||||
if mode == string(syncdrive.UploadOnly) {
|
||||
task.Mode = syncdrive.UploadOnly
|
||||
} else if mode == string(syncdrive.DownloadOnly) {
|
||||
task.Mode = syncdrive.DownloadOnly
|
||||
task.Mode = syncdrive.Upload
|
||||
if mode == string(syncdrive.Upload) {
|
||||
task.Mode = syncdrive.Upload
|
||||
} else if mode == string(syncdrive.Download) {
|
||||
task.Mode = syncdrive.Download
|
||||
} else if mode == string(syncdrive.SyncTwoWay) {
|
||||
task.Mode = syncdrive.SyncTwoWay
|
||||
} else {
|
||||
task.Mode = syncdrive.UploadOnly
|
||||
task.Mode = syncdrive.Upload
|
||||
}
|
||||
if policy == string(syncdrive.SyncPolicyExclusive) {
|
||||
task.Policy = syncdrive.SyncPolicyExclusive
|
||||
} else if policy == string(syncdrive.SyncPolicyIncrement) {
|
||||
task.Policy = syncdrive.SyncPolicyIncrement
|
||||
} else {
|
||||
task.Policy = syncdrive.SyncPolicyIncrement
|
||||
}
|
||||
task.Name = path.Base(task.LocalFolderPath)
|
||||
task.Id = utils.Md5Str(task.LocalFolderPath)
|
||||
task.Priority = syncOpt
|
||||
task.UserId = activeUser.UserId
|
||||
|
||||
// drive id
|
||||
task.DriveName = driveName
|
||||
if strings.ToLower(task.DriveName) == "backup" {
|
||||
task.DriveId = activeUser.DriveList.GetFileDriveId()
|
||||
} else if strings.ToLower(task.DriveName) == "resource" {
|
||||
task.DriveId = activeUser.DriveList.GetResourceDriveId()
|
||||
}
|
||||
if len(task.DriveId) == 0 {
|
||||
task.DriveName = "backup"
|
||||
task.DriveId = activeUser.DriveList.GetFileDriveId()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取同步文件锁,保证同步操作单实例
|
||||
//locker := filelocker.NewFileLocker(config.GetLockerDir() + "/aliyunpan-sync")
|
||||
//if e := filelocker.LockFile(locker, 0755, true, 5*time.Second); e != nil {
|
||||
// logger.Verboseln(e)
|
||||
// fmt.Println("本应用其他实例正在执行同步,请先停止或者等待其完成")
|
||||
// return nil
|
||||
//}
|
||||
cycleMode := syncdrive.CycleInfiniteLoop
|
||||
if c.String("cycle") == "onetime" {
|
||||
cycleMode = syncdrive.CycleOneTime
|
||||
} else {
|
||||
cycleMode = syncdrive.CycleInfiniteLoop
|
||||
}
|
||||
RunSync(task, cycleMode, dp, up, downloadBlockSize, uploadBlockSize, syncOpt, c.Int("ldt"))
|
||||
|
||||
RunSync(task, dp, up, downloadBlockSize, uploadBlockSize, syncOpt, c.Int("ldt"), step)
|
||||
|
||||
// 释放文件锁
|
||||
//if locker != nil {
|
||||
// filelocker.UnlockFile(locker)
|
||||
//}
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "drive",
|
||||
Usage: "drive name, 网盘名称,backup(备份盘),resource(资源盘)",
|
||||
Value: "backup",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "ldir",
|
||||
Usage: "local dir, 本地文件夹完整路径",
|
||||
@@ -246,13 +252,23 @@ priority - 优先级,只对双向同步备份模式有效。选项支持三种
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "mode",
|
||||
Usage: "备份模式, 支持三种: upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)",
|
||||
Usage: "备份模式, 支持两种: upload(备份本地文件到云盘),download(备份云盘文件到本地)",
|
||||
Value: "upload",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "pri",
|
||||
Usage: "优先级priority,只对双向同步备份模式有效。当网盘和本地存在同名文件,优先使用哪个,选项支持三种: time-时间优先,local-本地优先,pan-网盘优先",
|
||||
Value: "time",
|
||||
Name: "policy",
|
||||
Usage: "备份策略, 支持两种: exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)",
|
||||
Value: "increment",
|
||||
},
|
||||
//cli.StringFlag{
|
||||
// Name: "pri",
|
||||
// Usage: "同步优先级,只对sync模式有效。当网盘和本地存在同名文件,优先使用哪个,选项支持三种: time-时间优先,local-本地优先,pan-网盘优先",
|
||||
// Value: "time",
|
||||
//},
|
||||
cli.StringFlag{
|
||||
Name: "cycle",
|
||||
Usage: "备份周期, 支持两种: infinity(永久循环备份),onetime(只运行一次备份)",
|
||||
Value: "infinity",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "dp",
|
||||
@@ -284,41 +300,37 @@ priority - 优先级,只对双向同步备份模式有效。选项支持三种
|
||||
Usage: "local delay time,本地文件修改检测延迟间隔,单位秒。如果本地文件会被频繁修改,例如录制视频文件,配置好该时间可以避免上传未录制好的文件",
|
||||
Value: 3,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "step",
|
||||
Usage: "task step 任务步骤, 支持两种: scan(只扫描并建立同步数据库),sync(正常启动同步任务)",
|
||||
Value: "sync",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunSync(defaultTask *syncdrive.SyncTask, fileDownloadParallel, fileUploadParallel int, downloadBlockSize, uploadBlockSize int64,
|
||||
flag syncdrive.SyncPriorityOption, localDelayTime int, taskStep syncdrive.TaskStep) {
|
||||
useInternalUrl := config.Config.TransferUrlType == 2
|
||||
func RunSync(defaultTask *syncdrive.SyncTask, cycleMode syncdrive.CycleMode, fileDownloadParallel, fileUploadParallel int, downloadBlockSize, uploadBlockSize int64,
|
||||
flag syncdrive.SyncPriorityOption, localDelayTime int) {
|
||||
maxDownloadRate := config.Config.MaxDownloadRate
|
||||
maxUploadRate := config.Config.MaxUploadRate
|
||||
activeUser := GetActiveUser()
|
||||
panClient := activeUser.PanClient()
|
||||
panClient.DisableCache()
|
||||
panClient.OpenapiPanClient().ClearCache()
|
||||
panClient.OpenapiPanClient().DisableCache()
|
||||
|
||||
// pan token expired checker
|
||||
continueFlag := int32(0)
|
||||
atomic.StoreInt32(&continueFlag, 0)
|
||||
defer func() {
|
||||
atomic.StoreInt32(&continueFlag, 1)
|
||||
}()
|
||||
go func(flag *int32) {
|
||||
for atomic.LoadInt32(flag) == 0 {
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
logger.Verboseln("update access token for sync task")
|
||||
panClient.UpdateToken(activeUser.WebToken)
|
||||
}
|
||||
}
|
||||
}(&continueFlag)
|
||||
//// pan token expired checker
|
||||
//continueFlag := int32(0)
|
||||
//atomic.StoreInt32(&continueFlag, 0)
|
||||
//defer func() {
|
||||
// atomic.StoreInt32(&continueFlag, 1)
|
||||
//}()
|
||||
//go func(flag *int32) {
|
||||
// for atomic.LoadInt32(flag) == 0 {
|
||||
// time.Sleep(time.Duration(1) * time.Minute)
|
||||
// if RefreshWebTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
// logger.Verboseln("update access token for sync task")
|
||||
// userWebToken := NewWebLoginToken(activeUser.WebapiToken.AccessToken, activeUser.WebapiToken.Expired)
|
||||
// panClient.WebapiPanClient().UpdateToken(userWebToken)
|
||||
// }
|
||||
// }
|
||||
//}(&continueFlag)
|
||||
|
||||
syncFolderRootPath := config.GetSyncDriveDir()
|
||||
if b, e := utils.PathExists(syncFolderRootPath); e == nil {
|
||||
@@ -334,10 +346,6 @@ func RunSync(defaultTask *syncdrive.SyncTask, fileDownloadParallel, fileUploadPa
|
||||
}
|
||||
|
||||
fmt.Println("启动同步备份进程")
|
||||
typeUrlStr := "默认链接"
|
||||
if useInternalUrl {
|
||||
typeUrlStr = "阿里ECS内部链接"
|
||||
}
|
||||
|
||||
// 文件同步记录器
|
||||
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/sync_file_records.csv")
|
||||
@@ -347,37 +355,46 @@ func RunSync(defaultTask *syncdrive.SyncTask, fileDownloadParallel, fileUploadPa
|
||||
FileUploadParallel: fileUploadParallel,
|
||||
FileDownloadBlockSize: downloadBlockSize,
|
||||
FileUploadBlockSize: uploadBlockSize,
|
||||
UseInternalUrl: useInternalUrl,
|
||||
MaxDownloadRate: maxDownloadRate,
|
||||
MaxUploadRate: maxUploadRate,
|
||||
SyncPriority: flag,
|
||||
LocalFileModifiedCheckIntervalSec: localDelayTime,
|
||||
FileRecorder: fileRecorder,
|
||||
}
|
||||
syncMgr := syncdrive.NewSyncTaskManager(activeUser, activeUser.DriveList.GetFileDriveId(), panClient, syncFolderRootPath, option)
|
||||
syncMgr := syncdrive.NewSyncTaskManager(activeUser, panClient, syncFolderRootPath, option)
|
||||
syncConfigFile := syncMgr.ConfigFilePath()
|
||||
if tasks != nil {
|
||||
syncConfigFile = "(使用命令行配置)"
|
||||
}
|
||||
fmt.Printf("备份配置文件:%s\n链接类型:%s\n下载并发:%d\n上传并发:%d\n下载分片大小:%s\n上传分片大小:%s\n",
|
||||
syncConfigFile, typeUrlStr, fileDownloadParallel, fileUploadParallel, converter.ConvertFileSize(downloadBlockSize, 2),
|
||||
fmt.Printf("备份配置文件:%s\n下载并发:%d\n上传并发:%d\n下载分片大小:%s\n上传分片大小:%s\n",
|
||||
syncConfigFile, fileDownloadParallel, fileUploadParallel, converter.ConvertFileSize(downloadBlockSize, 2),
|
||||
converter.ConvertFileSize(uploadBlockSize, 2))
|
||||
if _, e := syncMgr.Start(tasks, taskStep); e != nil {
|
||||
if _, e := syncMgr.Start(tasks, cycleMode); e != nil {
|
||||
fmt.Println("启动任务失败:", e)
|
||||
return
|
||||
}
|
||||
|
||||
if taskStep != syncdrive.StepScanFile {
|
||||
_, ok := os.LookupEnv("ALIYUNPAN_DOCKER")
|
||||
if ok {
|
||||
// in docker container
|
||||
_, ok := os.LookupEnv("ALIYUNPAN_DOCKER")
|
||||
if ok {
|
||||
// in docker container
|
||||
if cycleMode == syncdrive.CycleInfiniteLoop {
|
||||
// 使用休眠以节省CPU资源
|
||||
fmt.Println("本命令不会退出,程序正在以Docker的方式运行。如需退出请借助Docker提供的方式。")
|
||||
for {
|
||||
time.Sleep(60 * time.Second)
|
||||
}
|
||||
} else {
|
||||
if config.IsAppInCliMode {
|
||||
for {
|
||||
if syncMgr.IsAllTaskCompletely() {
|
||||
fmt.Println("所有备份任务已完成")
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if cycleMode == syncdrive.CycleInfiniteLoop {
|
||||
if global.IsAppInCliMode {
|
||||
// in cmd mode
|
||||
c := ""
|
||||
fmt.Println("本命令不会退出,如需要结束同步备份进程请输入y,然后按Enter键进行停止。")
|
||||
@@ -391,15 +408,19 @@ func RunSync(defaultTask *syncdrive.SyncTask, fileDownloadParallel, fileUploadPa
|
||||
time.Sleep(60 * time.Second)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for {
|
||||
if syncMgr.IsAllTaskCompletely() {
|
||||
fmt.Println("所有备份任务已完成")
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("正在停止同步备份任务,请稍等...")
|
||||
}
|
||||
|
||||
fmt.Println("正在退出同步备份任务,请稍等...")
|
||||
|
||||
// stop task
|
||||
syncMgr.Stop(taskStep)
|
||||
|
||||
if taskStep == syncdrive.StepScanFile {
|
||||
fmt.Println("\n已完成文件扫描和同步数据库的构建,可以启动任务同步了")
|
||||
}
|
||||
syncMgr.Stop()
|
||||
}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func CmdToken() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "token",
|
||||
Usage: "Token相关操作",
|
||||
UsageText: cmder.App().Name + " token",
|
||||
Category: "阿里云盘账号",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
},
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "update",
|
||||
Usage: "更新Token",
|
||||
UsageText: cmder.App().Name + " token update",
|
||||
Description: `
|
||||
示例:
|
||||
|
||||
更新当前登录用户的Token
|
||||
aliyunpan token update -mode 1
|
||||
|
||||
更新全部登录用户的Token
|
||||
aliyunpan token update -mode 2
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号,无需刷新Token")
|
||||
return nil
|
||||
}
|
||||
|
||||
modeFlag := "1"
|
||||
if c.IsSet("mode") {
|
||||
modeFlag = c.String("mode")
|
||||
}
|
||||
RunTokenUpdate(modeFlag)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "mode",
|
||||
Usage: "模式,1-登录用户,2-全部用户",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunTokenUpdate 执行Token更新
|
||||
func RunTokenUpdate(modeFlag string) {
|
||||
ReloadConfigFunc(nil)
|
||||
|
||||
// 获取当前插件
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
params := &plugins.UserTokenRefreshFinishParams{
|
||||
Result: "success",
|
||||
Message: "",
|
||||
OldToken: "",
|
||||
NewToken: "",
|
||||
UpdatedAt: utils.NowTimeStr(),
|
||||
}
|
||||
|
||||
userList := config.Config.UserList
|
||||
if userList == nil || len(userList) == 0 {
|
||||
fmt.Printf("没有登录用户,无需刷新Token\n")
|
||||
return
|
||||
}
|
||||
for _, user := range userList {
|
||||
params.Result = "success"
|
||||
|
||||
if modeFlag == "1" {
|
||||
if user.UserId != config.Config.ActiveUID {
|
||||
continue
|
||||
}
|
||||
}
|
||||
newToken, e := aliyunpan.GetAccessTokenFromRefreshToken(user.RefreshToken)
|
||||
if e != nil {
|
||||
params.Result = "fail"
|
||||
params.Message = e.Error()
|
||||
fmt.Printf("无法为%s用户获取新的RefreshToken,可能需要重新登录\n", user.Nickname)
|
||||
continue
|
||||
}
|
||||
if newToken != nil && newToken.RefreshToken != "" {
|
||||
params.OldToken = user.RefreshToken
|
||||
params.NewToken = newToken.RefreshToken
|
||||
|
||||
user.RefreshToken = newToken.RefreshToken
|
||||
user.WebToken = *newToken
|
||||
fmt.Printf("成功刷新%s用户的RefreshToken\n", user.Nickname)
|
||||
} else {
|
||||
params.Result = "fail"
|
||||
}
|
||||
|
||||
// plugin callback
|
||||
if er := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(user), params); er != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er.Error())
|
||||
}
|
||||
}
|
||||
SaveConfigFunc(nil)
|
||||
}
|
||||
@@ -113,23 +113,31 @@ func getTree(driveId, pathStr string, depth int, statistic *treeStatistic, setti
|
||||
pathStr = activeUser.PathJoin(driveId, pathStr)
|
||||
pathStr = path.Clean(pathStr)
|
||||
|
||||
files, err := matchPathByShellPattern(driveId, pathStr)
|
||||
// 获取目标路径文件信息
|
||||
targetPathInfo, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, pathStr)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
var targetPathInfo *aliyunpan.FileEntity
|
||||
if len(files) == 1 {
|
||||
targetPathInfo = files[0]
|
||||
} else {
|
||||
for _, f := range files {
|
||||
if f.IsFolder() {
|
||||
targetPathInfo = f
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// 适配通配符路径获取目标文件信息(弃用,容易触发风控)
|
||||
//files, err := matchPathByShellPattern(driveId, pathStr)
|
||||
//if err != nil {
|
||||
// fmt.Println(err)
|
||||
// return
|
||||
//}
|
||||
//var targetPathInfo *aliyunpan.FileEntity
|
||||
//if len(files) == 1 {
|
||||
// targetPathInfo = files[0]
|
||||
//} else {
|
||||
// for _, f := range files {
|
||||
// if f.IsFolder() {
|
||||
// targetPathInfo = f
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
if targetPathInfo == nil {
|
||||
fmt.Println("路径不存在")
|
||||
return
|
||||
@@ -146,7 +154,7 @@ func getTree(driveId, pathStr string, depth int, statistic *treeStatistic, setti
|
||||
fileListParam.OrderBy = aliyunpan.FileOrderByName
|
||||
fileListParam.OrderDirection = aliyunpan.FileOrderDirectionAsc
|
||||
if targetPathInfo.IsFolder() {
|
||||
fileResult, err := activeUser.PanClient().FileListGetAll(fileListParam, 500)
|
||||
fileResult, err := activeUser.PanClient().OpenapiPanClient().FileListGetAll(fileListParam, 500)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
@@ -169,7 +177,7 @@ func getTree(driveId, pathStr string, depth int, statistic *treeStatistic, setti
|
||||
} else {
|
||||
fmt.Printf("%v%v %v/\n", indentPrefixStr, pathPrefix, file.FileName)
|
||||
}
|
||||
getTree(driveId, targetPathInfo.Path+"/"+file.Path, depth+1, statistic, setting)
|
||||
getTree(driveId, targetPathInfo.Path+"/"+file.FileName, depth+1, statistic, setting)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -214,9 +222,9 @@ func getTree(driveId, pathStr string, depth int, statistic *treeStatistic, setti
|
||||
// RunTree 列出树形图
|
||||
func RunTree(driveId, pathStr string, showFullPath, showFileSize bool, minSize, maxSize int64) {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
activeUser.PanClient().ClearCache()
|
||||
activeUser.PanClient().EnableCache()
|
||||
defer activeUser.PanClient().DisableCache()
|
||||
activeUser.PanClient().OpenapiPanClient().ClearCache()
|
||||
activeUser.PanClient().OpenapiPanClient().EnableCache()
|
||||
defer activeUser.PanClient().OpenapiPanClient().DisableCache()
|
||||
pathStr = activeUser.PathJoin(driveId, pathStr)
|
||||
statistic := &treeStatistic{
|
||||
CountOfDir: 0,
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/tickstep/library-go/logger"
|
||||
@@ -66,7 +65,6 @@ type (
|
||||
DriveId string
|
||||
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行上传,支持正则表达式
|
||||
BlockSize int64 // 分片大小
|
||||
UseInternalUrl bool // 是否使用内置链接
|
||||
}
|
||||
)
|
||||
|
||||
@@ -217,79 +215,27 @@ func CmdUpload() cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func CmdRapidUpload() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "rapidupload",
|
||||
Aliases: []string{"ru"},
|
||||
Usage: "手动秒传文件",
|
||||
UsageText: cmder.App().Name + " rapidupload \"aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder\"",
|
||||
Description: `
|
||||
使用此功能秒传文件, 前提是知道文件的大小, sha1, 且网盘中存在一模一样的文件.
|
||||
上传的文件将会保存到网盘的目标目录。文件的秒传链接可以通过share或者export命令获取。
|
||||
|
||||
链接格式说明:aliyunpan://文件名|sha1|文件大小|<相对路径>
|
||||
"相对路径" 可以为空,为空代表存储到网盘根目录
|
||||
|
||||
示例:
|
||||
1. 如果秒传成功, 则保存到网盘路径 /pan_folder/file.dmg
|
||||
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder"
|
||||
|
||||
2. 如果秒传成功, 则保存到网盘路径 /file.dmg
|
||||
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|"
|
||||
|
||||
3. 同时秒传多个文件,如果秒传成功, 则保存到网盘路径 /pan_folder/file.dmg, /pan_folder/file1.dmg
|
||||
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder" "aliyunpan://file1.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder"
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() <= 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
RunRapidUpload(parseDriveId(c), c.Bool("ow"), subArgs, c.String("path"))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "ow",
|
||||
Usage: "overwrite, 覆盖已存在的文件。已存在的文件会并移到回收站",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "path",
|
||||
Usage: "存储到网盘目录,绝对路径,例如:/myfolder",
|
||||
Value: "",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunUpload 执行文件上传
|
||||
func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
activeUser := GetActiveUser()
|
||||
activeUser.PanClient().EnableCache()
|
||||
activeUser.PanClient().ClearCache()
|
||||
defer activeUser.PanClient().DisableCache()
|
||||
// pan token expired checker
|
||||
continueFlag := int32(0)
|
||||
atomic.StoreInt32(&continueFlag, 0)
|
||||
defer func() {
|
||||
atomic.StoreInt32(&continueFlag, 1)
|
||||
}()
|
||||
go func(flag *int32) {
|
||||
for atomic.LoadInt32(flag) == 0 {
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
logger.Verboseln("update access token for upload task")
|
||||
}
|
||||
}
|
||||
}(&continueFlag)
|
||||
activeUser.PanClient().OpenapiPanClient().EnableCache()
|
||||
activeUser.PanClient().OpenapiPanClient().ClearCache()
|
||||
defer activeUser.PanClient().OpenapiPanClient().DisableCache()
|
||||
|
||||
//// pan token expired checker
|
||||
//continueFlag := int32(0)
|
||||
//atomic.StoreInt32(&continueFlag, 0)
|
||||
//defer func() {
|
||||
// atomic.StoreInt32(&continueFlag, 1)
|
||||
//}()
|
||||
//go func(flag *int32) {
|
||||
// for atomic.LoadInt32(flag) == 0 {
|
||||
// time.Sleep(time.Duration(1) * time.Minute)
|
||||
// if RefreshWebTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
// logger.Verboseln("update access token for upload task")
|
||||
// }
|
||||
// }
|
||||
//}(&continueFlag)
|
||||
|
||||
if opt == nil {
|
||||
opt = &UploadOptions{}
|
||||
@@ -312,17 +258,16 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
if opt.MaxRetry < 0 {
|
||||
opt.MaxRetry = DefaultUploadMaxRetry
|
||||
}
|
||||
opt.UseInternalUrl = config.Config.TransferUrlType == 2
|
||||
|
||||
// 超时时间
|
||||
if opt.MaxTimeoutSec > 0 {
|
||||
activeUser.PanClient().SetTimeout(time.Duration(opt.MaxTimeoutSec) * time.Second)
|
||||
activeUser.PanClient().OpenapiPanClient().SetTimeout(time.Duration(opt.MaxTimeoutSec) * time.Second)
|
||||
}
|
||||
|
||||
fmt.Printf("\n[0] 当前文件上传最大并发量为: %d, 上传分片大小为: %s\n", opt.AllParallel, converter.ConvertFileSize(opt.BlockSize, 2))
|
||||
|
||||
savePath = activeUser.PathJoin(opt.DriveId, savePath)
|
||||
_, err1 := activeUser.PanClient().FileInfoByPath(opt.DriveId, savePath)
|
||||
_, err1 := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(opt.DriveId, savePath)
|
||||
if err1 != nil {
|
||||
fmt.Printf("警告: 上传文件, 获取云盘路径 %s 错误, %s\n", savePath, err1)
|
||||
}
|
||||
@@ -333,7 +278,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
return
|
||||
}
|
||||
|
||||
// 打开上传状态
|
||||
// 打开上传状态数据库
|
||||
uploadDatabase, err := panupload.NewUploadingDatabase()
|
||||
if err != nil {
|
||||
fmt.Printf("打开上传未完成数据库错误: %s\n", err)
|
||||
@@ -451,7 +396,6 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
ShowProgress: opt.ShowProgress,
|
||||
IsOverwrite: opt.IsOverwrite,
|
||||
IsSkipSameName: opt.IsSkipSameName,
|
||||
UseInternalUrl: opt.UseInternalUrl,
|
||||
GlobalSpeedsStat: globalSpeedsStat,
|
||||
FileRecorder: fileRecorder,
|
||||
}, opt.MaxRetry)
|
||||
@@ -463,11 +407,11 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
if saveFilePath != "/" {
|
||||
folderCreateMutex.Lock()
|
||||
fmt.Printf("正在检测和创建云盘文件夹: %s\n", saveFilePath)
|
||||
_, apierr1 := activeUser.PanClient().FileInfoByPath(opt.DriveId, saveFilePath)
|
||||
_, apierr1 := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(opt.DriveId, saveFilePath)
|
||||
time.Sleep(1 * time.Second)
|
||||
if apierr1 != nil && apierr1.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
logger.Verbosef("%s 创建云盘文件夹: %s\n", utils.NowTimeStr(), saveFilePath)
|
||||
rs, apierr := activeUser.PanClient().Mkdir(opt.DriveId, "root", saveFilePath)
|
||||
rs, apierr := activeUser.PanClient().OpenapiPanClient().MkdirByFullPath(opt.DriveId, saveFilePath)
|
||||
if apierr != nil || rs.FileId == "" {
|
||||
fmt.Printf("创建云盘文件夹失败: %s\n", saveFilePath)
|
||||
}
|
||||
@@ -511,108 +455,3 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
}
|
||||
activeUser.DeleteCache(GetAllPathFolderByPath(savePath))
|
||||
}
|
||||
|
||||
// RunRapidUpload 秒传
|
||||
func RunRapidUpload(driveId string, isOverwrite bool, fileMetaList []string, savePanPath string) {
|
||||
activeUser := GetActiveUser()
|
||||
savePanPath = activeUser.PathJoin(driveId, savePanPath)
|
||||
|
||||
if len(fileMetaList) == 0 {
|
||||
fmt.Println("秒传链接为空")
|
||||
return
|
||||
}
|
||||
|
||||
items := []*RapidUploadItem{}
|
||||
// parse file meta strings
|
||||
for _, fileMeta := range fileMetaList {
|
||||
item, e := newRapidUploadItem(fileMeta)
|
||||
if e != nil {
|
||||
fmt.Println(e)
|
||||
continue
|
||||
}
|
||||
if item == nil {
|
||||
fmt.Println("秒传链接格式错误: ", fileMeta)
|
||||
continue
|
||||
}
|
||||
|
||||
// pan path
|
||||
item.FilePath = path.Join(savePanPath, item.FilePath)
|
||||
|
||||
// append
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
// upload one by one
|
||||
for _, item := range items {
|
||||
fmt.Println("准备秒传:", item.FilePath)
|
||||
if ee := doRapidUpload(driveId, isOverwrite, item); ee != nil {
|
||||
fmt.Println(ee)
|
||||
} else {
|
||||
fmt.Printf("秒传成功, 保存到网盘路径:%s\n", item.FilePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doRapidUpload(driveId string, isOverwrite bool, item *RapidUploadItem) error {
|
||||
activeUser := GetActiveUser()
|
||||
panClient := activeUser.PanClient()
|
||||
|
||||
var apierr *apierror.ApiError
|
||||
var rs *aliyunpan.MkdirResult
|
||||
var appCreateUploadFileParam *aliyunpan.CreateFileUploadParam
|
||||
var saveFilePath string
|
||||
|
||||
panDir, panFileName := path.Split(item.FilePath)
|
||||
saveFilePath = item.FilePath
|
||||
if panDir != "/" {
|
||||
rs, apierr = panClient.MkdirRecursive(driveId, "", "", 0, strings.Split(path.Clean(panDir), "/"))
|
||||
if apierr != nil || rs.FileId == "" {
|
||||
return fmt.Errorf("创建云盘文件夹失败")
|
||||
}
|
||||
} else {
|
||||
rs = &aliyunpan.MkdirResult{}
|
||||
rs.FileId = aliyunpan.DefaultRootParentFileId
|
||||
}
|
||||
time.Sleep(time.Duration(2) * time.Second)
|
||||
|
||||
if isOverwrite {
|
||||
// 标记覆盖旧同名文件
|
||||
// 检查同名文件是否存在
|
||||
efi, apierr := panClient.FileInfoByPath(driveId, saveFilePath)
|
||||
if apierr != nil && apierr.Code != apierror.ApiCodeFileNotFoundCode {
|
||||
return fmt.Errorf("检测同名文件失败,请稍后重试")
|
||||
}
|
||||
if efi != nil && efi.FileId != "" {
|
||||
// existed, delete it
|
||||
fileDeleteResult, err1 := panClient.FileDelete([]*aliyunpan.FileBatchActionParam{{DriveId: efi.DriveId, FileId: efi.FileId}})
|
||||
if err1 != nil || len(fileDeleteResult) == 0 {
|
||||
return fmt.Errorf("无法删除文件,请稍后重试")
|
||||
}
|
||||
time.Sleep(time.Duration(500) * time.Millisecond)
|
||||
if fileDeleteResult[0].Success {
|
||||
logger.Verboseln("检测到同名文件,已移动到回收站: ", saveFilePath)
|
||||
} else {
|
||||
return fmt.Errorf("无法删除文件,请稍后重试")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appCreateUploadFileParam = &aliyunpan.CreateFileUploadParam{
|
||||
DriveId: driveId,
|
||||
Name: panFileName,
|
||||
Size: item.FileSize,
|
||||
ContentHash: item.FileSha1,
|
||||
ParentFileId: rs.FileId,
|
||||
}
|
||||
uploadOpEntity, apierr := panClient.CreateUploadFile(appCreateUploadFileParam)
|
||||
if apierr != nil {
|
||||
return fmt.Errorf("创建秒传任务失败:" + apierr.Error())
|
||||
}
|
||||
|
||||
if uploadOpEntity.RapidUpload {
|
||||
logger.Verboseln("秒传成功, 保存到网盘路径: ", path.Join(panDir, uploadOpEntity.FileName))
|
||||
} else {
|
||||
return fmt.Errorf("失败,文件未曾上传,无法秒传")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -15,7 +15,6 @@ package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
@@ -47,7 +46,7 @@ func CmdSu() cli.Command {
|
||||
|
||||
示例:
|
||||
aliyunpan su
|
||||
aliyunpan su <uid or name>
|
||||
aliyunpan su <uid>
|
||||
`,
|
||||
Category: "阿里云盘账号",
|
||||
Before: ReloadConfigFunc,
|
||||
@@ -95,7 +94,7 @@ func CmdSu() cli.Command {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
}
|
||||
|
||||
switchedUser, err := config.Config.SwitchUser(uid, inputData)
|
||||
switchedUser, err := config.Config.SwitchUser(uid)
|
||||
if err != nil {
|
||||
fmt.Printf("切换用户失败, %s\n", err)
|
||||
return nil
|
||||
@@ -130,12 +129,8 @@ func CmdWho() cli.Command {
|
||||
}
|
||||
activeUser := config.Config.ActiveUser()
|
||||
cloudName := activeUser.GetDriveById(activeUser.ActiveDriveId).DriveName
|
||||
fmt.Printf("当前帐号UID: %s, 昵称: %s, 用户名: %s, 网盘:%s\n", activeUser.UserId, activeUser.Nickname, activeUser.AccountName, cloudName)
|
||||
fmt.Printf("当前帐号UID: %s, 昵称: %s, 用户名: %s, 当前使用网盘:%s\n", activeUser.UserId, activeUser.Nickname, activeUser.AccountName, cloudName)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunGetUserInfo() (userInfo *aliyunpan.UserInfo, error error) {
|
||||
return GetActivePanClient().GetUserInfo()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -16,8 +16,7 @@ package command
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdliner"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
@@ -60,7 +59,7 @@ var (
|
||||
// RunTestShellPattern 执行测试通配符
|
||||
func RunTestShellPattern(driveId string, pattern string) {
|
||||
acUser := GetActiveUser()
|
||||
files, err := acUser.PanClient().MatchPathByShellPattern(driveId, GetActiveUser().PathJoin(driveId, pattern))
|
||||
files, err := acUser.PanClient().OpenapiPanClient().MatchPathByShellPattern(driveId, GetActiveUser().PathJoin(driveId, pattern))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
@@ -75,7 +74,7 @@ func RunTestShellPattern(driveId string, pattern string) {
|
||||
func matchPathByShellPattern(driveId string, patterns ...string) (files []*aliyunpan.FileEntity, e error) {
|
||||
acUser := GetActiveUser()
|
||||
for k := range patterns {
|
||||
ps, err := acUser.PanClient().MatchPathByShellPattern(driveId, acUser.PathJoin(driveId, patterns[k]))
|
||||
ps, err := acUser.PanClient().OpenapiPanClient().MatchPathByShellPattern(driveId, acUser.PathJoin(driveId, patterns[k]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -128,39 +127,62 @@ func UnescapeStr(s string) string {
|
||||
return r
|
||||
}
|
||||
|
||||
// RefreshTokenInNeed 刷新refresh token
|
||||
func RefreshTokenInNeed(activeUser *config.PanUser, deviceName string) bool {
|
||||
func NewWebLoginToken(accessToken string, expired int64) aliyunpan_web.WebLoginToken {
|
||||
webapiToken := &config.PanClientToken{
|
||||
AccessToken: accessToken,
|
||||
Expired: expired,
|
||||
}
|
||||
return aliyunpan_web.WebLoginToken{
|
||||
AccessTokenType: "Bearer",
|
||||
AccessToken: webapiToken.AccessToken,
|
||||
RefreshToken: "",
|
||||
ExpiresIn: 7200,
|
||||
ExpireTime: webapiToken.GetExpiredTimeCstStr(),
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshWebTokenInNeed 刷新 webapi access token
|
||||
func RefreshWebTokenInNeed(activeUser *config.PanUser, deviceName string) bool {
|
||||
if activeUser == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// refresh expired token
|
||||
if activeUser.PanClient() != nil {
|
||||
if len(activeUser.WebToken.RefreshToken) > 0 {
|
||||
// refresh expired web token
|
||||
if activeUser.PanClient().WebapiPanClient() != nil {
|
||||
if activeUser.WebapiToken != nil && len(activeUser.WebapiToken.AccessToken) > 0 {
|
||||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||||
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", activeUser.WebToken.ExpireTime, cz)
|
||||
expiredTime := time.Unix(activeUser.WebapiToken.Expired, 0).In(cz)
|
||||
now := time.Now()
|
||||
if (expiredTime.Unix() - now.Unix()) <= (20 * 60) { // 20min
|
||||
if (expiredTime.Unix() - now.Unix()) <= (2 * 60) { // 有效期小于2min就刷新
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
params := &plugins.UserTokenRefreshFinishParams{
|
||||
Result: "success",
|
||||
Message: "",
|
||||
Message: "webapi",
|
||||
OldToken: "",
|
||||
NewToken: "",
|
||||
UpdatedAt: utils.NowTimeStr(),
|
||||
}
|
||||
|
||||
// need update refresh token
|
||||
logger.Verboseln("access token expired, get new from refresh token")
|
||||
if wt, er := aliyunpan.GetAccessTokenFromRefreshToken(activeUser.RefreshToken); er == nil {
|
||||
logger.Verboseln("web access token expired, get new from server")
|
||||
loginHelper := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
|
||||
wt, e := loginHelper.GetWebapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().WebapiPanClient().GetAccessToken())
|
||||
if e != nil {
|
||||
logger.Verboseln("get web token from server error: ", e)
|
||||
}
|
||||
if wt != nil {
|
||||
params.Result = "success"
|
||||
params.OldToken = activeUser.RefreshToken
|
||||
params.NewToken = wt.RefreshToken
|
||||
params.OldToken = activeUser.WebapiToken.AccessToken
|
||||
params.NewToken = wt.AccessToken
|
||||
|
||||
activeUser.RefreshToken = wt.RefreshToken
|
||||
activeUser.WebToken = *wt
|
||||
activeUser.PanClient().UpdateToken(*wt)
|
||||
// update for user & client
|
||||
userWebToken := NewWebLoginToken(wt.AccessToken, wt.Expired)
|
||||
activeUser.WebapiToken = &config.PanClientToken{
|
||||
AccessToken: wt.AccessToken,
|
||||
Expired: wt.Expired,
|
||||
}
|
||||
activeUser.PanClient().WebapiPanClient().UpdateToken(userWebToken)
|
||||
logger.Verboseln("get new access token success")
|
||||
|
||||
// plugin callback
|
||||
@@ -169,9 +191,9 @@ func RefreshTokenInNeed(activeUser *config.PanUser, deviceName string) bool {
|
||||
}
|
||||
|
||||
// create new signature
|
||||
_, e := activeUser.PanClient().CreateSession(nil)
|
||||
_, e := activeUser.PanClient().WebapiPanClient().CreateSession(nil)
|
||||
if e != nil {
|
||||
logger.Verboseln("call CreateSession error in RefreshTokenInNeed: " + e.Error())
|
||||
logger.Verboseln("call CreateSession error in RefreshWebTokenInNeed: " + e.Error())
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
@@ -179,8 +201,74 @@ func RefreshTokenInNeed(activeUser *config.PanUser, deviceName string) bool {
|
||||
// if token has expired, callback plugin api for notify
|
||||
if now.Unix() >= expiredTime.Unix() {
|
||||
params.Result = "fail"
|
||||
params.Message = er.Error()
|
||||
params.OldToken = activeUser.RefreshToken
|
||||
params.Message = e.Error()
|
||||
params.OldToken = activeUser.WebapiToken.AccessToken
|
||||
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RefreshOpenTokenInNeed 刷新 openapi access token
|
||||
func RefreshOpenTokenInNeed(activeUser *config.PanUser) bool {
|
||||
if activeUser == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// refresh expired openapi token
|
||||
if activeUser.PanClient().OpenapiPanClient() != nil {
|
||||
if activeUser.OpenapiToken != nil && len(activeUser.OpenapiToken.AccessToken) > 0 {
|
||||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||||
expiredTime := time.Unix(activeUser.OpenapiToken.Expired, 0).In(cz)
|
||||
now := time.Now().In(cz)
|
||||
if (expiredTime.Unix() - now.Unix()) <= (2 * 60) { // 有效期小于2min就刷新
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
params := &plugins.UserTokenRefreshFinishParams{
|
||||
Result: "success",
|
||||
Message: "openapi",
|
||||
OldToken: "",
|
||||
NewToken: "",
|
||||
UpdatedAt: utils.NowTimeStr(),
|
||||
}
|
||||
|
||||
// need update refresh token
|
||||
logger.Verboseln("openapi access token expired, get new from server")
|
||||
loginHelper := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
|
||||
wt, e := loginHelper.GetOpenapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().OpenapiPanClient().GetAccessToken())
|
||||
if e != nil {
|
||||
logger.Verboseln("get openapi token from server error: ", e)
|
||||
}
|
||||
if wt != nil {
|
||||
params.Result = "success"
|
||||
params.OldToken = activeUser.OpenapiToken.AccessToken
|
||||
params.NewToken = wt.AccessToken
|
||||
|
||||
// update for user
|
||||
activeUser.OpenapiToken = &config.PanClientToken{
|
||||
AccessToken: wt.AccessToken,
|
||||
Expired: wt.Expired,
|
||||
}
|
||||
logger.Verboseln("get new access token success")
|
||||
|
||||
// plugin callback
|
||||
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
|
||||
}
|
||||
|
||||
return true
|
||||
} else {
|
||||
// token refresh error
|
||||
// if token has expired, callback plugin api for notify
|
||||
if now.Unix() >= expiredTime.Unix() {
|
||||
params.Result = "fail"
|
||||
params.Message = e.Error()
|
||||
params.OldToken = activeUser.WebapiToken.AccessToken
|
||||
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
|
||||
}
|
||||
@@ -205,110 +293,27 @@ func isMatchWildcardPattern(name string) bool {
|
||||
return strings.ContainsAny(name, "*") || strings.ContainsAny(name, "?") || strings.ContainsAny(name, "[")
|
||||
}
|
||||
|
||||
// DoLoginHelper 登录助手,使用token进行登录
|
||||
func DoLoginHelper(refreshToken string) (refreshTokenStr string, webToken aliyunpan.WebLoginToken, error error) {
|
||||
line := cmdliner.NewLiner()
|
||||
defer line.Close()
|
||||
|
||||
if refreshToken == "" {
|
||||
refreshToken, error = line.State.Prompt("请输入RefreshToken, 回车键提交 > ")
|
||||
if error != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// app login
|
||||
atoken, apperr := aliyunpan.GetAccessTokenFromRefreshToken(refreshToken)
|
||||
if apperr != nil {
|
||||
if apperr.Code == apierror.ApiCodeTokenExpiredCode || apperr.Code == apierror.ApiCodeRefreshTokenExpiredCode {
|
||||
fmt.Println("Token过期,需要重新登录")
|
||||
} else {
|
||||
fmt.Println("Token登录失败:", apperr)
|
||||
}
|
||||
return "", webToken, fmt.Errorf("登录失败")
|
||||
}
|
||||
refreshTokenStr = refreshToken
|
||||
return refreshTokenStr, *atoken, nil
|
||||
}
|
||||
|
||||
// TryLogin 尝试登录,基础应用支持的各类登录方式进行尝试成功登录
|
||||
func TryLogin() *config.PanUser {
|
||||
// 获取当前插件
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
params := &plugins.UserTokenRefreshFinishParams{
|
||||
Result: "success",
|
||||
Message: "",
|
||||
OldToken: "",
|
||||
NewToken: "",
|
||||
UpdatedAt: utils.NowTimeStr(),
|
||||
}
|
||||
|
||||
// can do automatically login?
|
||||
for _, u := range config.Config.UserList {
|
||||
if u.UserId == config.Config.ActiveUID {
|
||||
// login
|
||||
_, webToken, err := DoLoginHelper(u.RefreshToken)
|
||||
if err != nil {
|
||||
logger.Verboseln("automatically login use saved refresh token error ", err)
|
||||
if u.TokenId != "" {
|
||||
logger.Verboseln("try to login use tokenId")
|
||||
h := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
|
||||
r, e := h.GetRefreshToken(u.TokenId)
|
||||
if e != nil {
|
||||
logger.Verboseln("try to login use tokenId error", e)
|
||||
// login fail plugin callback
|
||||
params.Result = "fail"
|
||||
params.OldToken = u.RefreshToken
|
||||
params.NewToken = ""
|
||||
if er := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(u), params); er != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er.Error())
|
||||
}
|
||||
break
|
||||
}
|
||||
refreshToken, e := h.ParseSecureRefreshToken("", r.SecureRefreshToken)
|
||||
if e != nil {
|
||||
logger.Verboseln("try to parse refresh token error", e)
|
||||
break
|
||||
}
|
||||
_, webToken, err = DoLoginHelper(refreshToken)
|
||||
if err != nil {
|
||||
logger.Verboseln("try to use refresh token from tokenId error", e)
|
||||
break
|
||||
}
|
||||
fmt.Println("Token重新自动登录成功")
|
||||
// save new refresh token
|
||||
u.RefreshToken = refreshToken
|
||||
} else {
|
||||
// login fail plugin callback
|
||||
params.Result = "fail"
|
||||
params.OldToken = u.RefreshToken
|
||||
params.NewToken = ""
|
||||
if er := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(u), params); er != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er.Error())
|
||||
}
|
||||
}
|
||||
break
|
||||
cloudUser, err := config.SetupUserByCookie(u.OpenapiToken, u.WebapiToken,
|
||||
u.TicketId, u.UserId,
|
||||
config.Config.DeviceId, config.Config.DeviceName,
|
||||
config.Config.ClientId, config.Config.ClientSecret)
|
||||
if cloudUser == nil {
|
||||
logger.Verboseln("尝试登录失败: ", err)
|
||||
fmt.Println("尝试登录失败,请使用 login 命令进行重新登录")
|
||||
return nil
|
||||
}
|
||||
// plugin param
|
||||
params.Result = "success"
|
||||
params.OldToken = u.RefreshToken
|
||||
params.NewToken = webToken.RefreshToken
|
||||
|
||||
// success login, save new token and access token
|
||||
u.RefreshToken = webToken.RefreshToken
|
||||
u.WebToken = webToken
|
||||
|
||||
// save
|
||||
SaveConfigFunc(nil)
|
||||
// reload
|
||||
ReloadConfigFunc(nil)
|
||||
|
||||
// do plugin callback
|
||||
if er := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(u), params); er != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er.Error())
|
||||
}
|
||||
|
||||
return config.Config.ActiveUser()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/webdav"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CmdWebdav() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "webdav",
|
||||
Usage: "在线网盘服务",
|
||||
Description: "webdav在线网盘服务",
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
After: SaveConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Print(`
|
||||
本文命令可以让阿里云盘变身为webdav协议的文件服务器。这样你可以把阿里云盘挂载为Windows、Linux、Mac系统的磁盘,可以通过NAS系统做文件管理或文件同步等等。
|
||||
当把阿里云盘作为webdav文件服务器进行使用的时候,上传文件是不支持秒传的,所以当你挂载为网络磁盘使用的时候,不建议在webdav挂载目录中上传、下载过大的文件,不然体验会非常差。
|
||||
建议作为文档,图片等小文件的同步网盘。
|
||||
|
||||
请输入以下命令查看如何启动
|
||||
aliyunpan webdav start -h
|
||||
|
||||
`)
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
},
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "start",
|
||||
Usage: "启动webdav在线网盘服务",
|
||||
UsageText: cmder.App().Name + " webdav start [arguments...]",
|
||||
Description: `
|
||||
启动webdav服务,让阿里云盘变身为webdav协议的文件服务器。这样你可以把阿里云盘挂载为Windows、Linux、Mac系统的磁盘,可以通过NAS系统做文件管理或文件同步等等。
|
||||
当把阿里云盘作为webdav文件服务器进行使用的时候,上传文件是不支持秒传的,所以当你挂载为网络磁盘使用的时候,不建议在webdav挂载目录中上传、下载过大的文件,不然体验会非常差。
|
||||
建议作为文档,图片等小文件的同步网盘。
|
||||
|
||||
例子:
|
||||
1. 查看帮助
|
||||
aliyunpan webdav start -h
|
||||
|
||||
2. 使用默认配置启动webdav服务
|
||||
aliyunpan webdav start
|
||||
|
||||
3. 启动webdav服务,并配置IP为127.0.0.1,端口为23077,登录用户名为admin,登录密码为admin123,模式为读写,文件网盘目录 /webdav_folder 作为服务的根目录
|
||||
aliyunpan webdav start -ip "127.0.0.1" -port 23077 -webdav_user "admin" -webdav_password "admin123" -webdav_mode "rw" -pan_drive "File" -pan_dir_path "/webdav_folder"
|
||||
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号,请先登录")
|
||||
return nil
|
||||
}
|
||||
activeUser := GetActiveUser()
|
||||
|
||||
// pan token expired checker
|
||||
continueFlag := int32(0)
|
||||
atomic.StoreInt32(&continueFlag, 0)
|
||||
defer func() {
|
||||
atomic.StoreInt32(&continueFlag, 1)
|
||||
}()
|
||||
go func(flag *int32) {
|
||||
for atomic.LoadInt32(flag) == 0 {
|
||||
// token刷新
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
//time.Sleep(time.Duration(5) * time.Second)
|
||||
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
logger.Verboseln("reload new access token for webdav")
|
||||
}
|
||||
}
|
||||
}(&continueFlag)
|
||||
|
||||
webdavServ := &webdav.WebdavConfig{
|
||||
PanDriveId: "",
|
||||
PanUserId: "",
|
||||
PanUser: nil,
|
||||
UploadChunkSize: c.Int("bs") * 1024,
|
||||
TransferUrlType: config.Config.TransferUrlType,
|
||||
Address: "0.0.0.0",
|
||||
Port: 23077,
|
||||
Prefix: "/",
|
||||
Users: []webdav.WebdavUser{{
|
||||
Username: "admin",
|
||||
Password: "admin",
|
||||
Scope: "/",
|
||||
Mode: "rw",
|
||||
}},
|
||||
}
|
||||
|
||||
// pan user
|
||||
panUserId := activeUser.UserId
|
||||
webdavServ.PanUserId = panUserId
|
||||
webdavServ.PanUser = activeUser
|
||||
|
||||
// address
|
||||
ip := "0.0.0.0"
|
||||
if c.IsSet("ip") {
|
||||
ip = c.String("ip")
|
||||
}
|
||||
webdavServ.Address = ip
|
||||
|
||||
// port
|
||||
port := 23077
|
||||
if c.IsSet("port") {
|
||||
port = c.Int("port")
|
||||
}
|
||||
webdavServ.Port = port
|
||||
|
||||
// binding pan drive
|
||||
panDriveName := "File"
|
||||
panDriveNameStr := "文件"
|
||||
if c.IsSet("pan_drive") {
|
||||
panDriveName = c.String("pan_drive")
|
||||
}
|
||||
if strings.ToLower(panDriveName) == "album" {
|
||||
webdavServ.PanDriveId = activeUser.DriveList.GetAlbumDriveId()
|
||||
panDriveNameStr = "相册"
|
||||
} else {
|
||||
webdavServ.PanDriveId = activeUser.DriveList.GetFileDriveId()
|
||||
panDriveNameStr = "文件"
|
||||
}
|
||||
|
||||
// binding pan dir path
|
||||
panDirPath := "/"
|
||||
if c.IsSet("pan_dir_path") {
|
||||
panDirPath = c.String("pan_dir_path")
|
||||
}
|
||||
webdavServ.Users[0].Scope = panDirPath
|
||||
|
||||
webdavUserName := "admin"
|
||||
if c.IsSet("webdav_user") {
|
||||
webdavUserName = c.String("webdav_user")
|
||||
}
|
||||
webdavServ.Users[0].Username = webdavUserName
|
||||
|
||||
webdavPassword := "admin"
|
||||
if c.IsSet("webdav_password") {
|
||||
webdavPassword = c.String("webdav_password")
|
||||
}
|
||||
webdavServ.Users[0].Password = webdavPassword
|
||||
|
||||
webdavMode := "rw"
|
||||
if c.IsSet("webdav_mode") {
|
||||
webdavMode = c.String("webdav_mode")
|
||||
}
|
||||
webdavServ.Users[0].Mode = webdavMode
|
||||
if webdavServ.Users[0].Mode != "rw" && webdavServ.Users[0].Mode != "ro" {
|
||||
webdavServ.Users[0].Mode = "rw"
|
||||
}
|
||||
|
||||
err := config.Config.Save()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
modeStr := "读写"
|
||||
if webdavServ.Users[0].Mode == "rw" {
|
||||
modeStr = "读写"
|
||||
} else if webdavServ.Users[0].Mode == "ro" {
|
||||
modeStr = "只读"
|
||||
}
|
||||
|
||||
fmt.Println("----------------------------------------")
|
||||
fmt.Printf("webdav网盘信息:\n链接:http://localhost:%d\n用户名:%s\n密码:%s\n网盘模式:%s\n网盘服务类型:%s\n网盘服务目录:%s\n",
|
||||
webdavServ.Port, webdavServ.Users[0].Username, webdavServ.Users[0].Password, modeStr, panDriveNameStr, webdavServ.Users[0].Scope)
|
||||
fmt.Println("----------------------------------------")
|
||||
fmt.Println("webdav在线网盘服务运行中...")
|
||||
webdavServ.StartServer()
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "ip",
|
||||
Usage: "绑定的本地IP,多网卡的环境下建议指定绑定的IP。默认为0.0.0.0代表绑定全部网卡",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "port",
|
||||
Usage: "绑定的本地端口,默认为:23077",
|
||||
Value: 23077,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "webdav_user",
|
||||
Usage: "Webdav登录用户名,默认为:admin",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "webdav_password",
|
||||
Usage: "Webdav登录密码,默认为:admin",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "webdav_mode",
|
||||
Usage: "Webdav模式,包括:rw-读写,ro-只读,默认为:rw",
|
||||
Value: "rw",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "pan_drive",
|
||||
Usage: "Webdav绑定的网盘类型。File-文件 Album-相册。默认为文件网盘",
|
||||
Value: "File",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "pan_dir_path",
|
||||
Usage: "Webdav绑定的网盘文件夹路径,默认为:/",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "bs",
|
||||
Usage: "block size,上传分片大小,单位KB。推荐值:1024 ~ 10240",
|
||||
Value: 10240,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ package command
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
@@ -55,6 +56,10 @@ func CmdXcp() cli.Command {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
srcDriveId := parseDriveId(c)
|
||||
dstDriveId := ""
|
||||
driveList := config.Config.ActiveUser().DriveList
|
||||
@@ -106,7 +111,7 @@ func RunXCopy(srcDriveId, dstDriveId string, paths ...string) {
|
||||
copyFileParamList = append(copyFileParamList, mfi.FileId)
|
||||
cacheCleanPaths = append(cacheCleanPaths, path.Dir(mfi.Path))
|
||||
}
|
||||
fccr, er := activeUser.PanClient().FileCrossDriveCopy(&aliyunpan.FileCrossCopyParam{
|
||||
fccr, er := activeUser.PanClient().WebapiPanClient().FileCrossDriveCopy(&aliyunpan_web.FileCrossCopyParam{
|
||||
FromDriveId: srcDriveId,
|
||||
FromFileIds: copyFileParamList,
|
||||
ToDriveId: dstDriveId,
|
||||
@@ -151,7 +156,7 @@ func getCrossCopyFileInfo(srcDriveId, dstDriveId string, paths ...string) (opFil
|
||||
// the last one is the target file path
|
||||
targetFilePath := path.Clean(paths[len(paths)-1])
|
||||
absolutePath := activeUser.PathJoin(dstDriveId, targetFilePath)
|
||||
targetFile, err := activeUser.PanClient().FileInfoByPath(dstDriveId, absolutePath)
|
||||
targetFile, err := activeUser.PanClient().WebapiPanClient().FileInfoByPath(dstDriveId, absolutePath)
|
||||
if err != nil || !targetFile.IsFolder() {
|
||||
return nil, nil, nil, fmt.Errorf("指定目标文件夹不存在")
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func (pu *PanUser) DeleteOneCache(dirPath string) {
|
||||
func (pu *PanUser) CacheFilesDirectoriesList(pathStr string) (fdl aliyunpan.FileList, apiError *apierror.ApiError) {
|
||||
data := pu.cacheOpMap.CacheOperation(pu.ActiveDriveId, pathStr+"_OrderByName", func() expires.DataExpires {
|
||||
var fi *aliyunpan.FileEntity
|
||||
fi, apiError = pu.panClient.FileInfoByPath(pu.ActiveDriveId, pathStr)
|
||||
fi, apiError = pu.panClient.OpenapiPanClient().FileInfoByPath(pu.ActiveDriveId, pathStr)
|
||||
if apiError != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func (pu *PanUser) CacheFilesDirectoriesList(pathStr string) (fdl aliyunpan.File
|
||||
DriveId: pu.ActiveDriveId,
|
||||
ParentFileId: fi.FileId,
|
||||
}
|
||||
fdl, apiError = pu.panClient.FileListGetAll(fileListParam, 100)
|
||||
fdl, apiError = pu.panClient.OpenapiPanClient().FileListGetAll(fileListParam, 200)
|
||||
if apiError != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_open"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
)
|
||||
|
||||
type (
|
||||
// PanClient 云盘客户端
|
||||
PanClient struct {
|
||||
// 网页WEB接口客户端
|
||||
webapiPanClient *aliyunpan_web.WebPanClient
|
||||
// 阿里openapi接口客户端
|
||||
openapiPanClient *aliyunpan_open.OpenPanClient
|
||||
}
|
||||
)
|
||||
|
||||
func NewPanClient(webClient *aliyunpan_web.WebPanClient, openClient *aliyunpan_open.OpenPanClient) *PanClient {
|
||||
return &PanClient{
|
||||
webapiPanClient: webClient,
|
||||
openapiPanClient: openClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PanClient) WebapiPanClient() *aliyunpan_web.WebPanClient {
|
||||
return p.webapiPanClient
|
||||
}
|
||||
|
||||
func (p *PanClient) OpenapiPanClient() *aliyunpan_open.OpenPanClient {
|
||||
return p.openapiPanClient
|
||||
}
|
||||
@@ -61,6 +61,9 @@ const (
|
||||
|
||||
// DefaultDeviceName 默认客户端名称
|
||||
DefaultDeviceName = "Chrome浏览器"
|
||||
|
||||
// DefaultClientId 默认的clientId
|
||||
DefaultClientId = "cf9f70e8fc61430f8ec5ab5cadf31375"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -69,11 +72,6 @@ var (
|
||||
|
||||
// Config 配置信息, 由外部调用
|
||||
Config = NewConfig(configFilePath)
|
||||
|
||||
AppVersion string
|
||||
|
||||
// IsAppInCliMode 是否在交互模式
|
||||
IsAppInCliMode = false
|
||||
)
|
||||
|
||||
type UpdateCheckInfo struct {
|
||||
@@ -95,7 +93,6 @@ type PanConfig struct {
|
||||
|
||||
MaxDownloadRate int64 `json:"maxDownloadRate"` // 限制最大下载速度,单位 B/s, 即字节/每秒
|
||||
MaxUploadRate int64 `json:"maxUploadRate"` // 限制最大上传速度,单位 B/s, 即字节/每秒
|
||||
TransferUrlType int `json:"transferUrlType"` // 上传/下载URL类别,1-默认,2-阿里云ECS
|
||||
|
||||
SaveDir string `json:"saveDir"` // 下载储存路径
|
||||
|
||||
@@ -110,6 +107,10 @@ type PanConfig struct {
|
||||
DeviceId string `json:"deviceId"` // 客户端ID,用于标识登录客户端,阿里单个账号最多允许10个客户端同时登录
|
||||
DeviceName string `json:"deviceName"` // 客户端名称,默认为:Chrome浏览器
|
||||
|
||||
// Openapi客户端信息
|
||||
ClientId string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
|
||||
configFilePath string
|
||||
configFile *os.File
|
||||
fileMu sync.Mutex
|
||||
@@ -197,10 +198,6 @@ func (c *PanConfig) init() error {
|
||||
if c.Proxy != "" {
|
||||
requester.SetGlobalProxy(c.Proxy)
|
||||
}
|
||||
// 设置本地网卡地址
|
||||
if c.LocalAddrs != "" {
|
||||
requester.SetLocalTCPAddrList(strings.Split(c.LocalAddrs, ",")...)
|
||||
}
|
||||
|
||||
// 设置域名解析策略 IPv4 or IPv6
|
||||
t := requester.IPAny
|
||||
@@ -211,6 +208,15 @@ func (c *PanConfig) init() error {
|
||||
}
|
||||
requester.SetPreferIPType(t)
|
||||
|
||||
// 设置本地网卡地址
|
||||
if c.LocalAddrs != "" {
|
||||
ips := ParseLocalAddress(c.LocalAddrs, strings.ToLower(c.PreferIPType))
|
||||
if len(ips) > 0 {
|
||||
logger.Verboseln("bind local address list: ", ips)
|
||||
requester.SetLocalTCPAddrList(ips...)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -270,6 +276,9 @@ func (c *PanConfig) loadConfigFromFile() (err error) {
|
||||
if c.DeviceName == "" {
|
||||
c.DeviceName = DefaultDeviceName
|
||||
}
|
||||
if c.ClientId == "" {
|
||||
c.ClientId = DefaultClientId
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -294,7 +303,8 @@ func (c *PanConfig) initDefaultConfig() {
|
||||
c.VideoFileExtensions = DefaultVideoFileExtensions
|
||||
c.DeviceId = RandomDeviceId() // 生成默认客户端ID
|
||||
c.DeviceName = DefaultDeviceName
|
||||
c.FileRecordConfig = "1" // 默认开启
|
||||
c.ClientId = DefaultClientId
|
||||
c.FileRecordConfig = "2" // 默认关闭
|
||||
c.PreferIPType = "ipv4" // 默认优先IPv4
|
||||
}
|
||||
|
||||
@@ -390,7 +400,10 @@ func (c *PanConfig) ActiveUser() *PanUser {
|
||||
if u.UserId == c.ActiveUID {
|
||||
if u.PanClient() == nil {
|
||||
// restore client
|
||||
user, err := SetupUserByCookie(&u.WebToken, c.DeviceId, c.DeviceName)
|
||||
user, err := SetupUserByCookie(u.OpenapiToken, u.WebapiToken,
|
||||
u.TicketId, u.UserId,
|
||||
c.DeviceId, c.DeviceName,
|
||||
c.ClientId, c.ClientSecret)
|
||||
if err != nil {
|
||||
logger.Verboseln("setup user error")
|
||||
return nil
|
||||
@@ -398,34 +411,46 @@ func (c *PanConfig) ActiveUser() *PanUser {
|
||||
u.panClient = user.panClient
|
||||
u.Nickname = user.Nickname
|
||||
|
||||
if u.ActiveDriveId == "" {
|
||||
u.ActiveDriveId = user.DriveList.GetFileDriveId()
|
||||
}
|
||||
u.DriveList = user.DriveList
|
||||
// check workdir valid or not
|
||||
if user.IsFileDriveActive() {
|
||||
fe, err1 := u.PanClient().FileInfoByPath(u.ActiveDriveId, u.Workdir)
|
||||
fe, err1 := u.PanClient().OpenapiPanClient().FileInfoByPath(u.ActiveDriveId, u.Workdir)
|
||||
if err1 != nil {
|
||||
// default to root
|
||||
u.Workdir = "/"
|
||||
u.WorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
|
||||
} else {
|
||||
u.WorkdirFileEntity = *fe
|
||||
if u.Workdir == "" {
|
||||
u.Workdir = "/"
|
||||
}
|
||||
}
|
||||
} else if user.IsResourceDriveActive() {
|
||||
fe, err1 := u.PanClient().FileInfoByPath(u.ActiveDriveId, u.ResourceWorkdir)
|
||||
fe, err1 := u.PanClient().OpenapiPanClient().FileInfoByPath(u.ActiveDriveId, u.ResourceWorkdir)
|
||||
if err1 != nil {
|
||||
// default to root
|
||||
u.ResourceWorkdir = "/"
|
||||
u.ResourceWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
|
||||
} else {
|
||||
u.ResourceWorkdirFileEntity = *fe
|
||||
if u.ResourceWorkdir == "" {
|
||||
u.ResourceWorkdir = "/"
|
||||
}
|
||||
}
|
||||
} else if user.IsAlbumDriveActive() {
|
||||
fe, err1 := u.PanClient().FileInfoByPath(u.ActiveDriveId, u.AlbumWorkdir)
|
||||
fe, err1 := u.PanClient().WebapiPanClient().FileInfoByPath(u.ActiveDriveId, u.AlbumWorkdir)
|
||||
if err1 != nil {
|
||||
// default to root
|
||||
u.AlbumWorkdir = "/"
|
||||
u.AlbumWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
|
||||
} else {
|
||||
u.AlbumWorkdirFileEntity = *fe
|
||||
if u.AlbumWorkdir == "" {
|
||||
u.AlbumWorkdir = "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -433,7 +458,7 @@ func (c *PanConfig) ActiveUser() *PanUser {
|
||||
return u
|
||||
}
|
||||
}
|
||||
return &PanUser{}
|
||||
return nil
|
||||
}
|
||||
return c.activeUser
|
||||
}
|
||||
@@ -444,9 +469,12 @@ func (c *PanConfig) SetActiveUser(user *PanUser) *PanUser {
|
||||
if u.UserId == user.UserId {
|
||||
// update user info
|
||||
u.Nickname = user.Nickname
|
||||
u.WebToken = user.WebToken
|
||||
u.RefreshToken = user.RefreshToken
|
||||
u.TokenId = user.TokenId
|
||||
u.WebapiToken = user.WebapiToken
|
||||
u.OpenapiToken = user.OpenapiToken
|
||||
u.TicketId = user.TicketId
|
||||
if u.PanClient() != nil && user.PanClient() != nil {
|
||||
u.UpdateClient(user.PanClient().OpenapiPanClient(), user.PanClient().WebapiPanClient())
|
||||
}
|
||||
needToInsert = false
|
||||
break
|
||||
}
|
||||
@@ -474,9 +502,9 @@ func (c *PanConfig) NumLogins() int {
|
||||
}
|
||||
|
||||
// SwitchUser 切换登录用户
|
||||
func (c *PanConfig) SwitchUser(uid, username string) (*PanUser, error) {
|
||||
func (c *PanConfig) SwitchUser(uid string) (*PanUser, error) {
|
||||
for _, u := range c.UserList {
|
||||
if u.UserId == uid || u.AccountName == username {
|
||||
if u.UserId == uid {
|
||||
return c.SetActiveUser(u), nil
|
||||
}
|
||||
}
|
||||
@@ -492,7 +520,7 @@ func (c *PanConfig) DeleteUser(uid string) (*PanUser, error) {
|
||||
c.ActiveUID = ""
|
||||
c.activeUser = nil
|
||||
if len(c.UserList) > 0 {
|
||||
c.SwitchUser(c.UserList[0].UserId, "")
|
||||
c.SwitchUser(c.UserList[0].UserId)
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
@@ -33,7 +33,10 @@ func (c *PanConfig) SetProxy(proxy string) {
|
||||
// SetLocalAddrs 设置localAddrs
|
||||
func (c *PanConfig) SetLocalAddrs(localAddrs string) {
|
||||
c.LocalAddrs = localAddrs
|
||||
requester.SetLocalTCPAddrList(strings.Split(localAddrs, ",")...)
|
||||
ips := ParseLocalAddress(localAddrs, strings.ToLower(c.PreferIPType))
|
||||
if len(ips) > 0 {
|
||||
requester.SetLocalTCPAddrList(ips...)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PanConfig) SetPreferIPType(ipType string) {
|
||||
@@ -110,10 +113,9 @@ func (c *PanConfig) PrintTable() {
|
||||
[]string{"max_upload_parallel", strconv.Itoa(c.MaxUploadParallel), "1 ~ 20", "最大上传并发量,即同时上传文件最大数量"},
|
||||
[]string{"max_download_rate", showMaxRate(c.MaxDownloadRate), "", "限制单个文件最大下载速度, 0代表不限制"},
|
||||
[]string{"max_upload_rate", showMaxRate(c.MaxUploadRate), "", "限制单个文件最大上传速度, 0代表不限制"},
|
||||
[]string{"transfer_url_type", strconv.Itoa(c.TransferUrlType), "1-默认,2-阿里云ECS", "上传下载URL类别。除非在阿里云ECS(暂只支持经典网络)服务器中使用,不然请设置1"},
|
||||
[]string{"savedir", c.SaveDir, "", "下载文件的储存目录"},
|
||||
[]string{"proxy", c.Proxy, "", "设置代理, 支持 http/socks5 代理,例如: http://127.0.0.1:8888 或者 socks5://127.0.0.1:8889"},
|
||||
[]string{"local_addrs", c.LocalAddrs, "", "设置本地网卡地址, 多个地址用逗号隔开,例如: 127.0.0.1,192.168.100.126"},
|
||||
[]string{"local_addrs", c.LocalAddrs, "", "绑定本地网卡地址, 多个地址用逗号隔开,支持网口名称,例如: 127.0.0.1,192.168.100.126,en0,eth0"},
|
||||
[]string{"ip_type", c.PreferIPType, "ipv4-优先IPv4,ipv6-优先IPv6", "设置域名解析IP优先类型。修改后需要重启应用生效"},
|
||||
[]string{"file_record_config", fileRecorderLabel, "1-开启,2-禁用", "设置是否开启上传、下载、同步文件的结果记录,开启后会把结果记录到CSV文件方便后期查看"},
|
||||
[]string{"device_id", c.DeviceId, "", "客户端ID,用于标识登录客户端,阿里单个账号最多允许10个客户端同时在线。修改后需要重启应用生效"},
|
||||
|
||||
@@ -17,10 +17,15 @@ import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_open"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
|
||||
"github.com/tickstep/library-go/expires/cachemap"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DriveInfo struct {
|
||||
@@ -57,12 +62,28 @@ func (d DriveInfoList) GetResourceDriveId() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// PanClientToken 授权Token
|
||||
type PanClientToken struct {
|
||||
// AccessToken AccessToken
|
||||
AccessToken string `json:"accessToken"`
|
||||
// Expired 过期时间戳,单位秒
|
||||
Expired int64 `json:"expired"`
|
||||
}
|
||||
|
||||
// GetExpiredTimeCstStr 获取东八区时间字符串
|
||||
func (t *PanClientToken) GetExpiredTimeCstStr() string {
|
||||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||||
tm := time.Unix(t.Expired, 0).In(cz)
|
||||
return tm.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
type PanUser struct {
|
||||
// 用户信息
|
||||
UserId string `json:"userId"`
|
||||
Nickname string `json:"nickname"`
|
||||
AccountName string `json:"accountName"`
|
||||
|
||||
// 文件/备份盘
|
||||
// 文件(备份盘)
|
||||
Workdir string `json:"workdir"`
|
||||
WorkdirFileEntity aliyunpan.FileEntity `json:"workdirFileEntity"`
|
||||
|
||||
@@ -74,103 +95,203 @@ type PanUser struct {
|
||||
AlbumWorkdir string `json:"albumWorkdir"`
|
||||
AlbumWorkdirFileEntity aliyunpan.FileEntity `json:"albumWorkdirFileEntity"`
|
||||
|
||||
// 用户网盘信息
|
||||
ActiveDriveId string `json:"activeDriveId"`
|
||||
DriveList DriveInfoList `json:"driveList"`
|
||||
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
WebToken aliyunpan.WebLoginToken `json:"webToken"`
|
||||
TokenId string `json:"tokenId"`
|
||||
// 授权Token信息
|
||||
TicketId string `json:"ticketId"`
|
||||
WebapiToken *PanClientToken `json:"webapiToken"`
|
||||
OpenapiToken *PanClientToken `json:"openapiToken"`
|
||||
|
||||
panClient *aliyunpan.PanClient
|
||||
cacheOpMap cachemap.CacheOpMap
|
||||
// API客户端
|
||||
panClient *PanClient `json:"-"`
|
||||
cacheOpMap cachemap.CacheOpMap `json:"-"`
|
||||
}
|
||||
|
||||
type PanUserList []*PanUser
|
||||
|
||||
func SetupUserByCookie(webToken *aliyunpan.WebLoginToken, deviceId, deviceName string) (user *PanUser, err *apierror.ApiError) {
|
||||
func SetupUserByCookie(openapiToken, webapiToken *PanClientToken, ticketId, userId, deviceId, deviceName, clientId, clientSecret string) (user *PanUser, err *apierror.ApiError) {
|
||||
tryRefreshWebToken := true
|
||||
tryRefreshOpenToken := true
|
||||
loginHelper := panlogin.NewLoginHelper(DefaultTokenServiceWebHost)
|
||||
|
||||
if webToken == nil {
|
||||
return nil, apierror.NewFailedApiError("web token is empty")
|
||||
if openapiToken == nil {
|
||||
return nil, apierror.NewFailedApiError("openapi token is empty")
|
||||
}
|
||||
//if webapiToken == nil {
|
||||
// return nil, apierror.NewFailedApiError("webapi token is empty")
|
||||
//}
|
||||
|
||||
doLoginAct:
|
||||
appConfig := aliyunpan.AppConfig{
|
||||
AppId: "25dzX3vbYqktVxyX",
|
||||
DeviceId: deviceId,
|
||||
UserId: "",
|
||||
Nonce: 0,
|
||||
PublicKey: "",
|
||||
}
|
||||
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{}, appConfig, aliyunpan.SessionConfig{
|
||||
DeviceName: deviceName,
|
||||
ModelName: "Windows网页版",
|
||||
})
|
||||
u := &PanUser{
|
||||
WebToken: *webToken,
|
||||
panClient: panClient,
|
||||
Workdir: "/",
|
||||
WorkdirFileEntity: *aliyunpan.NewFileEntityForRootDir(),
|
||||
}
|
||||
doOpenLoginAct:
|
||||
// setup openapi client
|
||||
openPanClient := aliyunpan_open.NewOpenPanClient(openapi.ApiConfig{
|
||||
TicketId: ticketId,
|
||||
UserId: userId,
|
||||
ClientId: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
}, openapi.ApiToken{
|
||||
AccessToken: openapiToken.AccessToken,
|
||||
ExpiredAt: openapiToken.Expired,
|
||||
}, nil)
|
||||
|
||||
// web api token maybe expired
|
||||
userInfo, err := panClient.GetUserInfo()
|
||||
// open api token maybe expired
|
||||
// check & refresh new one
|
||||
openUserInfo, err := openPanClient.GetUserInfo()
|
||||
if err != nil {
|
||||
if err.Code == apierror.ApiCodeTokenExpiredCode && tryRefreshWebToken {
|
||||
tryRefreshWebToken = false
|
||||
webCookie, _ := aliyunpan.GetAccessTokenFromRefreshToken(webToken.RefreshToken)
|
||||
if webCookie != nil {
|
||||
webToken = webCookie
|
||||
goto doLoginAct
|
||||
if err.Code == apierror.ApiCodeTokenExpiredCode && tryRefreshOpenToken {
|
||||
tryRefreshOpenToken = false
|
||||
wt, e := loginHelper.GetOpenapiNewToken(ticketId, userId, openapiToken.AccessToken)
|
||||
if e != nil {
|
||||
logger.Verboseln("get openapi token from server error: ", e)
|
||||
return nil, apierror.NewFailedApiError("get new openapi token error, try login again")
|
||||
}
|
||||
if wt != nil {
|
||||
openapiToken = &PanClientToken{
|
||||
AccessToken: wt.AccessToken,
|
||||
Expired: wt.Expired,
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Duration(1) * time.Second)
|
||||
goto doOpenLoginAct
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
doWebLoginAct:
|
||||
// setup webapi client
|
||||
var webUserInfo *aliyunpan.UserInfo
|
||||
var err2 *apierror.ApiError
|
||||
var webPanClient *aliyunpan_web.WebPanClient
|
||||
if webapiToken != nil && webapiToken.AccessToken != "" {
|
||||
appConfig := aliyunpan_web.AppConfig{
|
||||
AppId: "25dzX3vbYqktVxyX",
|
||||
DeviceId: deviceId,
|
||||
UserId: userId,
|
||||
Nonce: 0,
|
||||
PublicKey: "",
|
||||
}
|
||||
webPanClient = aliyunpan_web.NewWebPanClient(aliyunpan_web.WebLoginToken{
|
||||
AccessTokenType: "Bearer",
|
||||
AccessToken: webapiToken.AccessToken,
|
||||
RefreshToken: "",
|
||||
ExpiresIn: 7200,
|
||||
ExpireTime: webapiToken.GetExpiredTimeCstStr(),
|
||||
}, aliyunpan_web.AppLoginToken{}, appConfig, aliyunpan_web.SessionConfig{
|
||||
DeviceName: deviceName,
|
||||
ModelName: "Windows网页版",
|
||||
})
|
||||
// web api token maybe expired
|
||||
// check & refresh new one
|
||||
webUserInfo, err2 = webPanClient.GetUserInfo()
|
||||
if err2 != nil {
|
||||
if err2.Code == apierror.ApiCodeTokenExpiredCode && tryRefreshWebToken {
|
||||
tryRefreshWebToken = false
|
||||
wt, e := loginHelper.GetWebapiNewToken(ticketId, userId, webapiToken.AccessToken)
|
||||
if e != nil {
|
||||
logger.Verboseln("get web token from server error: ", e)
|
||||
}
|
||||
if wt != nil {
|
||||
webapiToken = &PanClientToken{
|
||||
AccessToken: wt.AccessToken,
|
||||
Expired: wt.Expired,
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Duration(1) * time.Second)
|
||||
goto doWebLoginAct
|
||||
}
|
||||
webPanClient = nil
|
||||
//return nil, err2
|
||||
}
|
||||
// web create session
|
||||
if webUserInfo != nil {
|
||||
appConfig.UserId = webUserInfo.UserId
|
||||
webPanClient.UpdateAppConfig(appConfig)
|
||||
r, e := webPanClient.CreateSession(nil)
|
||||
if e != nil {
|
||||
logger.Verboseln("call CreateSession error in SetupUserByCookie: " + e.Error())
|
||||
}
|
||||
if r != nil && !r.Result {
|
||||
logger.Verboseln("上传签名秘钥失败,可能是你账号登录的设备已超最大数量")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// setup PanUser
|
||||
//
|
||||
u := &PanUser{
|
||||
WebapiToken: webapiToken,
|
||||
OpenapiToken: openapiToken,
|
||||
panClient: NewPanClient(webPanClient, openPanClient),
|
||||
Workdir: "/",
|
||||
WorkdirFileEntity: *aliyunpan.NewFileEntityForRootDir(),
|
||||
}
|
||||
u.PanClient().OpenapiPanClient().SetAccessTokenRefreshCallback(func(userId string, newToken openapi.ApiToken) error {
|
||||
logger.Verboseln("openapi token refresh, update for user")
|
||||
u.OpenapiToken = &PanClientToken{
|
||||
AccessToken: newToken.AccessToken,
|
||||
Expired: newToken.ExpiredAt,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// setup user info
|
||||
name := "Unknown"
|
||||
if userInfo != nil {
|
||||
if userInfo.Nickname != "" {
|
||||
name = userInfo.Nickname
|
||||
if openUserInfo != nil {
|
||||
// fill userId for client
|
||||
u.PanClient().OpenapiPanClient().UpdateUserId(openUserInfo.UserId)
|
||||
if u.PanClient().WebapiPanClient() != nil {
|
||||
u.PanClient().WebapiPanClient().UpdateUserId(openUserInfo.UserId)
|
||||
}
|
||||
|
||||
// update user
|
||||
u.UserId = userInfo.UserId
|
||||
if openUserInfo.Nickname != "" {
|
||||
name = openUserInfo.Nickname
|
||||
}
|
||||
u.UserId = openUserInfo.UserId
|
||||
u.Nickname = name
|
||||
u.AccountName = userInfo.UserName
|
||||
u.AccountName = openUserInfo.UserName
|
||||
|
||||
// default file drive
|
||||
if u.ActiveDriveId == "" {
|
||||
u.ActiveDriveId = userInfo.FileDriveId
|
||||
u.ActiveDriveId = openUserInfo.FileDriveId
|
||||
}
|
||||
|
||||
// drive list
|
||||
u.DriveList = DriveInfoList{
|
||||
{DriveId: userInfo.FileDriveId, DriveTag: "File", DriveName: "备份盘"},
|
||||
{DriveId: userInfo.ResourceDriveId, DriveTag: "Resource", DriveName: "资源库"},
|
||||
{DriveId: userInfo.AlbumDriveId, DriveTag: "Album", DriveName: "相册"},
|
||||
{DriveId: openUserInfo.FileDriveId, DriveTag: "File", DriveName: "备份盘"},
|
||||
{DriveId: openUserInfo.ResourceDriveId, DriveTag: "Resource", DriveName: "资源库"},
|
||||
}
|
||||
if webUserInfo != nil {
|
||||
u.AccountName = webUserInfo.UserName
|
||||
u.DriveList = append(u.DriveList, &DriveInfo{DriveId: webUserInfo.AlbumDriveId, DriveTag: "Album", DriveName: "相册"})
|
||||
}
|
||||
} else {
|
||||
// error, maybe the token has expired
|
||||
return nil, apierror.NewFailedApiError("cannot get user info, the token has expired")
|
||||
}
|
||||
|
||||
// create session
|
||||
appConfig.UserId = u.UserId
|
||||
panClient.UpdateAppConfig(appConfig)
|
||||
r, e := panClient.CreateSession(nil)
|
||||
if e != nil {
|
||||
logger.Verboseln("call CreateSession error in SetupUserByCookie: " + e.Error())
|
||||
}
|
||||
if r != nil && !r.Result {
|
||||
logger.Verboseln("上传签名秘钥失败,可能是你账号登录的设备已超最大数量")
|
||||
return nil, apierror.NewFailedApiError("cannot get user info, the token has expired. please login again")
|
||||
}
|
||||
|
||||
// return user
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (pu *PanUser) PanClient() *aliyunpan.PanClient {
|
||||
func (pu *PanUser) PanClient() *PanClient {
|
||||
return pu.panClient
|
||||
}
|
||||
|
||||
func (pu *PanUser) UpdateClient(openClient *aliyunpan_open.OpenPanClient, webClient *aliyunpan_web.WebPanClient) {
|
||||
if pu.panClient == nil {
|
||||
pu.panClient = &PanClient{
|
||||
webapiPanClient: webClient,
|
||||
openapiPanClient: openClient,
|
||||
}
|
||||
} else {
|
||||
pu.panClient.webapiPanClient = webClient
|
||||
pu.panClient.openapiPanClient = openClient
|
||||
}
|
||||
}
|
||||
|
||||
// PathJoin 合并工作目录和相对路径p, 若p为绝对路径则忽略
|
||||
func (pu *PanUser) PathJoin(driveId, p string) string {
|
||||
if path.IsAbs(p) {
|
||||
@@ -192,21 +313,20 @@ func (pu *PanUser) PathJoin(driveId, p string) string {
|
||||
|
||||
func (pu *PanUser) FreshWorkdirInfo() {
|
||||
if pu.IsFileDriveActive() {
|
||||
fe, err := pu.PanClient().FileInfoById(pu.ActiveDriveId, pu.WorkdirFileEntity.FileId)
|
||||
fe, err := pu.PanClient().OpenapiPanClient().FileInfoById(pu.ActiveDriveId, pu.WorkdirFileEntity.FileId)
|
||||
if err != nil {
|
||||
logger.Verboseln("刷新工作目录信息失败")
|
||||
return
|
||||
}
|
||||
pu.WorkdirFileEntity = *fe
|
||||
} else if pu.IsAlbumDriveActive() {
|
||||
fe, err := pu.PanClient().FileInfoById(pu.ActiveDriveId, pu.AlbumWorkdirFileEntity.FileId)
|
||||
fe, err := pu.PanClient().OpenapiPanClient().FileInfoById(pu.ActiveDriveId, pu.AlbumWorkdirFileEntity.FileId)
|
||||
if err != nil {
|
||||
logger.Verboseln("刷新工作目录信息失败")
|
||||
return
|
||||
}
|
||||
pu.AlbumWorkdirFileEntity = *fe
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// GetSavePath 根据提供的网盘文件路径 panpath, 返回本地储存路径,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -17,11 +17,13 @@ import (
|
||||
"encoding/hex"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/library/nets"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/crypto"
|
||||
"github.com/tickstep/library-go/ids"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -109,7 +111,7 @@ func DecryptString(text string) string {
|
||||
return string(r)
|
||||
}
|
||||
|
||||
// isFolderExist 判断文件夹是否存在
|
||||
// IsFolderExist 判断文件夹是否存在
|
||||
func IsFolderExist(pathStr string) bool {
|
||||
fi, err := os.Stat(pathStr)
|
||||
if err != nil {
|
||||
@@ -135,3 +137,42 @@ func RandomDeviceId() string {
|
||||
}
|
||||
return str.String()
|
||||
}
|
||||
|
||||
// ParseLocalAddress 解析网络接口配置为对应的本地IP地址
|
||||
func ParseLocalAddress(localAddrs string, filterIpType string) []string {
|
||||
allLocalAddress, _ := nets.GetLocalNetInterfaceAddress()
|
||||
localAddrNames := strings.Split(localAddrs, ",")
|
||||
ips := []string{}
|
||||
for _, addr := range localAddrNames {
|
||||
if addr == "" {
|
||||
continue
|
||||
}
|
||||
if net.ParseIP(addr) == nil {
|
||||
// maybe local interface name
|
||||
if localAddr := allLocalAddress.GetByName(addr); localAddr != nil {
|
||||
if localAddr.IPv4 != "" {
|
||||
if filterIpType == "any" || filterIpType == "" { // 不限制
|
||||
ips = append(ips, localAddr.IPv4)
|
||||
} else {
|
||||
if filterIpType == "ipv4" {
|
||||
ips = append(ips, localAddr.IPv4)
|
||||
}
|
||||
}
|
||||
}
|
||||
if localAddr.IPv6 != "" {
|
||||
if filterIpType == "any" || filterIpType == "" { // 不限制
|
||||
ips = append(ips, localAddr.IPv6)
|
||||
} else {
|
||||
if filterIpType == "ipv6" {
|
||||
ips = append(ips, localAddr.IPv6)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ip
|
||||
ips = append(ips, addr)
|
||||
}
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -27,10 +27,11 @@ var (
|
||||
MinParallelSize int64 = 10 * 1024 * 1024 // 10MB
|
||||
|
||||
// MaxParallelWorkerCount 单个文件下载最大并发线程数量
|
||||
// 阿里云盘规定:并发下载线程数不要超过10,否则会有风控检测处罚的风险
|
||||
MaxParallelWorkerCount int = 3
|
||||
)
|
||||
|
||||
//Config 下载配置
|
||||
// Config 下载配置
|
||||
type Config struct {
|
||||
Mode transfer.RangeGenMode // 下载Range分配模式
|
||||
MaxParallel int // 最大下载并发量
|
||||
@@ -41,11 +42,10 @@ type Config struct {
|
||||
InstanceStatePath string // 断点续传信息路径
|
||||
TryHTTP bool // 是否尝试使用 http 连接
|
||||
ShowProgress bool // 是否展示下载进度条
|
||||
UseInternalUrl bool // 是否使用内置链接
|
||||
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
|
||||
}
|
||||
|
||||
//NewConfig 返回默认配置
|
||||
// NewConfig 返回默认配置
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
MaxParallel: 5,
|
||||
@@ -53,7 +53,7 @@ func NewConfig() *Config {
|
||||
}
|
||||
}
|
||||
|
||||
//Fix 修复配置信息, 使其合法
|
||||
// Fix 修复配置信息, 使其合法
|
||||
func (cfg *Config) Fix() {
|
||||
fixCacheSize(&cfg.CacheSize)
|
||||
if cfg.MaxParallel < 1 {
|
||||
@@ -61,7 +61,7 @@ func (cfg *Config) Fix() {
|
||||
}
|
||||
}
|
||||
|
||||
//Copy 拷贝新的配置
|
||||
// Copy 拷贝新的配置
|
||||
func (cfg *Config) Copy() *Config {
|
||||
newCfg := *cfg
|
||||
return &newCfg
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdutil"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/waitgroup"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/cachepool"
|
||||
@@ -62,7 +63,7 @@ type (
|
||||
loadBalansers []string
|
||||
writer io.WriterAt
|
||||
client *requester.HTTPClient
|
||||
panClient *aliyunpan.PanClient
|
||||
panClient *config.PanClient
|
||||
config *Config
|
||||
monitor *Monitor
|
||||
instanceState *InstanceState
|
||||
@@ -74,8 +75,8 @@ type (
|
||||
StatusCodeBodyCheckFunc func(respBody io.Reader) error
|
||||
)
|
||||
|
||||
//NewDownloader 初始化Downloader
|
||||
func NewDownloader(writer io.WriterAt, config *Config, p *aliyunpan.PanClient, globalSpeedsStat *speeds.Speeds) (der *Downloader) {
|
||||
// NewDownloader 初始化Downloader
|
||||
func NewDownloader(writer io.WriterAt, config *Config, p *config.PanClient, globalSpeedsStat *speeds.Speeds) (der *Downloader) {
|
||||
der = &Downloader{
|
||||
config: config,
|
||||
writer: writer,
|
||||
@@ -85,7 +86,7 @@ func NewDownloader(writer io.WriterAt, config *Config, p *aliyunpan.PanClient, g
|
||||
return
|
||||
}
|
||||
|
||||
//SetClient 设置http客户端
|
||||
// SetClient 设置http客户端
|
||||
func (der *Downloader) SetFileInfo(f *aliyunpan.FileEntity) {
|
||||
der.fileInfo = f
|
||||
}
|
||||
@@ -94,7 +95,7 @@ func (der *Downloader) SetDriveId(driveId string) {
|
||||
der.driveId = driveId
|
||||
}
|
||||
|
||||
//SetClient 设置http客户端
|
||||
// SetClient 设置http客户端
|
||||
func (der *Downloader) SetClient(client *requester.HTTPClient) {
|
||||
der.client = client
|
||||
}
|
||||
@@ -104,7 +105,7 @@ func (der *Downloader) SetLoadBalancerCompareFunc(f LoadBalancerCompareFunc) {
|
||||
der.loadBalancerCompareFunc = f
|
||||
}
|
||||
|
||||
//SetStatusCodeBodyCheckFunc 设置响应状态码出错的检查函数, 当FirstCheckMethod不为HEAD时才有效
|
||||
// SetStatusCodeBodyCheckFunc 设置响应状态码出错的检查函数, 当FirstCheckMethod不为HEAD时才有效
|
||||
func (der *Downloader) SetStatusCodeBodyCheckFunc(f StatusCodeBodyCheckFunc) {
|
||||
der.statusCodeBodyCheckFunc = f
|
||||
}
|
||||
@@ -136,12 +137,13 @@ func (der *Downloader) SelectParallel(single bool, maxParallel int, totalSize in
|
||||
} else if isRange {
|
||||
parallel = len(instanceRangeList)
|
||||
} else {
|
||||
parallel = maxParallel
|
||||
if int64(parallel) > totalSize/int64(MinParallelSize) {
|
||||
parallel = maxParallel // 默认为设置为maxParallel个并发线程数
|
||||
if int64(parallel) > totalSize/int64(MinParallelSize) { // 如果文件太小不足切片成maxParallel数量的分片,则计算最接近的分片数量
|
||||
parallel = int(totalSize/int64(MinParallelSize)) + 1
|
||||
}
|
||||
}
|
||||
|
||||
// 其他情况默认使用单线程下载
|
||||
if parallel < 1 {
|
||||
parallel = 1
|
||||
}
|
||||
@@ -283,7 +285,7 @@ func (der *Downloader) checkLoadBalancers() *LoadBalancerResponseList {
|
||||
return loadBalancerResponseList
|
||||
}
|
||||
|
||||
//Execute 开始任务
|
||||
// Execute 开始任务
|
||||
func (der *Downloader) Execute() error {
|
||||
der.lazyInit()
|
||||
|
||||
@@ -307,7 +309,7 @@ func (der *Downloader) Execute() error {
|
||||
var (
|
||||
isInstance = bii != nil // 是否存在断点信息
|
||||
status *transfer.DownloadStatus
|
||||
single = false // 默认开启多线程下载
|
||||
single = false // 默认开启多线程下载,所以当前single值都为false代表不是单线程下载
|
||||
)
|
||||
if !isInstance {
|
||||
bii = &transfer.DownloadInstanceInfo{}
|
||||
@@ -329,7 +331,7 @@ func (der *Downloader) Execute() error {
|
||||
defer rl.Stop()
|
||||
}
|
||||
|
||||
// 计算文件下载的线程数
|
||||
// 计算文件下载的并发线程数,计单个文件下载的并发数
|
||||
parallel := der.SelectParallel(single, MaxParallelWorkerCount, status.TotalSize(), bii.Ranges) // 实际的下载并行量
|
||||
blockSize, err := der.SelectBlockSizeAndInitRangeGen(single, status, parallel) // 实际的BlockSize
|
||||
if err != nil {
|
||||
@@ -379,22 +381,10 @@ func (der *Downloader) Execute() error {
|
||||
|
||||
// 获取下载链接
|
||||
var apierr *apierror.ApiError
|
||||
durl, apierr := der.panClient.GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
|
||||
durl, apierr := der.panClient.OpenapiPanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
|
||||
DriveId: der.driveId,
|
||||
FileId: der.fileInfo.FileId,
|
||||
})
|
||||
if apierr != nil && apierr.Code == apierror.ApiCodeDeviceSessionSignatureInvalid {
|
||||
_, e := der.panClient.CreateSession(nil)
|
||||
if e == nil {
|
||||
// retry
|
||||
durl, apierr = der.panClient.GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
|
||||
DriveId: der.driveId,
|
||||
FileId: der.fileInfo.FileId,
|
||||
})
|
||||
} else {
|
||||
logger.Verboseln("CreateSession failed")
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Duration(200) * time.Millisecond)
|
||||
if apierr != nil {
|
||||
logger.Verbosef("ERROR: get download url error: %s\n", der.fileInfo.FileId)
|
||||
@@ -422,9 +412,6 @@ func (der *Downloader) Execute() error {
|
||||
client.SetTimeout(10 * time.Minute)
|
||||
|
||||
realUrl := durl.Url
|
||||
if der.config.UseInternalUrl {
|
||||
realUrl = durl.InternalUrl
|
||||
}
|
||||
worker := NewWorker(k, der.driveId, der.fileInfo.FileId, realUrl, writer, der.globalSpeedsStat)
|
||||
worker.SetClient(client)
|
||||
worker.SetPanClient(der.panClient)
|
||||
@@ -438,8 +425,8 @@ func (der *Downloader) Execute() error {
|
||||
|
||||
der.monitor.SetStatus(status)
|
||||
|
||||
// 服务器不支持断点续传, 或者单线程下载, 都不重载worker
|
||||
der.monitor.SetReloadWorker(parallel > 1)
|
||||
// 阿里云盘支持断点续传,开启重载worker
|
||||
der.monitor.SetReloadWorker(true)
|
||||
|
||||
moniterCtx, moniterCancelFunc := context.WithCancel(context.Background())
|
||||
der.monitorCancelFunc = moniterCancelFunc
|
||||
@@ -469,7 +456,7 @@ func (der *Downloader) Execute() error {
|
||||
return err
|
||||
}
|
||||
|
||||
//downloadStatusEvent 执行状态处理事件
|
||||
// downloadStatusEvent 执行状态处理事件
|
||||
func (der *Downloader) downloadStatusEvent() {
|
||||
if der.onDownloadStatusEvent == nil {
|
||||
return
|
||||
@@ -491,7 +478,7 @@ func (der *Downloader) downloadStatusEvent() {
|
||||
}()
|
||||
}
|
||||
|
||||
//Pause 暂停
|
||||
// Pause 暂停
|
||||
func (der *Downloader) Pause() {
|
||||
if der.monitor == nil {
|
||||
return
|
||||
@@ -500,7 +487,7 @@ func (der *Downloader) Pause() {
|
||||
der.monitor.Pause()
|
||||
}
|
||||
|
||||
//Resume 恢复
|
||||
// Resume 恢复
|
||||
func (der *Downloader) Resume() {
|
||||
if der.monitor == nil {
|
||||
return
|
||||
@@ -509,7 +496,7 @@ func (der *Downloader) Resume() {
|
||||
der.monitor.Resume()
|
||||
}
|
||||
|
||||
//Cancel 取消
|
||||
// Cancel 取消
|
||||
func (der *Downloader) Cancel() {
|
||||
if der.monitor == nil {
|
||||
return
|
||||
@@ -518,7 +505,7 @@ func (der *Downloader) Cancel() {
|
||||
cmdutil.Trigger(der.monitorCancelFunc)
|
||||
}
|
||||
|
||||
//Failed 失败
|
||||
// Failed 失败
|
||||
func (der *Downloader) Failed() {
|
||||
if der.monitor == nil {
|
||||
return
|
||||
@@ -527,42 +514,42 @@ func (der *Downloader) Failed() {
|
||||
cmdutil.Trigger(der.monitorCancelFunc)
|
||||
}
|
||||
|
||||
//OnExecute 设置开始下载事件
|
||||
// OnExecute 设置开始下载事件
|
||||
func (der *Downloader) OnExecute(onExecuteEvent requester.Event) {
|
||||
der.onExecuteEvent = onExecuteEvent
|
||||
}
|
||||
|
||||
//OnSuccess 设置成功下载事件
|
||||
// OnSuccess 设置成功下载事件
|
||||
func (der *Downloader) OnSuccess(onSuccessEvent requester.Event) {
|
||||
der.onSuccessEvent = onSuccessEvent
|
||||
}
|
||||
|
||||
//OnFailed 设置失败事件
|
||||
// OnFailed 设置失败事件
|
||||
func (der *Downloader) OnFailed(onFailedEvent requester.Event) {
|
||||
der.onFailedEvent = onFailedEvent
|
||||
}
|
||||
|
||||
//OnFinish 设置结束下载事件
|
||||
// OnFinish 设置结束下载事件
|
||||
func (der *Downloader) OnFinish(onFinishEvent requester.Event) {
|
||||
der.onFinishEvent = onFinishEvent
|
||||
}
|
||||
|
||||
//OnPause 设置暂停下载事件
|
||||
// OnPause 设置暂停下载事件
|
||||
func (der *Downloader) OnPause(onPauseEvent requester.Event) {
|
||||
der.onPauseEvent = onPauseEvent
|
||||
}
|
||||
|
||||
//OnResume 设置恢复下载事件
|
||||
// OnResume 设置恢复下载事件
|
||||
func (der *Downloader) OnResume(onResumeEvent requester.Event) {
|
||||
der.onResumeEvent = onResumeEvent
|
||||
}
|
||||
|
||||
//OnCancel 设置取消下载事件
|
||||
// OnCancel 设置取消下载事件
|
||||
func (der *Downloader) OnCancel(onCancelEvent requester.Event) {
|
||||
der.onCancelEvent = onCancelEvent
|
||||
}
|
||||
|
||||
//OnDownloadStatusEvent 设置状态处理函数
|
||||
// OnDownloadStatusEvent 设置状态处理函数
|
||||
func (der *Downloader) OnDownloadStatusEvent(f DownloadStatusFunc) {
|
||||
der.onDownloadStatusEvent = f
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -16,8 +16,8 @@ package downloader
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
@@ -36,7 +36,7 @@ type (
|
||||
completed chan struct{}
|
||||
err error
|
||||
resetController *ResetController
|
||||
isReloadWorker bool //是否重载worker, 单线程模式不重载
|
||||
isReloadWorker bool //是否重载worker
|
||||
|
||||
// 临时变量
|
||||
lastAvaliableIndex int
|
||||
@@ -46,7 +46,7 @@ type (
|
||||
RangeWorkerFunc func(key int, worker *Worker) bool
|
||||
)
|
||||
|
||||
//NewMonitor 初始化Monitor
|
||||
// NewMonitor 初始化Monitor
|
||||
func NewMonitor() *Monitor {
|
||||
monitor := &Monitor{}
|
||||
return monitor
|
||||
@@ -60,16 +60,16 @@ func (mt *Monitor) lazyInit() {
|
||||
mt.status = transfer.NewDownloadStatus()
|
||||
}
|
||||
if mt.resetController == nil {
|
||||
mt.resetController = NewResetController(80)
|
||||
mt.resetController = NewResetController(1000)
|
||||
}
|
||||
}
|
||||
|
||||
//InitMonitorCapacity 初始化workers, 用于Append
|
||||
// InitMonitorCapacity 初始化workers, 用于Append
|
||||
func (mt *Monitor) InitMonitorCapacity(capacity int) {
|
||||
mt.workers = make(WorkerList, 0, capacity)
|
||||
}
|
||||
|
||||
//Append 增加Worker
|
||||
// Append 增加Worker
|
||||
func (mt *Monitor) Append(worker *Worker) {
|
||||
if worker == nil {
|
||||
return
|
||||
@@ -77,37 +77,37 @@ func (mt *Monitor) Append(worker *Worker) {
|
||||
mt.workers = append(mt.workers, worker)
|
||||
}
|
||||
|
||||
//SetWorkers 设置workers, 此操作会覆盖原有的workers
|
||||
// SetWorkers 设置workers, 此操作会覆盖原有的workers
|
||||
func (mt *Monitor) SetWorkers(workers WorkerList) {
|
||||
mt.workers = workers
|
||||
}
|
||||
|
||||
//SetStatus 设置DownloadStatus
|
||||
// SetStatus 设置DownloadStatus
|
||||
func (mt *Monitor) SetStatus(status *transfer.DownloadStatus) {
|
||||
mt.status = status
|
||||
}
|
||||
|
||||
//SetInstanceState 设置状态
|
||||
// SetInstanceState 设置状态
|
||||
func (mt *Monitor) SetInstanceState(instanceState *InstanceState) {
|
||||
mt.instanceState = instanceState
|
||||
}
|
||||
|
||||
//Status 返回DownloadStatus
|
||||
// Status 返回DownloadStatus
|
||||
func (mt *Monitor) Status() *transfer.DownloadStatus {
|
||||
return mt.status
|
||||
}
|
||||
|
||||
//Err 返回遇到的错误
|
||||
// Err 返回遇到的错误
|
||||
func (mt *Monitor) Err() error {
|
||||
return mt.err
|
||||
}
|
||||
|
||||
//CompletedChan 获取completed chan
|
||||
// CompletedChan 获取completed chan
|
||||
func (mt *Monitor) CompletedChan() <-chan struct{} {
|
||||
return mt.completed
|
||||
}
|
||||
|
||||
//GetAvailableWorker 获取空闲的worker
|
||||
// GetAvailableWorker 获取空闲的worker
|
||||
func (mt *Monitor) GetAvailableWorker() *Worker {
|
||||
workerCount := len(mt.workers)
|
||||
for i := mt.lastAvaliableIndex; i < mt.lastAvaliableIndex+workerCount; i++ {
|
||||
@@ -121,7 +121,7 @@ func (mt *Monitor) GetAvailableWorker() *Worker {
|
||||
return nil
|
||||
}
|
||||
|
||||
//GetAllWorkersRange 获取所有worker的范围
|
||||
// GetAllWorkersRange 获取所有worker的范围
|
||||
func (mt *Monitor) GetAllWorkersRange() transfer.RangeList {
|
||||
allWorkerRanges := make(transfer.RangeList, 0, len(mt.workers))
|
||||
for _, worker := range mt.workers {
|
||||
@@ -130,7 +130,7 @@ func (mt *Monitor) GetAllWorkersRange() transfer.RangeList {
|
||||
return allWorkerRanges
|
||||
}
|
||||
|
||||
//NumLeftWorkers 剩余的worker数量
|
||||
// NumLeftWorkers 剩余的worker数量
|
||||
func (mt *Monitor) NumLeftWorkers() (num int) {
|
||||
for _, worker := range mt.workers {
|
||||
if !worker.Completed() {
|
||||
@@ -140,12 +140,12 @@ func (mt *Monitor) NumLeftWorkers() (num int) {
|
||||
return
|
||||
}
|
||||
|
||||
//SetReloadWorker 是否重载worker
|
||||
// SetReloadWorker 是否重载worker
|
||||
func (mt *Monitor) SetReloadWorker(b bool) {
|
||||
mt.isReloadWorker = b
|
||||
}
|
||||
|
||||
//IsLeftWorkersAllFailed 剩下的线程是否全部失败
|
||||
// IsLeftWorkersAllFailed 剩下的线程是否全部失败
|
||||
func (mt *Monitor) IsLeftWorkersAllFailed() bool {
|
||||
failedNum := 0
|
||||
for _, worker := range mt.workers {
|
||||
@@ -161,7 +161,7 @@ func (mt *Monitor) IsLeftWorkersAllFailed() bool {
|
||||
return failedNum != 0
|
||||
}
|
||||
|
||||
//registerAllCompleted 全部完成则发送消息
|
||||
// registerAllCompleted 全部完成则发送消息
|
||||
func (mt *Monitor) registerAllCompleted() {
|
||||
mt.completed = make(chan struct{}, 0)
|
||||
var (
|
||||
@@ -197,7 +197,7 @@ func (mt *Monitor) registerAllCompleted() {
|
||||
}()
|
||||
}
|
||||
|
||||
//ResetFailedAndNetErrorWorkers 重设部分网络错误的worker
|
||||
// ResetFailedAndNetErrorWorkers 重设部分网络错误的worker
|
||||
func (mt *Monitor) ResetFailedAndNetErrorWorkers() {
|
||||
for k := range mt.workers {
|
||||
if !mt.resetController.CanReset() {
|
||||
@@ -221,7 +221,7 @@ func (mt *Monitor) ResetFailedAndNetErrorWorkers() {
|
||||
}
|
||||
}
|
||||
|
||||
//RangeWorker 遍历worker
|
||||
// RangeWorker 遍历worker
|
||||
func (mt *Monitor) RangeWorker(f RangeWorkerFunc) {
|
||||
for k := range mt.workers {
|
||||
if !f(k, mt.workers[k]) {
|
||||
@@ -230,14 +230,14 @@ func (mt *Monitor) RangeWorker(f RangeWorkerFunc) {
|
||||
}
|
||||
}
|
||||
|
||||
//Pause 暂停所有的下载
|
||||
// Pause 暂停所有的下载
|
||||
func (mt *Monitor) Pause() {
|
||||
for k := range mt.workers {
|
||||
mt.workers[k].Pause()
|
||||
}
|
||||
}
|
||||
|
||||
//Resume 恢复所有的下载
|
||||
// Resume 恢复所有的下载
|
||||
func (mt *Monitor) Resume() {
|
||||
for k := range mt.workers {
|
||||
mt.workers[k].Resume()
|
||||
@@ -346,7 +346,7 @@ func (mt *Monitor) ResetWorker(worker *Worker) {
|
||||
// 忽略, 返回
|
||||
return
|
||||
case StatusCodeDownloadUrlExpired: // 下载链接已经过期
|
||||
worker.RefreshDownloadUrl()
|
||||
worker.RefreshDownloadUrl()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -357,7 +357,7 @@ func (mt *Monitor) ResetWorker(worker *Worker) {
|
||||
worker.Reset()
|
||||
}
|
||||
|
||||
//Execute 执行任务
|
||||
// Execute 执行任务
|
||||
func (mt *Monitor) Execute(cancelCtx context.Context) {
|
||||
if len(mt.workers) == 0 {
|
||||
mt.err = ErrNoWokers
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -99,3 +99,18 @@ func fixCacheSize(size *int) {
|
||||
*size = 1024
|
||||
}
|
||||
}
|
||||
|
||||
// IsUrlExpired 下载链接是否已过期。过期返回True
|
||||
func IsUrlExpired(urlStr string) bool {
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
expiredTimeSecStr := u.Query().Get("x-oss-expires")
|
||||
expiredTimeSec, _ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
|
||||
if (expiredTimeSec - time.Now().Unix()) <= 5 { // 小于5秒钟
|
||||
// expired
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -19,33 +19,36 @@ import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/cachepool"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type (
|
||||
//Worker 工作单元
|
||||
Worker struct {
|
||||
totalSize int64 // 整个文件的大小, worker请求range时会获取尝试获取该值, 如果不匹配, 则返回错误
|
||||
wrange *transfer.Range
|
||||
speedsStat *speeds.Speeds
|
||||
globalSpeedsStat *speeds.Speeds // 全局速度统计
|
||||
id int // work id
|
||||
fileId string // 文件ID
|
||||
driveId string
|
||||
url string // 下载地址
|
||||
acceptRanges string
|
||||
panClient *aliyunpan.PanClient
|
||||
client *requester.HTTPClient
|
||||
writerAt io.WriterAt
|
||||
writeMu *sync.Mutex
|
||||
execMu sync.Mutex
|
||||
totalSize int64 // 整个文件的大小, worker请求range时会获取尝试获取该值, 如果不匹配, 则返回错误
|
||||
wrange *transfer.Range
|
||||
speedsStat *speeds.Speeds
|
||||
globalSpeedsStat *speeds.Speeds // 全局速度统计
|
||||
id int // work id
|
||||
fileId string // 文件ID
|
||||
driveId string
|
||||
url string // 下载地址
|
||||
acceptRanges string
|
||||
panClient *config.PanClient
|
||||
client *requester.HTTPClient
|
||||
writerAt io.WriterAt
|
||||
writeMu *sync.Mutex
|
||||
execMu sync.Mutex
|
||||
|
||||
pauseChan chan struct{}
|
||||
workerCancelFunc context.CancelFunc
|
||||
@@ -67,19 +70,19 @@ func (wl WorkerList) Duplicate() WorkerList {
|
||||
return n
|
||||
}
|
||||
|
||||
//NewWorker 初始化Worker
|
||||
// NewWorker 初始化Worker
|
||||
func NewWorker(id int, driveId string, fileId, durl string, writerAt io.WriterAt, globalSpeedsStat *speeds.Speeds) *Worker {
|
||||
return &Worker{
|
||||
id: id,
|
||||
url: durl,
|
||||
writerAt: writerAt,
|
||||
fileId: fileId,
|
||||
driveId: driveId,
|
||||
id: id,
|
||||
url: durl,
|
||||
writerAt: writerAt,
|
||||
fileId: fileId,
|
||||
driveId: driveId,
|
||||
globalSpeedsStat: globalSpeedsStat,
|
||||
}
|
||||
}
|
||||
|
||||
//ID 返回worker ID
|
||||
// ID 返回worker ID
|
||||
func (wer *Worker) ID() int {
|
||||
return wer.id
|
||||
}
|
||||
@@ -109,21 +112,21 @@ func (wer *Worker) SetTotalSize(size int64) {
|
||||
wer.totalSize = size
|
||||
}
|
||||
|
||||
//SetClient 设置http客户端
|
||||
// SetClient 设置http客户端
|
||||
func (wer *Worker) SetClient(c *requester.HTTPClient) {
|
||||
wer.client = c
|
||||
}
|
||||
|
||||
func (wer *Worker) SetPanClient(p *aliyunpan.PanClient) {
|
||||
func (wer *Worker) SetPanClient(p *config.PanClient) {
|
||||
wer.panClient = p
|
||||
}
|
||||
|
||||
//SetAcceptRange 设置AcceptRange
|
||||
// SetAcceptRange 设置AcceptRange
|
||||
func (wer *Worker) SetAcceptRange(acceptRanges string) {
|
||||
wer.acceptRanges = acceptRanges
|
||||
}
|
||||
|
||||
//SetRange 设置请求范围
|
||||
// SetRange 设置请求范围
|
||||
func (wer *Worker) SetRange(r *transfer.Range) {
|
||||
if wer.wrange == nil {
|
||||
wer.wrange = r
|
||||
@@ -133,33 +136,38 @@ func (wer *Worker) SetRange(r *transfer.Range) {
|
||||
wer.wrange.StoreEnd(r.LoadEnd())
|
||||
}
|
||||
|
||||
//SetWriteMutex 设置数据写锁
|
||||
// SetWriteMutex 设置数据写锁
|
||||
func (wer *Worker) SetWriteMutex(mu *sync.Mutex) {
|
||||
wer.writeMu = mu
|
||||
}
|
||||
|
||||
//SetDownloadStatus 增加其他需要统计的数据
|
||||
// SetDownloadStatus 增加其他需要统计的数据
|
||||
func (wer *Worker) SetDownloadStatus(downloadStatus *transfer.DownloadStatus) {
|
||||
wer.downloadStatus = downloadStatus
|
||||
}
|
||||
|
||||
//GetStatus 返回下载状态
|
||||
// SetUrl 更新新的下载路径
|
||||
func (wer *Worker) SetUrl(newUrl string) {
|
||||
wer.url = newUrl
|
||||
}
|
||||
|
||||
// GetStatus 返回下载状态
|
||||
func (wer *Worker) GetStatus() WorkerStatuser {
|
||||
// 空接口与空指针不等价
|
||||
return &wer.status
|
||||
}
|
||||
|
||||
//GetRange 返回worker范围
|
||||
// GetRange 返回worker范围
|
||||
func (wer *Worker) GetRange() *transfer.Range {
|
||||
return wer.wrange
|
||||
}
|
||||
|
||||
//GetSpeedsPerSecond 获取每秒的速度
|
||||
// GetSpeedsPerSecond 获取每秒的速度
|
||||
func (wer *Worker) GetSpeedsPerSecond() int64 {
|
||||
return wer.speedsStat.GetSpeeds()
|
||||
}
|
||||
|
||||
//Pause 暂停下载
|
||||
// Pause 暂停下载
|
||||
func (wer *Worker) Pause() {
|
||||
wer.lazyInit()
|
||||
if wer.acceptRanges == "" {
|
||||
@@ -174,7 +182,7 @@ func (wer *Worker) Pause() {
|
||||
wer.status.statusCode = StatusCodePaused
|
||||
}
|
||||
|
||||
//Resume 恢复下载
|
||||
// Resume 恢复下载
|
||||
func (wer *Worker) Resume() {
|
||||
if wer.status.statusCode != StatusCodePaused {
|
||||
return
|
||||
@@ -182,7 +190,7 @@ func (wer *Worker) Resume() {
|
||||
go wer.Execute()
|
||||
}
|
||||
|
||||
//Cancel 取消下载
|
||||
// Cancel 取消下载
|
||||
func (wer *Worker) Cancel() error {
|
||||
if wer.workerCancelFunc == nil {
|
||||
return errors.New("cancelFunc not set")
|
||||
@@ -194,7 +202,7 @@ func (wer *Worker) Cancel() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//Reset 重设连接
|
||||
// Reset 重设连接
|
||||
func (wer *Worker) Reset() {
|
||||
if wer.resetFunc == nil {
|
||||
logger.Verbosef("DEBUG: worker: resetFunc not set")
|
||||
@@ -212,7 +220,7 @@ func (wer *Worker) Reset() {
|
||||
func (wer *Worker) RefreshDownloadUrl() {
|
||||
var apierr *apierror.ApiError
|
||||
|
||||
durl, apierr := wer.panClient.GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{DriveId: wer.driveId, FileId: wer.fileId})
|
||||
durl, apierr := wer.panClient.OpenapiPanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{DriveId: wer.driveId, FileId: wer.fileId})
|
||||
if apierr != nil {
|
||||
wer.status.statusCode = StatusCodeTooManyConnections
|
||||
return
|
||||
@@ -225,7 +233,7 @@ func (wer *Worker) Canceled() bool {
|
||||
return wer.status.statusCode == StatusCodeCanceled
|
||||
}
|
||||
|
||||
//Completed 是否已经完成
|
||||
// Completed 是否已经完成
|
||||
func (wer *Worker) Completed() bool {
|
||||
switch wer.status.statusCode {
|
||||
case StatusCodeSuccessed, StatusCodeCanceled:
|
||||
@@ -235,7 +243,7 @@ func (wer *Worker) Completed() bool {
|
||||
}
|
||||
}
|
||||
|
||||
//Failed 是否失败
|
||||
// Failed 是否失败
|
||||
func (wer *Worker) Failed() bool {
|
||||
switch wer.status.statusCode {
|
||||
case StatusCodeFailed, StatusCodeInternalError, StatusCodeTooManyConnections, StatusCodeNetError:
|
||||
@@ -245,17 +253,17 @@ func (wer *Worker) Failed() bool {
|
||||
}
|
||||
}
|
||||
|
||||
//ClearStatus 清空状态
|
||||
// ClearStatus 清空状态
|
||||
func (wer *Worker) ClearStatus() {
|
||||
wer.status.statusCode = StatusCodeInit
|
||||
}
|
||||
|
||||
//Err 返回worker错误
|
||||
// Err 返回worker错误
|
||||
func (wer *Worker) Err() error {
|
||||
return wer.err
|
||||
}
|
||||
|
||||
//Execute 执行任务
|
||||
// Execute 执行任务
|
||||
func (wer *Worker) Execute() {
|
||||
wer.lazyInit()
|
||||
|
||||
@@ -294,11 +302,19 @@ func (wer *Worker) Execute() {
|
||||
|
||||
wer.status.statusCode = StatusCodePending
|
||||
|
||||
var resp *http.Response
|
||||
// check url expired or not
|
||||
if IsUrlExpired(wer.url) {
|
||||
logger.Verbosef("download url expired, renew url and reset worker: %d\n", wer.ID())
|
||||
wer.status.statusCode = StatusCodeDownloadUrlExpired
|
||||
wer.err = errors.New("403")
|
||||
return
|
||||
}
|
||||
|
||||
apierr := wer.panClient.DownloadFileData(wer.url, aliyunpan.FileDownloadRange{
|
||||
// do download data
|
||||
var resp *http.Response
|
||||
apierr := wer.panClient.OpenapiPanClient().DownloadFileData(wer.url, aliyunpan.FileDownloadRange{
|
||||
Offset: wer.wrange.Begin,
|
||||
End: wer.wrange.End - 1,
|
||||
End: wer.wrange.End - 1,
|
||||
}, func(httpMethod, fullUrl string, headers map[string]string) (*http.Response, error) {
|
||||
resp, wer.err = wer.client.Req(httpMethod, fullUrl, nil, headers)
|
||||
if wer.err != nil {
|
||||
@@ -328,16 +344,23 @@ func (wer *Worker) Execute() {
|
||||
break
|
||||
case 416: //Requested Range Not Satisfiable
|
||||
fallthrough
|
||||
case 403: // Forbidden
|
||||
fallthrough
|
||||
case 403: // 链接过期也会返回403
|
||||
if respBody, e := ioutil.ReadAll(resp.Body); e == nil {
|
||||
respBodyStr := string(respBody)
|
||||
if strings.Contains(respBodyStr, "Request has expired") { // 链接已过期
|
||||
logger.Verboseln("download url return 403 error and expired url response")
|
||||
wer.status.statusCode = StatusCodeDownloadUrlExpired
|
||||
wer.err = errors.New(resp.Status)
|
||||
}
|
||||
}
|
||||
return
|
||||
case 406: // Not Acceptable
|
||||
wer.status.statusCode = StatusCodeNetError
|
||||
wer.err = errors.New(resp.Status)
|
||||
return
|
||||
case 404:
|
||||
wer.status.statusCode = StatusCodeDownloadUrlExpired
|
||||
wer.err = errors.New(resp.Status)
|
||||
return
|
||||
logger.Verboseln("request download url 404 error")
|
||||
fallthrough
|
||||
case 429, 509: // Too Many Requests
|
||||
wer.status.SetStatusCode(StatusCodeTooManyConnections)
|
||||
wer.err = errors.New(resp.Status)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -42,11 +42,21 @@ func (us *UploadStatus) TotalSize() int64 {
|
||||
return us.totalSize
|
||||
}
|
||||
|
||||
// SetTotalSize 设置总大小
|
||||
func (us *UploadStatus) SetTotalSize(size int64) {
|
||||
us.totalSize = size
|
||||
}
|
||||
|
||||
// Uploaded 返回已上传数据
|
||||
func (us *UploadStatus) Uploaded() int64 {
|
||||
return us.uploaded
|
||||
}
|
||||
|
||||
// SetUploaded 设置已上传数据
|
||||
func (us *UploadStatus) SetUploaded(size int64) {
|
||||
us.uploaded = size
|
||||
}
|
||||
|
||||
// SpeedsPerSecond 返回每秒的上传速度
|
||||
func (us *UploadStatus) SpeedsPerSecond() int64 {
|
||||
return us.speedsPerSecond
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
package pandownload
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
@@ -30,10 +29,8 @@ import (
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -50,7 +47,7 @@ type (
|
||||
taskInfo *taskframework.TaskInfo // 任务信息
|
||||
|
||||
Cfg *downloader.Config
|
||||
PanClient *aliyunpan.PanClient
|
||||
PanClient *config.PanClient
|
||||
ParentTaskExecutor *taskframework.TaskExecutor
|
||||
|
||||
DownloadStatistic *DownloadStatistic // 下载统计
|
||||
@@ -69,8 +66,7 @@ type (
|
||||
OriginSaveRootPath string // 文件保存在本地的根目录路径
|
||||
DriveId string
|
||||
|
||||
fileInfo *aliyunpan.FileEntity // 文件或目录详情
|
||||
downloadFileSource FileSourceType // 下载项类型,File-普通文件(备份盘/资源库),Album-相册文件
|
||||
fileInfo *aliyunpan.FileEntity // 文件或目录详情
|
||||
|
||||
// 下载文件记录器
|
||||
FileRecorder *log.FileRecorder
|
||||
@@ -101,11 +97,6 @@ const (
|
||||
AlbumFileSource FileSourceType = "album"
|
||||
)
|
||||
|
||||
func (dtu *DownloadTaskUnit) SetFileInfo(fileType FileSourceType, info *aliyunpan.FileEntity) {
|
||||
dtu.downloadFileSource = fileType
|
||||
dtu.fileInfo = info
|
||||
}
|
||||
|
||||
func (dtu *DownloadTaskUnit) SetTaskInfo(info *taskframework.TaskInfo) {
|
||||
dtu.taskInfo = info
|
||||
}
|
||||
@@ -292,20 +283,6 @@ func (dtu *DownloadTaskUnit) download() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// panHTTPClient 获取包含特定User-Agent的HTTPClient
|
||||
func (dtu *DownloadTaskUnit) panHTTPClient() (client *requester.HTTPClient) {
|
||||
client = requester.NewHTTPClient()
|
||||
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 10 {
|
||||
return errors.New("stopped after 10 redirects")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
client.SetTimeout(20 * time.Minute)
|
||||
client.SetKeepAlive(true)
|
||||
return client
|
||||
}
|
||||
|
||||
// handleError 下载错误处理器
|
||||
func (dtu *DownloadTaskUnit) handleError(result *taskframework.TaskUnitRunResult) {
|
||||
switch value := result.Err.(type) {
|
||||
@@ -457,26 +434,19 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
result = &taskframework.TaskUnitRunResult{}
|
||||
// 获取文件信息
|
||||
var apierr *apierror.ApiError
|
||||
if dtu.downloadFileSource != AlbumFileSource { // 相簿文件信息是传递进来的,无法在这里获取
|
||||
if dtu.fileInfo == nil || dtu.taskInfo.Retry() > 0 {
|
||||
// 没有获取文件信息
|
||||
// 如果是动态添加的下载任务, 是会写入文件信息的
|
||||
// 如果该任务重试过, 则应该再获取一次文件信息
|
||||
dtu.fileInfo, apierr = dtu.PanClient.FileInfoByPath(dtu.DriveId, dtu.FilePanPath)
|
||||
if apierr != nil {
|
||||
// 如果不是未登录或文件不存在, 则不重试
|
||||
result.ResultMessage = "获取下载路径信息错误"
|
||||
result.Err = apierr
|
||||
dtu.handleError(result)
|
||||
return
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
} else {
|
||||
if dtu.taskInfo.Retry() > 0 {
|
||||
// 延时避免风控
|
||||
time.Sleep(2 * time.Second)
|
||||
if dtu.fileInfo == nil || dtu.taskInfo.Retry() > 0 {
|
||||
// 没有获取文件信息
|
||||
// 如果是动态添加的下载任务, 是会写入文件信息的
|
||||
// 如果该任务重试过, 则应该再获取一次文件信息
|
||||
dtu.fileInfo, apierr = dtu.PanClient.OpenapiPanClient().FileInfoByPath(dtu.DriveId, dtu.FilePanPath)
|
||||
if apierr != nil {
|
||||
// 如果不是未登录或文件不存在, 则不重试
|
||||
result.ResultMessage = "获取下载路径信息错误"
|
||||
result.Err = apierr
|
||||
dtu.handleError(result)
|
||||
return
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
// 输出文件信息
|
||||
@@ -545,7 +515,7 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
}
|
||||
|
||||
// 获取该目录下的文件列表
|
||||
fileList, apierr := dtu.PanClient.FileListGetAll(&aliyunpan.FileListParam{
|
||||
fileList, apierr := dtu.PanClient.OpenapiPanClient().FileListGetAll(&aliyunpan.FileListParam{
|
||||
DriveId: dtu.DriveId,
|
||||
ParentFileId: dtu.fileInfo.FileId,
|
||||
}, 1000)
|
||||
@@ -553,7 +523,7 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
// retry one more time
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
fileList, apierr = dtu.PanClient.FileListGetAll(&aliyunpan.FileListParam{
|
||||
fileList, apierr = dtu.PanClient.OpenapiPanClient().FileListGetAll(&aliyunpan.FileListParam{
|
||||
DriveId: dtu.DriveId,
|
||||
ParentFileId: dtu.fileInfo.FileId,
|
||||
}, 1000)
|
||||
@@ -644,13 +614,10 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
return result
|
||||
}
|
||||
|
||||
// 文件下载成功,更改文件修改时间
|
||||
if dtu.downloadFileSource == AlbumFileSource {
|
||||
// 只有相册文件才需要更改时间
|
||||
if err := os.Chtimes(dtu.SavePath, utils.ParseTimeStr(dtu.fileInfo.CreatedAt), utils.ParseTimeStr(dtu.fileInfo.CreatedAt)); err != nil {
|
||||
logger.Verbosef(err.Error())
|
||||
}
|
||||
}
|
||||
//// 文件下载成功,更改文件修改时间和云盘的同步
|
||||
//if err := os.Chtimes(dtu.SavePath, utils.ParseTimeStr(dtu.fileInfo.CreatedAt), utils.ParseTimeStr(dtu.fileInfo.CreatedAt)); err != nil {
|
||||
// logger.Verbosef(err.Error())
|
||||
//}
|
||||
|
||||
// 统计下载
|
||||
dtu.DownloadStatistic.AddTotalSize(dtu.fileInfo.FileSize)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/crypto"
|
||||
"github.com/tickstep/library-go/getip"
|
||||
@@ -40,6 +40,17 @@ type QRCodeLoginResult struct {
|
||||
SecureRefreshToken string `json:"secureRefreshToken"`
|
||||
}
|
||||
|
||||
// LoginTokenResult Token结果
|
||||
type LoginTokenResult struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
Expired int64 `json:"expired"`
|
||||
}
|
||||
|
||||
type CommonTokenEntity struct {
|
||||
Openapi *LoginTokenResult `json:"openapi"`
|
||||
Webapi *LoginTokenResult `json:"webapi"`
|
||||
}
|
||||
|
||||
func NewLoginHelper(webHost string) *LoginHelper {
|
||||
return &LoginHelper{
|
||||
webHost: webHost,
|
||||
@@ -57,13 +68,13 @@ func (h *LoginHelper) GetQRCodeLoginUrl(keyStr string) (*QRCodeUrlResult, error)
|
||||
ipAddr = "127.0.0.1"
|
||||
}
|
||||
fmt.Fprintf(&fullUrl, "%s/auth/tickstep/aliyunpan/token/qrcode/create?ip=%s&os=%s&arch=%s&version=%s&key=%s",
|
||||
h.webHost, ipAddr, runtime.GOOS, runtime.GOARCH, config.AppVersion, keyStr)
|
||||
h.webHost, ipAddr, runtime.GOOS, runtime.GOARCH, global.AppVersion, keyStr)
|
||||
|
||||
logger.Verboseln("do request url: " + fullUrl.String())
|
||||
header := map[string]string{
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"content-type": "application/json;charset=UTF-8",
|
||||
"user-agent": "aliyunpan/" + config.AppVersion,
|
||||
"user-agent": "aliyunpan/" + global.AppVersion,
|
||||
}
|
||||
// request
|
||||
client := requester.NewHTTPClient()
|
||||
@@ -114,7 +125,7 @@ func (h *LoginHelper) GetQRCodeLoginResult(tokenId string) (*QRCodeLoginResult,
|
||||
header := map[string]string{
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"content-type": "application/json;charset=UTF-8",
|
||||
"user-agent": "aliyunpan/" + config.AppVersion,
|
||||
"user-agent": "aliyunpan/" + global.AppVersion,
|
||||
}
|
||||
// request
|
||||
client := requester.NewHTTPClient()
|
||||
@@ -154,7 +165,7 @@ func (h *LoginHelper) GetRefreshToken(tokenId string) (*QRCodeLoginResult, error
|
||||
header := map[string]string{
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"content-type": "application/json;charset=UTF-8",
|
||||
"user-agent": "aliyunpan/" + config.AppVersion,
|
||||
"user-agent": "aliyunpan/" + global.AppVersion,
|
||||
}
|
||||
// request
|
||||
client := requester.NewHTTPClient()
|
||||
@@ -220,3 +231,125 @@ func (h *LoginHelper) ParseSecureRefreshToken(keyStr, secureRefreshToken string)
|
||||
}
|
||||
return refreshToken, nil
|
||||
}
|
||||
|
||||
// GetWebapiNewToken 获取Webapi Token
|
||||
func (h *LoginHelper) GetWebapiNewToken(ticketId, userId, oldAccessToken string) (*LoginTokenResult, error) {
|
||||
fullUrl := strings.Builder{}
|
||||
fmt.Fprintf(&fullUrl, "%s/auth/tickstep/aliyunpan/token/webapi/%s/refresh?userId=%s",
|
||||
h.webHost, ticketId, userId)
|
||||
logger.Verboseln("do request url: " + fullUrl.String())
|
||||
header := map[string]string{
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"content-type": "application/json;charset=UTF-8",
|
||||
"user-agent": "aliyunpan/" + global.AppVersion,
|
||||
"old-token": oldAccessToken,
|
||||
}
|
||||
// request
|
||||
client := requester.NewHTTPClient()
|
||||
client.SetTimeout(20 * time.Second)
|
||||
client.SetKeepAlive(false)
|
||||
body, err := client.Fetch("GET", fullUrl.String(), nil, header)
|
||||
if err != nil {
|
||||
logger.Verboseln("get web token result error ", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errResp := &LoginHttpResult{}
|
||||
if err1 := json.Unmarshal(body, errResp); err1 != nil {
|
||||
logger.Verboseln("parse result json error ", err1)
|
||||
return nil, err1
|
||||
}
|
||||
if errResp.Code != 0 {
|
||||
return nil, fmt.Errorf(errResp.Msg)
|
||||
}
|
||||
|
||||
// parse result
|
||||
r := &LoginHttpResult{}
|
||||
r.Data = &LoginTokenResult{}
|
||||
if err2 := json.Unmarshal(body, r); err2 != nil {
|
||||
logger.Verboseln("parse web token result json error ", err2)
|
||||
return nil, err2
|
||||
}
|
||||
return r.Data.(*LoginTokenResult), nil
|
||||
}
|
||||
|
||||
// GetOpenapiNewToken 获取Openapi Token
|
||||
func (h *LoginHelper) GetOpenapiNewToken(ticketId, userId, oldAccessToken string) (*LoginTokenResult, error) {
|
||||
fullUrl := strings.Builder{}
|
||||
fmt.Fprintf(&fullUrl, "%s/auth/tickstep/aliyunpan/token/openapi/%s/refresh?userId=%s",
|
||||
h.webHost, ticketId, userId)
|
||||
logger.Verboseln("do request url: " + fullUrl.String())
|
||||
header := map[string]string{
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"content-type": "application/json;charset=UTF-8",
|
||||
"user-agent": "aliyunpan/" + global.AppVersion,
|
||||
"old-token": oldAccessToken,
|
||||
}
|
||||
// request
|
||||
client := requester.NewHTTPClient()
|
||||
client.SetTimeout(20 * time.Second)
|
||||
client.SetKeepAlive(false)
|
||||
body, err := client.Fetch("GET", fullUrl.String(), nil, header)
|
||||
if err != nil {
|
||||
logger.Verboseln("get openapi token result error ", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errResp := &LoginHttpResult{}
|
||||
if err1 := json.Unmarshal(body, errResp); err1 != nil {
|
||||
logger.Verboseln("parse result json error ", err1)
|
||||
return nil, err1
|
||||
}
|
||||
if errResp.Code != 0 {
|
||||
return nil, fmt.Errorf(errResp.Msg)
|
||||
}
|
||||
|
||||
// parse result
|
||||
r := &LoginHttpResult{}
|
||||
r.Data = &LoginTokenResult{}
|
||||
if err2 := json.Unmarshal(body, r); err2 != nil {
|
||||
logger.Verboseln("parse openapi token result json error ", err2)
|
||||
return nil, err2
|
||||
}
|
||||
return r.Data.(*LoginTokenResult), nil
|
||||
}
|
||||
|
||||
// GetLoginToken 获取登录后的Token
|
||||
func (h *LoginHelper) GetLoginToken(ticketId string) (*CommonTokenEntity, error) {
|
||||
fullUrl := strings.Builder{}
|
||||
fmt.Fprintf(&fullUrl, "%s/auth/tickstep/aliyunpan/token/common/%s/login",
|
||||
h.webHost, ticketId)
|
||||
logger.Verboseln("do request url: " + fullUrl.String())
|
||||
header := map[string]string{
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"content-type": "application/json;charset=UTF-8",
|
||||
"user-agent": "aliyunpan/" + global.AppVersion,
|
||||
}
|
||||
// request
|
||||
client := requester.NewHTTPClient()
|
||||
client.SetTimeout(20 * time.Second)
|
||||
client.SetKeepAlive(false)
|
||||
body, err := client.Fetch("GET", fullUrl.String(), nil, header)
|
||||
if err != nil {
|
||||
logger.Verboseln("get login token result error ", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errResp := &LoginHttpResult{}
|
||||
if err1 := json.Unmarshal(body, errResp); err1 != nil {
|
||||
logger.Verboseln("parse result json error ", err1)
|
||||
return nil, err1
|
||||
}
|
||||
if errResp.Code != 0 {
|
||||
return nil, fmt.Errorf(errResp.Msg)
|
||||
}
|
||||
|
||||
// parse result
|
||||
r := &LoginHttpResult{}
|
||||
r.Data = &CommonTokenEntity{}
|
||||
if err2 := json.Unmarshal(body, r); err2 != nil {
|
||||
logger.Verboseln("parse login token result json error ", err2)
|
||||
return nil, err2
|
||||
}
|
||||
return r.Data.(*CommonTokenEntity), nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -16,6 +16,7 @@ package panupload
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"io"
|
||||
@@ -31,13 +32,12 @@ import (
|
||||
|
||||
type (
|
||||
PanUpload struct {
|
||||
panClient *aliyunpan.PanClient
|
||||
panClient *config.PanClient
|
||||
targetPath string
|
||||
driveId string
|
||||
|
||||
// 网盘上传参数
|
||||
uploadOpEntity *aliyunpan.CreateFileUploadResult
|
||||
useInternalUrl bool
|
||||
}
|
||||
|
||||
EmptyReaderLen64 struct {
|
||||
@@ -52,19 +52,18 @@ func (e EmptyReaderLen64) Len() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func NewPanUpload(panClient *aliyunpan.PanClient, targetPath, driveId string, uploadOpEntity *aliyunpan.CreateFileUploadResult, useInternalUrl bool) uploader.MultiUpload {
|
||||
func NewPanUpload(panClient *config.PanClient, targetPath, driveId string, uploadOpEntity *aliyunpan.CreateFileUploadResult) uploader.MultiUpload {
|
||||
return &PanUpload{
|
||||
panClient: panClient,
|
||||
targetPath: targetPath,
|
||||
driveId: driveId,
|
||||
uploadOpEntity: uploadOpEntity,
|
||||
useInternalUrl: useInternalUrl,
|
||||
}
|
||||
}
|
||||
|
||||
func (pu *PanUpload) lazyInit() {
|
||||
if pu.panClient == nil {
|
||||
pu.panClient = &aliyunpan.PanClient{}
|
||||
pu.panClient = &config.PanClient{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,9 +76,6 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
|
||||
// check url expired or not
|
||||
uploadUrl := pu.uploadOpEntity.PartInfoList[partseq].UploadURL
|
||||
if pu.useInternalUrl {
|
||||
uploadUrl = pu.uploadOpEntity.PartInfoList[partseq].InternalUploadURL
|
||||
}
|
||||
if IsUrlExpired(uploadUrl) {
|
||||
// get renew upload url
|
||||
infoList := make([]aliyunpan.FileUploadPartInfoParam, 0)
|
||||
@@ -94,7 +90,7 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
PartInfoList: infoList,
|
||||
UploadId: pu.uploadOpEntity.UploadId,
|
||||
}
|
||||
newUploadInfo, err := pu.panClient.GetUploadUrl(refreshUploadParam)
|
||||
newUploadInfo, err := pu.panClient.OpenapiPanClient().GetUploadUrl(refreshUploadParam)
|
||||
if err != nil {
|
||||
logger.Verboseln(err)
|
||||
return false, &uploader.MultiError{
|
||||
@@ -182,15 +178,12 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
|
||||
// 上传一个分片数据
|
||||
uploadUrl = pu.uploadOpEntity.PartInfoList[partseq].UploadURL
|
||||
if pu.useInternalUrl {
|
||||
uploadUrl = pu.uploadOpEntity.PartInfoList[partseq].InternalUploadURL
|
||||
}
|
||||
apiError := pu.panClient.UploadFileData(uploadUrl, uploadFunc)
|
||||
apiError := pu.panClient.OpenapiPanClient().UploadFileData(uploadUrl, uploadFunc)
|
||||
|
||||
if respErr != nil {
|
||||
if respErr.Err == uploader.UploadUrlExpired {
|
||||
// URL过期,获取新的URL
|
||||
guur, er := pu.panClient.GetUploadUrl(&aliyunpan.GetUploadUrlParam{
|
||||
guur, er := pu.panClient.OpenapiPanClient().GetUploadUrl(&aliyunpan.GetUploadUrlParam{
|
||||
DriveId: pu.driveId,
|
||||
FileId: pu.uploadOpEntity.FileId,
|
||||
UploadId: pu.uploadOpEntity.UploadId,
|
||||
@@ -205,10 +198,7 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
// 获取新的上传URL重试一次
|
||||
pu.uploadOpEntity.PartInfoList[partseq] = guur.PartInfoList[0]
|
||||
uploadUrl := pu.uploadOpEntity.PartInfoList[partseq].UploadURL
|
||||
if pu.useInternalUrl {
|
||||
uploadUrl = pu.uploadOpEntity.PartInfoList[partseq].InternalUploadURL
|
||||
}
|
||||
apiError = pu.panClient.UploadFileData(uploadUrl, uploadFunc)
|
||||
apiError = pu.panClient.OpenapiPanClient().UploadFileData(uploadUrl, uploadFunc)
|
||||
} else if respErr.Err == uploader.UploadPartAlreadyExist {
|
||||
// already upload
|
||||
// success
|
||||
@@ -233,24 +223,11 @@ func (pu *PanUpload) CommitFile() (cerr error) {
|
||||
pu.lazyInit()
|
||||
var er *apierror.ApiError
|
||||
|
||||
_, er = pu.panClient.CompleteUploadFile(&aliyunpan.CompleteUploadFileParam{
|
||||
_, er = pu.panClient.OpenapiPanClient().CompleteUploadFile(&aliyunpan.CompleteUploadFileParam{
|
||||
DriveId: pu.driveId,
|
||||
FileId: pu.uploadOpEntity.FileId,
|
||||
UploadId: pu.uploadOpEntity.UploadId,
|
||||
})
|
||||
if er != nil && er.Code == apierror.ApiCodeDeviceSessionSignatureInvalid {
|
||||
_, e := pu.panClient.CreateSession(nil)
|
||||
if e == nil {
|
||||
// retry
|
||||
_, er = pu.panClient.CompleteUploadFile(&aliyunpan.CompleteUploadFileParam{
|
||||
DriveId: pu.driveId,
|
||||
FileId: pu.uploadOpEntity.FileId,
|
||||
UploadId: pu.uploadOpEntity.UploadId,
|
||||
})
|
||||
} else {
|
||||
logger.Verboseln("CreateSession failed")
|
||||
}
|
||||
}
|
||||
if er != nil {
|
||||
return er
|
||||
}
|
||||
@@ -266,7 +243,7 @@ func (pu *PanUpload) triggerVideoTranscodeAction() {
|
||||
// 视频文件触发云端转码请求
|
||||
if pu.uploadOpEntity != nil && IsVideoFile(pu.uploadOpEntity.FileName) {
|
||||
time.Sleep(3 * time.Second)
|
||||
_, er1 := pu.panClient.VideoGetPreviewPlayInfo(&aliyunpan.VideoGetPreviewPlayInfoParam{
|
||||
_, er1 := pu.panClient.OpenapiPanClient().VideoGetPreviewPlayInfo(&aliyunpan.VideoGetPreviewPlayInfoParam{
|
||||
DriveId: pu.driveId,
|
||||
FileId: pu.uploadOpEntity.FileId,
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -39,6 +39,11 @@ import (
|
||||
"github.com/tickstep/library-go/requester/rio"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultCheckPreHashFileSize PreHash计算文件大小门限,默认100MB以上文件才计算
|
||||
DefaultCheckPreHashFileSize = 100 * 1024 * 1024
|
||||
)
|
||||
|
||||
type (
|
||||
// StepUpload 上传步骤
|
||||
StepUpload int
|
||||
@@ -51,7 +56,7 @@ type (
|
||||
DriveId string // 网盘ID,例如:文件网盘,相册网盘
|
||||
FolderCreateMutex *sync.Mutex
|
||||
|
||||
PanClient *aliyunpan.PanClient
|
||||
PanClient *config.PanClient
|
||||
UploadingDatabase *UploadingDatabase // 数据库
|
||||
Parallel int
|
||||
NoRapidUpload bool // 禁用秒传,无需计算SHA1,直接上传
|
||||
@@ -68,9 +73,6 @@ type (
|
||||
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
|
||||
IsSkipSameName bool // 跳过已存在的文件,即使文件内容不一致(不检查SHA1)
|
||||
|
||||
// 是否使用内置链接
|
||||
UseInternalUrl bool
|
||||
|
||||
// 全局速度统计
|
||||
GlobalSpeedsStat *speeds.Speeds
|
||||
|
||||
@@ -82,7 +84,7 @@ type (
|
||||
const (
|
||||
// StepUploadInit 初始化步骤
|
||||
StepUploadInit StepUpload = iota
|
||||
// 上传前准备,创建上传任务
|
||||
// StepUploadPrepareUpload 上传前准备,创建上传任务
|
||||
StepUploadPrepareUpload
|
||||
// StepUploadRapidUpload 秒传步骤
|
||||
StepUploadRapidUpload
|
||||
@@ -98,7 +100,7 @@ func (utu *UploadTaskUnit) SetTaskInfo(taskInfo *taskframework.TaskInfo) {
|
||||
utu.taskInfo = taskInfo
|
||||
}
|
||||
|
||||
// prepareFile 解析文件阶段
|
||||
// prepareFile 解析文件准备阶段
|
||||
func (utu *UploadTaskUnit) prepareFile() {
|
||||
// 解析文件保存路径
|
||||
var (
|
||||
@@ -128,6 +130,7 @@ func (utu *UploadTaskUnit) prepareFile() {
|
||||
// utu.Step = StepUploadUpload
|
||||
// return
|
||||
//}
|
||||
|
||||
// 下一步: 秒传
|
||||
utu.Step = StepUploadRapidUpload
|
||||
}
|
||||
@@ -159,7 +162,7 @@ func (utu *UploadTaskUnit) upload() (result *taskframework.TaskUnitRunResult) {
|
||||
// 阿里云盘默认就是分片上传,每一个分片对应一个part_info
|
||||
// 但是不支持分片同时上传,必须单线程,并且按照顺序从1开始一个一个上传
|
||||
muer := uploader.NewMultiUploader(
|
||||
NewPanUpload(utu.PanClient, utu.SavePath, utu.DriveId, utu.LocalFileChecksum.UploadOpEntity, utu.UseInternalUrl),
|
||||
NewPanUpload(utu.PanClient, utu.SavePath, utu.DriveId, utu.LocalFileChecksum.UploadOpEntity),
|
||||
rio.NewFileReaderAtLen64(utu.LocalFileChecksum.GetFile()), &uploader.MultiUploaderConfig{
|
||||
Parallel: utu.Parallel,
|
||||
BlockSize: utu.BlockSize,
|
||||
@@ -322,6 +325,7 @@ func (utu *UploadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
}
|
||||
fmt.Printf("[%s] %s 文件上传结果: %s 耗时 %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), msg, utils.ConvertTime(time.Now().Sub(timeStart)))
|
||||
}()
|
||||
|
||||
// 准备文件
|
||||
utu.prepareFile()
|
||||
logger.Verbosef("[%s] %s 准备结束, 准备耗时 %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utils.ConvertTime(time.Now().Sub(timeStart)))
|
||||
@@ -356,7 +360,7 @@ StepUploadPrepareUpload:
|
||||
if saveFilePath != "/" {
|
||||
utu.FolderCreateMutex.Lock()
|
||||
fmt.Printf("[%s] %s 正在检测和创建云盘文件夹: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), saveFilePath)
|
||||
fe, apierr1 := utu.PanClient.FileInfoByPath(utu.DriveId, saveFilePath)
|
||||
fe, apierr1 := utu.PanClient.OpenapiPanClient().FileInfoByPath(utu.DriveId, saveFilePath)
|
||||
time.Sleep(1 * time.Second)
|
||||
needToCreateFolder := false
|
||||
if apierr1 != nil && apierr1.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
@@ -371,16 +375,7 @@ StepUploadPrepareUpload:
|
||||
}
|
||||
if needToCreateFolder {
|
||||
logger.Verbosef("[%s] %s 创建云盘文件夹: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), saveFilePath)
|
||||
rs, apierr = utu.PanClient.Mkdir(utu.DriveId, "root", saveFilePath)
|
||||
if apierr != nil && apierr.Code == apierror.ApiCodeDeviceSessionSignatureInvalid {
|
||||
_, e := utu.PanClient.CreateSession(nil)
|
||||
if e == nil {
|
||||
// retry
|
||||
rs, apierr = utu.PanClient.Mkdir(utu.DriveId, "root", saveFilePath)
|
||||
} else {
|
||||
logger.Verboseln("CreateSession failed")
|
||||
}
|
||||
}
|
||||
rs, apierr = utu.PanClient.OpenapiPanClient().MkdirByFullPath(utu.DriveId, saveFilePath)
|
||||
if apierr != nil || rs.FileId == "" {
|
||||
result.Err = apierr
|
||||
result.ResultMessage = "创建云盘文件夹失败"
|
||||
@@ -402,7 +397,7 @@ StepUploadPrepareUpload:
|
||||
checkNameMode = "auto_rename"
|
||||
// 如果启用了 覆盖/跳过 已存在的文件,则需要提前检查文件是否存在
|
||||
if utu.IsOverwrite || utu.IsSkipSameName {
|
||||
efi, apierr = utu.PanClient.FileInfoByPath(utu.DriveId, utu.SavePath)
|
||||
efi, apierr = utu.PanClient.OpenapiPanClient().FileInfoByPath(utu.DriveId, utu.SavePath)
|
||||
if apierr != nil && apierr.Code != apierror.ApiCodeFileNotFoundCode {
|
||||
result.Err = apierr
|
||||
result.ResultMessage = "检测同名文件失败"
|
||||
@@ -418,22 +413,49 @@ StepUploadPrepareUpload:
|
||||
}
|
||||
}
|
||||
if !utu.NoRapidUpload {
|
||||
// 计算文件SHA1
|
||||
fmt.Printf("[%s] %s 正在计算文件SHA1: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path.LogicPath)
|
||||
utu.LocalFileChecksum.Sum(localfile.CHECKSUM_SHA1)
|
||||
sha1Str = utu.LocalFileChecksum.SHA1
|
||||
if utu.LocalFileChecksum.Length == 0 {
|
||||
sha1Str = aliyunpan.DefaultZeroSizeFileContentHash
|
||||
// 正常上传流程,检测是否能秒传
|
||||
preHashMatch := true
|
||||
if utu.LocalFileChecksum.Length >= DefaultCheckPreHashFileSize {
|
||||
// 大文件,先计算 PreHash,用于检测是否可能支持秒传
|
||||
preHash := CalcFilePreHash(utu.LocalFileChecksum.Path.RealPath)
|
||||
if len(preHash) > 0 {
|
||||
if b, er := utu.PanClient.OpenapiPanClient().CheckUploadFilePreHash(&aliyunpan.FileUploadCheckPreHashParam{
|
||||
DriveId: utu.DriveId,
|
||||
Name: filepath.Base(utu.SavePath),
|
||||
Size: utu.LocalFileChecksum.Length,
|
||||
ParentFileId: rs.FileId,
|
||||
PreHash: preHash,
|
||||
}); er == nil {
|
||||
preHashMatch = b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// proof code
|
||||
localFile, _ = os.Open(utu.LocalFileChecksum.Path.RealPath)
|
||||
localFileInfo, _ = localFile.Stat()
|
||||
proofCode = aliyunpan.CalcProofCode(utu.PanClient.GetAccessToken(), rio.NewFileReaderAtLen64(localFile), localFileInfo.Size())
|
||||
localFile.Close()
|
||||
if preHashMatch {
|
||||
// 计算完整文件SHA1
|
||||
fmt.Printf("[%s] %s 正在计算文件SHA1: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path.LogicPath)
|
||||
utu.LocalFileChecksum.Sum(localfile.CHECKSUM_SHA1)
|
||||
sha1Str = utu.LocalFileChecksum.SHA1
|
||||
if utu.LocalFileChecksum.Length == 0 {
|
||||
sha1Str = aliyunpan.DefaultZeroSizeFileContentHash
|
||||
}
|
||||
|
||||
// proof code
|
||||
localFile, _ = os.Open(utu.LocalFileChecksum.Path.RealPath)
|
||||
localFileInfo, _ = localFile.Stat()
|
||||
proofCode = aliyunpan.CalcProofCode(utu.PanClient.OpenapiPanClient().GetAccessToken(), rio.NewFileReaderAtLen64(localFile), localFileInfo.Size())
|
||||
localFile.Close()
|
||||
} else {
|
||||
// 无需计算 sha1,直接上传
|
||||
logger.Verboseln("PreHash not match, upload file directly")
|
||||
sha1Str = ""
|
||||
contentHashName = ""
|
||||
checkNameMode = "auto_rename"
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[%s] %s 已经禁用秒传检测,直接上传\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
contentHashName = "none"
|
||||
sha1Str = ""
|
||||
contentHashName = ""
|
||||
checkNameMode = "auto_rename"
|
||||
}
|
||||
|
||||
@@ -448,10 +470,10 @@ StepUploadPrepareUpload:
|
||||
return
|
||||
}
|
||||
// existed, delete it
|
||||
var fileDeleteResult []*aliyunpan.FileBatchActionResult
|
||||
var fileDeleteResult *aliyunpan.FileBatchActionResult
|
||||
var err *apierror.ApiError
|
||||
fileDeleteResult, err = utu.PanClient.FileDelete([]*aliyunpan.FileBatchActionParam{{DriveId: efi.DriveId, FileId: efi.FileId}})
|
||||
if err != nil || len(fileDeleteResult) == 0 {
|
||||
fileDeleteResult, err = utu.PanClient.OpenapiPanClient().FileDelete(&aliyunpan.FileBatchActionParam{DriveId: efi.DriveId, FileId: efi.FileId})
|
||||
if err != nil || !fileDeleteResult.Success {
|
||||
result.Err = err
|
||||
result.ResultMessage = "无法删除文件,请稍后重试"
|
||||
return
|
||||
@@ -469,29 +491,33 @@ StepUploadPrepareUpload:
|
||||
}
|
||||
|
||||
// 创建上传任务
|
||||
appCreateUploadFileParam = &aliyunpan.CreateFileUploadParam{
|
||||
DriveId: utu.DriveId,
|
||||
Name: filepath.Base(utu.SavePath),
|
||||
Size: utu.LocalFileChecksum.Length,
|
||||
ContentHash: sha1Str,
|
||||
ContentHashName: contentHashName,
|
||||
CheckNameMode: checkNameMode,
|
||||
ParentFileId: rs.FileId,
|
||||
BlockSize: utu.BlockSize,
|
||||
ProofCode: proofCode,
|
||||
ProofVersion: "v1",
|
||||
}
|
||||
|
||||
uploadOpEntity, apierr = utu.PanClient.CreateUploadFile(appCreateUploadFileParam)
|
||||
if apierr != nil && apierr.Code == apierror.ApiCodeDeviceSessionSignatureInvalid {
|
||||
_, e := utu.PanClient.CreateSession(nil)
|
||||
if e == nil {
|
||||
// retry
|
||||
uploadOpEntity, apierr = utu.PanClient.CreateUploadFile(appCreateUploadFileParam)
|
||||
} else {
|
||||
logger.Verboseln("CreateSession failed")
|
||||
if sha1Str != "" {
|
||||
// 计算SHA1和ProofCode,该方式支持秒传文件
|
||||
appCreateUploadFileParam = &aliyunpan.CreateFileUploadParam{
|
||||
DriveId: utu.DriveId,
|
||||
Name: filepath.Base(utu.SavePath),
|
||||
Size: utu.LocalFileChecksum.Length,
|
||||
CheckNameMode: checkNameMode,
|
||||
ParentFileId: rs.FileId,
|
||||
BlockSize: utu.BlockSize,
|
||||
ContentHash: sha1Str,
|
||||
ContentHashName: contentHashName,
|
||||
ProofCode: proofCode,
|
||||
ProofVersion: "v1",
|
||||
}
|
||||
} else {
|
||||
// 不支持秒传,不计算SHA1,直接上传文件
|
||||
appCreateUploadFileParam = &aliyunpan.CreateFileUploadParam{
|
||||
DriveId: utu.DriveId,
|
||||
Name: filepath.Base(utu.SavePath),
|
||||
Size: utu.LocalFileChecksum.Length,
|
||||
CheckNameMode: checkNameMode,
|
||||
ParentFileId: rs.FileId,
|
||||
BlockSize: utu.BlockSize,
|
||||
}
|
||||
}
|
||||
|
||||
uploadOpEntity, apierr = utu.PanClient.OpenapiPanClient().CreateUploadFile(appCreateUploadFileParam)
|
||||
if apierr != nil {
|
||||
result.Err = apierr
|
||||
result.ResultMessage = "创建上传任务失败:" + apierr.Error()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -14,10 +14,13 @@
|
||||
package panupload
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -78,3 +81,23 @@ func IsVideoFile(fileName string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CalcFilePreHash 计算文件 PreHash
|
||||
func CalcFilePreHash(filePath string) string {
|
||||
localFile, _ := os.Open(filePath)
|
||||
defer localFile.Close()
|
||||
bytes := make([]byte, 1024)
|
||||
localFile.ReadAt(bytes, 0)
|
||||
sha1w := sha1.New()
|
||||
sha1w.Write(bytes)
|
||||
shaBytes := sha1w.Sum(nil)
|
||||
hashCode := hex.EncodeToString(shaBytes)
|
||||
return strings.ToUpper(hashCode)
|
||||
}
|
||||
|
||||
// UnixTime2LocalFormatStr 时间戳转换为东8区时间字符串
|
||||
func UnixTime2LocalFormatStr(unixTime int64) string {
|
||||
t := time.Unix(unixTime, 0)
|
||||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||||
return t.In(cz).Format("2006-01-02T15:04:05.000Z")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package global
|
||||
|
||||
var (
|
||||
// AppVersion 应用版本
|
||||
AppVersion string
|
||||
|
||||
// IsAppInCliMode 是否在交互模式
|
||||
IsAppInCliMode = false
|
||||
|
||||
// IsSupportNoneOpenApiCommands 是否开启非OpenAPI的命令
|
||||
IsSupportNoneOpenApiCommands = false
|
||||
)
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdliner"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdutil"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/cachepool"
|
||||
@@ -66,7 +67,7 @@ func getReleaseFromTicstep(client *requester.HTTPClient, showPrompt bool) *Relea
|
||||
ipAddr = "127.0.0.1"
|
||||
}
|
||||
fmt.Fprintf(&fullUrl, "http://api.tickstep.com/update/tickstep/aliyunpan/releases/latest?ip=%s&os=%s&arch=%s&version=%s",
|
||||
ipAddr, runtime.GOOS, runtime.GOARCH, config.AppVersion)
|
||||
ipAddr, runtime.GOOS, runtime.GOARCH, global.AppVersion)
|
||||
resp, err := client.Req(http.MethodGet, fullUrl.String(), nil, nil)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
@@ -211,29 +212,25 @@ func CheckUpdate(version string, yes bool) {
|
||||
|
||||
builder := &strings.Builder{}
|
||||
builder.WriteString(ReleaseName + "-" + releaseInfo.TagName + "-" + runtime.GOOS + "-.*?")
|
||||
if runtime.GOOS == "darwin" && (runtime.GOARCH == "arm" || runtime.GOARCH == "arm64") {
|
||||
builder.WriteString("arm")
|
||||
} else {
|
||||
switch runtime.GOARCH {
|
||||
case "amd64":
|
||||
builder.WriteString("(amd64|x86_64|x64)")
|
||||
case "386":
|
||||
builder.WriteString("(386|x86)")
|
||||
case "arm":
|
||||
builder.WriteString("(armv5|armv7|arm)")
|
||||
case "arm64":
|
||||
builder.WriteString("arm64")
|
||||
case "mips":
|
||||
builder.WriteString("mips")
|
||||
case "mips64":
|
||||
builder.WriteString("mips64")
|
||||
case "mipsle":
|
||||
builder.WriteString("(mipsle|mipsel)")
|
||||
case "mips64le":
|
||||
builder.WriteString("(mips64le|mips64el)")
|
||||
default:
|
||||
builder.WriteString(runtime.GOARCH)
|
||||
}
|
||||
switch runtime.GOARCH {
|
||||
case "amd64":
|
||||
builder.WriteString("(amd64|x86_64|x64)")
|
||||
case "386":
|
||||
builder.WriteString("(386|x86)")
|
||||
case "arm":
|
||||
builder.WriteString("(armv5|armv7|arm)")
|
||||
case "arm64":
|
||||
builder.WriteString("arm64")
|
||||
case "mips":
|
||||
builder.WriteString("mips")
|
||||
case "mips64":
|
||||
builder.WriteString("mips64")
|
||||
case "mipsle":
|
||||
builder.WriteString("(mipsle|mipsel)")
|
||||
case "mips64le":
|
||||
builder.WriteString("(mips64le|mips64el)")
|
||||
default:
|
||||
builder.WriteString(runtime.GOARCH)
|
||||
}
|
||||
builder.WriteString("\\.zip")
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package plugins
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@@ -21,7 +22,7 @@ func GetContext(user *config.PanUser) *Context {
|
||||
if user == nil {
|
||||
return &Context{
|
||||
AppName: "aliyunpan",
|
||||
Version: config.AppVersion,
|
||||
Version: global.AppVersion,
|
||||
UserId: "",
|
||||
Nickname: "",
|
||||
FileDriveId: "",
|
||||
@@ -30,7 +31,7 @@ func GetContext(user *config.PanUser) *Context {
|
||||
}
|
||||
return &Context{
|
||||
AppName: "aliyunpan",
|
||||
Version: config.AppVersion,
|
||||
Version: global.AppVersion,
|
||||
UserId: user.UserId,
|
||||
Nickname: user.Nickname,
|
||||
FileDriveId: user.DriveList.GetFileDriveId(),
|
||||
|
||||
@@ -2,6 +2,7 @@ package syncdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"github.com/tickstep/library-go/requester/rio"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -33,7 +35,7 @@ type (
|
||||
panFileDb PanSyncDb
|
||||
syncFileDb SyncFileDb
|
||||
|
||||
panClient *aliyunpan.PanClient
|
||||
panClient *config.PanClient
|
||||
|
||||
syncItem *SyncFileItem
|
||||
maxDownloadRate int64 // 限制最大下载速度
|
||||
@@ -47,12 +49,6 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func (f *FileActionTask) prompt(msg string) {
|
||||
if LogPrompt {
|
||||
fmt.Println("[" + utils.NowTimeStr() + "] " + msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileActionTask) HashCode() string {
|
||||
postfix := ""
|
||||
if f.syncItem.Action == SyncFileActionDownload {
|
||||
@@ -66,7 +62,7 @@ func (f *FileActionTask) HashCode() string {
|
||||
func (f *FileActionTask) DoAction(ctx context.Context) error {
|
||||
logger.Verboseln("file action task:", utils.ObjectToJsonStr(f.syncItem, false))
|
||||
if f.syncItem.Action == SyncFileActionUpload {
|
||||
f.prompt("上传文件:" + f.syncItem.getLocalFileFullPath())
|
||||
PromptPrintln("上传文件:" + f.syncItem.getLocalFileFullPath())
|
||||
if e := f.uploadFile(ctx); e != nil {
|
||||
// TODO: retry / cleanup downloading file
|
||||
return e
|
||||
@@ -75,27 +71,25 @@ func (f *FileActionTask) DoAction(ctx context.Context) error {
|
||||
// save local file info into db
|
||||
var actFile *aliyunpan.FileEntity
|
||||
if f.syncItem.UploadEntity != nil && f.syncItem.UploadEntity.FileId != "" {
|
||||
if file, er := f.panClient.FileInfoById(f.syncItem.DriveId, f.syncItem.UploadEntity.FileId); er == nil {
|
||||
if file, er := f.panClient.OpenapiPanClient().FileInfoById(f.syncItem.DriveId, f.syncItem.UploadEntity.FileId); er == nil {
|
||||
file.Path = f.syncItem.getPanFileFullPath()
|
||||
fItem := NewPanFileItem(file)
|
||||
fItem.ScanTimeAt = utils.NowTimeStr()
|
||||
f.panFileDb.Add(fItem)
|
||||
|
||||
actFile = file
|
||||
}
|
||||
} else {
|
||||
if file, er := f.panClient.FileInfoByPath(f.syncItem.DriveId, f.syncItem.getPanFileFullPath()); er == nil {
|
||||
if file, er := f.panClient.OpenapiPanClient().FileInfoByPath(f.syncItem.DriveId, f.syncItem.getPanFileFullPath()); er == nil {
|
||||
file.Path = f.syncItem.getPanFileFullPath()
|
||||
fItem := NewPanFileItem(file)
|
||||
fItem.ScanTimeAt = utils.NowTimeStr()
|
||||
f.panFileDb.Add(fItem)
|
||||
|
||||
actFile = file
|
||||
}
|
||||
}
|
||||
|
||||
// recorder
|
||||
if actFile != nil {
|
||||
// save file sha1 to local DB
|
||||
if file, e := f.localFileDb.Get(f.syncItem.getLocalFileFullPath()); e == nil {
|
||||
file.Sha1Hash = actFile.ContentHash
|
||||
f.localFileDb.Update(file)
|
||||
}
|
||||
|
||||
// recorder file
|
||||
f.appendRecord(&log.FileRecordItem{
|
||||
Status: "成功-上传",
|
||||
TimeStr: utils.NowTimeStr(),
|
||||
@@ -107,7 +101,7 @@ func (f *FileActionTask) DoAction(ctx context.Context) error {
|
||||
}
|
||||
|
||||
if f.syncItem.Action == SyncFileActionDownload {
|
||||
f.prompt("下载文件:" + f.syncItem.getPanFileFullPath())
|
||||
PromptPrintln("下载文件:" + f.syncItem.getPanFileFullPath())
|
||||
if e := f.downloadFile(ctx); e != nil {
|
||||
// TODO: retry / cleanup downloading file
|
||||
return e
|
||||
@@ -167,90 +161,11 @@ func (f *FileActionTask) DoAction(ctx context.Context) error {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if f.syncItem.Action == SyncFileActionDeleteLocal {
|
||||
f.prompt("删除本地文件:" + f.syncItem.getLocalFileFullPath())
|
||||
if e := f.deleteLocalFile(ctx); e != nil {
|
||||
// TODO: retry
|
||||
return e
|
||||
} else {
|
||||
// clear DB
|
||||
f.localFileDb.Delete(f.syncItem.getLocalFileFullPath())
|
||||
f.panFileDb.Delete(f.syncItem.getPanFileFullPath())
|
||||
|
||||
// recorder
|
||||
f.appendRecord(&log.FileRecordItem{
|
||||
Status: "成功-删除本地文件",
|
||||
TimeStr: utils.NowTimeStr(),
|
||||
FileSize: 0,
|
||||
FilePath: f.syncItem.getLocalFileFullPath(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if f.syncItem.Action == SyncFileActionDeletePan {
|
||||
f.prompt("删除云盘文件:" + f.syncItem.getPanFileFullPath())
|
||||
if e := f.deletePanFile(ctx); e != nil {
|
||||
// TODO: retry
|
||||
return e
|
||||
} else {
|
||||
// clear DB
|
||||
f.localFileDb.Delete(f.syncItem.getLocalFileFullPath())
|
||||
f.panFileDb.Delete(f.syncItem.getPanFileFullPath())
|
||||
|
||||
// recorder
|
||||
f.appendRecord(&log.FileRecordItem{
|
||||
Status: "成功-删除云盘文件",
|
||||
TimeStr: utils.NowTimeStr(),
|
||||
FileSize: 0,
|
||||
FilePath: f.syncItem.getPanFileFullPath(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if f.syncItem.Action == SyncFileActionCreateLocalFolder {
|
||||
f.prompt("创建本地文件夹:" + f.syncItem.getLocalFileFullPath())
|
||||
if e := f.createLocalFolder(ctx); e != nil {
|
||||
// TODO: retry
|
||||
return e
|
||||
} else {
|
||||
if file, er := os.Stat(f.syncItem.getLocalFileFullPath()); er == nil {
|
||||
f.localFileDb.Add(&LocalFileItem{
|
||||
FileName: file.Name(),
|
||||
FileSize: file.Size(),
|
||||
FileType: "folder",
|
||||
CreatedAt: file.ModTime().Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: file.ModTime().Format("2006-01-02 15:04:05"),
|
||||
FileExtension: "",
|
||||
Sha1Hash: f.syncItem.PanFile.Sha1Hash,
|
||||
Path: f.syncItem.getLocalFileFullPath(),
|
||||
ScanTimeAt: utils.NowTimeStr(),
|
||||
ScanStatus: ScanStatusNormal,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if f.syncItem.Action == SyncFileActionCreatePanFolder {
|
||||
f.prompt("创建云盘文件夹:" + f.syncItem.getPanFileFullPath())
|
||||
if e := f.createPanFolder(ctx); e != nil {
|
||||
// TODO: retry
|
||||
return e
|
||||
} else {
|
||||
if file, er := f.panClient.FileInfoByPath(f.syncItem.DriveId, f.syncItem.getPanFileFullPath()); er == nil {
|
||||
file.Path = f.syncItem.getPanFileFullPath()
|
||||
fItem := NewPanFileItem(file)
|
||||
fItem.ScanTimeAt = utils.NowTimeStr()
|
||||
f.panFileDb.Add(fItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileActionTask) downloadFile(ctx context.Context) error {
|
||||
durl, apierr := f.panClient.GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
|
||||
durl, apierr := f.panClient.OpenapiPanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
|
||||
DriveId: f.syncItem.PanFile.DriveId,
|
||||
FileId: f.syncItem.PanFile.FileId,
|
||||
})
|
||||
@@ -293,40 +208,56 @@ func (f *FileActionTask) downloadFile(ctx context.Context) error {
|
||||
defer file.Close()
|
||||
if f.syncItem.PanFile.FileSize == 0 {
|
||||
// zero file
|
||||
f.syncItem.Status = SyncFileStatusDownloading
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return nil
|
||||
}
|
||||
if f.syncItem.DownloadRange == nil {
|
||||
f.syncItem.DownloadRange = &transfer.Range{
|
||||
Begin: 0,
|
||||
End: f.syncItem.DownloadBlockSize,
|
||||
}
|
||||
}
|
||||
|
||||
downloadUrl := durl.Url
|
||||
if f.syncItem.UseInternalUrl {
|
||||
downloadUrl = durl.InternalUrl
|
||||
}
|
||||
worker := downloader.NewWorker(0, f.syncItem.PanFile.DriveId, f.syncItem.PanFile.FileId, downloadUrl, writer, nil)
|
||||
|
||||
status := &transfer.DownloadStatus{}
|
||||
status.AddDownloaded(f.syncItem.DownloadRange.Begin)
|
||||
status.SetTotalSize(f.syncItem.PanFile.FileSize)
|
||||
// 限速
|
||||
if f.maxDownloadRate > 0 {
|
||||
rl := speeds.NewRateLimit(f.maxDownloadRate)
|
||||
defer rl.Stop()
|
||||
status := &transfer.DownloadStatus{}
|
||||
status.SetRateLimit(rl)
|
||||
worker.SetDownloadStatus(status)
|
||||
|
||||
//go func() {
|
||||
// for {
|
||||
// time.Sleep(1000 * time.Millisecond)
|
||||
// builder := &strings.Builder{}
|
||||
// status.UpdateSpeeds()
|
||||
// fmt.Fprintf(builder, "\r↓ %s/%s %s/s ............",
|
||||
// converter.ConvertFileSize(status.Downloaded(), 2),
|
||||
// converter.ConvertFileSize(status.TotalSize(), 2),
|
||||
// converter.ConvertFileSize(status.SpeedsPerSecond(), 2),
|
||||
// )
|
||||
// fmt.Print(builder.String())
|
||||
// }
|
||||
//}()
|
||||
}
|
||||
worker.SetDownloadStatus(status)
|
||||
completed := make(chan struct{}, 0)
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-completed:
|
||||
return
|
||||
case <-ticker.C:
|
||||
time.Sleep(time.Duration(rand.Intn(10)*33) * time.Millisecond) // 延迟随机时间
|
||||
builder := &strings.Builder{}
|
||||
status.UpdateSpeeds()
|
||||
downloadedPercentage := fmt.Sprintf("%.2f%%", float64(status.Downloaded())/float64(status.TotalSize())*100)
|
||||
fmt.Fprintf(builder, "\r下载到本地:%s ↓ %s/%s(%s) %s/s............",
|
||||
f.syncItem.getLocalFileFullPath(),
|
||||
converter.ConvertFileSize(status.Downloaded(), 2),
|
||||
converter.ConvertFileSize(status.TotalSize(), 2),
|
||||
downloadedPercentage,
|
||||
converter.ConvertFileSize(status.SpeedsPerSecond(), 2),
|
||||
)
|
||||
PromptPrint(builder.String())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
client := requester.NewHTTPClient()
|
||||
client.SetKeepAlive(true)
|
||||
@@ -338,12 +269,6 @@ func (f *FileActionTask) downloadFile(ctx context.Context) error {
|
||||
worker.SetWriteMutex(writeMu)
|
||||
worker.SetTotalSize(f.syncItem.PanFile.FileSize)
|
||||
worker.SetAcceptRange("bytes")
|
||||
if f.syncItem.DownloadRange == nil {
|
||||
f.syncItem.DownloadRange = &transfer.Range{
|
||||
Begin: 0,
|
||||
End: f.syncItem.DownloadBlockSize,
|
||||
}
|
||||
}
|
||||
worker.SetRange(f.syncItem.DownloadRange) // 分片
|
||||
|
||||
// update status
|
||||
@@ -355,8 +280,9 @@ func (f *FileActionTask) downloadFile(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// cancel routine & done
|
||||
logger.Verboseln("file download routine done")
|
||||
return nil
|
||||
logger.Verboseln("file download routine cancel")
|
||||
close(completed)
|
||||
return errors.New("file download routine cancel")
|
||||
default:
|
||||
logger.Verboseln("do file download process")
|
||||
if f.syncItem.DownloadRange.End > f.syncItem.PanFile.FileSize {
|
||||
@@ -366,6 +292,10 @@ func (f *FileActionTask) downloadFile(ctx context.Context) error {
|
||||
|
||||
// 检查上次执行是否有下载已完成
|
||||
if f.syncItem.DownloadRange.Begin == f.syncItem.PanFile.FileSize {
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
close(completed)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -376,9 +306,11 @@ func (f *FileActionTask) downloadFile(ctx context.Context) error {
|
||||
if worker.GetStatus().StatusCode() == downloader.StatusCodeSuccessed {
|
||||
if f.syncItem.DownloadRange.End == f.syncItem.PanFile.FileSize {
|
||||
// finished
|
||||
f.syncItem.Status = SyncFileStatusDownloading
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
close(completed)
|
||||
PromptPrintln("下载完毕:" + f.syncItem.getLocalFileFullPath())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -388,8 +320,34 @@ func (f *FileActionTask) downloadFile(ctx context.Context) error {
|
||||
|
||||
// 存储状态
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
} else if worker.GetStatus().StatusCode() == downloader.StatusCodeDownloadUrlExpired {
|
||||
logger.Verboseln("download url expired: ", f.syncItem.PanFile.Path)
|
||||
// 下载链接过期,获取新的链接
|
||||
newUrl, apierr1 := f.panClient.OpenapiPanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
|
||||
DriveId: f.syncItem.PanFile.DriveId,
|
||||
FileId: f.syncItem.PanFile.FileId,
|
||||
})
|
||||
time.Sleep(time.Duration(3) * time.Second)
|
||||
if apierr1 != nil {
|
||||
if apierr1.Code == apierror.ApiCodeFileNotFoundCode || apierr1.Code == apierror.ApiCodeForbiddenFileInTheRecycleBin {
|
||||
f.syncItem.Status = SyncFileStatusNotExisted
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return fmt.Errorf("文件不存在")
|
||||
}
|
||||
logger.Verbosef("ERROR: get download url error: %s, %s\n", f.syncItem.PanFile.Path, apierr.Error())
|
||||
return apierr
|
||||
}
|
||||
if newUrl == nil || newUrl.Url == "" {
|
||||
logger.Verbosef("无法获取有效的下载链接: %+v\n", durl)
|
||||
f.syncItem.Status = SyncFileStatusFailed
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return fmt.Errorf("无法获取有效的下载链接")
|
||||
}
|
||||
logger.Verboseln("query new download url: ", newUrl.Url)
|
||||
worker.SetUrl(newUrl.Url)
|
||||
}
|
||||
// TODO: 下载链接过期处理
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,81 +378,110 @@ func (f *FileActionTask) uploadFile(ctx context.Context) error {
|
||||
targetPanFilePath := f.syncItem.getPanFileFullPath()
|
||||
|
||||
if f.syncItem.UploadEntity == nil {
|
||||
// 尝试创建文件夹
|
||||
panDirPath := filepath.Dir(targetPanFilePath)
|
||||
panDirFileId := ""
|
||||
logger.Verbosef("检测云盘文件夹: %s\n", panDirPath)
|
||||
if dirFile, er2 := f.panClient.OpenapiPanClient().FileInfoByPath(f.syncItem.DriveId, panDirPath); er2 != nil {
|
||||
if er2.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
logger.Verbosef("创建云盘文件夹: %s\n", panDirPath)
|
||||
f.panFolderCreateMutex.Lock()
|
||||
rs, apierr1 := f.panClient.OpenapiPanClient().MkdirByFullPath(f.syncItem.DriveId, panDirPath)
|
||||
f.panFolderCreateMutex.Unlock()
|
||||
if apierr1 != nil || rs.FileId == "" {
|
||||
return apierr1
|
||||
}
|
||||
panDirFileId = rs.FileId
|
||||
logger.Verbosef("创建云盘文件夹成功: %s\n", panDirPath)
|
||||
} else {
|
||||
logger.Verbosef("查询云盘文件夹错误: %s\n", er2.String())
|
||||
return er2
|
||||
}
|
||||
} else {
|
||||
if dirFile != nil && dirFile.FileId != "" {
|
||||
panDirFileId = dirFile.FileId
|
||||
}
|
||||
}
|
||||
|
||||
// 计算文件SHA1
|
||||
sha1Str := ""
|
||||
|
||||
proofCode := ""
|
||||
contentHashName := "sha1"
|
||||
if f.syncItem.LocalFile.Sha1Hash != "" {
|
||||
sha1Str = f.syncItem.LocalFile.Sha1Hash
|
||||
} else {
|
||||
logger.Verbosef("正在计算文件SHA1: %s\n", localFile.Path)
|
||||
if localFile.Length == 0 {
|
||||
sha1Str = aliyunpan.DefaultZeroSizeFileContentHash
|
||||
} else {
|
||||
localFile.Sum(localfile.CHECKSUM_SHA1)
|
||||
sha1Str = localFile.SHA1
|
||||
// 正常上传流程,检测是否能秒传
|
||||
preHashMatch := true
|
||||
if f.syncItem.LocalFile.FileSize >= panupload.DefaultCheckPreHashFileSize {
|
||||
// 大文件,先计算 PreHash,用于检测是否可能支持秒传
|
||||
preHash := panupload.CalcFilePreHash(f.syncItem.LocalFile.Path)
|
||||
if len(preHash) > 0 {
|
||||
if b, er := f.panClient.OpenapiPanClient().CheckUploadFilePreHash(&aliyunpan.FileUploadCheckPreHashParam{
|
||||
DriveId: f.syncItem.DriveId,
|
||||
Name: f.syncItem.LocalFile.FileName,
|
||||
Size: f.syncItem.LocalFile.FileSize,
|
||||
ParentFileId: panDirFileId,
|
||||
PreHash: preHash,
|
||||
}); er == nil {
|
||||
preHashMatch = b
|
||||
}
|
||||
}
|
||||
}
|
||||
if preHashMatch {
|
||||
// 再计算完整文件SHA1
|
||||
logger.Verbosef("正在计算文件SHA1: %s\n", localFile.Path)
|
||||
if localFile.Length == 0 {
|
||||
sha1Str = aliyunpan.DefaultZeroSizeFileContentHash
|
||||
} else {
|
||||
localFile.Sum(localfile.CHECKSUM_SHA1)
|
||||
sha1Str = localFile.SHA1
|
||||
}
|
||||
f.syncItem.LocalFile.Sha1Hash = sha1Str
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
|
||||
// 计算proof code
|
||||
localFileEntity, _ := os.Open(localFile.Path.RealPath)
|
||||
localFileInfo, _ := localFileEntity.Stat()
|
||||
proofCode = aliyunpan.CalcProofCode(f.panClient.OpenapiPanClient().GetAccessToken(), rio.NewFileReaderAtLen64(localFileEntity), localFileInfo.Size())
|
||||
} else {
|
||||
// 无需计算 sha1,直接上传
|
||||
logger.Verboseln("PreHash not match, upload file directly")
|
||||
sha1Str = ""
|
||||
contentHashName = ""
|
||||
}
|
||||
f.syncItem.LocalFile.Sha1Hash = sha1Str
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
}
|
||||
|
||||
// 检查同名文件是否存在
|
||||
panFileId := ""
|
||||
if panFileInDb, e := f.panFileDb.Get(targetPanFilePath); e == nil {
|
||||
if panFileInDb != nil {
|
||||
panFileId = panFileInDb.FileId
|
||||
}
|
||||
} else {
|
||||
efi, apierr := f.panClient.FileInfoByPath(f.syncItem.DriveId, targetPanFilePath)
|
||||
if apierr != nil && apierr.Code != apierror.ApiCodeFileNotFoundCode {
|
||||
logger.Verbosef("上传文件错误: %s\n", apierr.String())
|
||||
return apierr
|
||||
}
|
||||
if efi != nil && efi.FileId != "" {
|
||||
panFileId = efi.FileId
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
panFileSha1Str := ""
|
||||
efi, apierr := f.panClient.OpenapiPanClient().FileInfoByPath(f.syncItem.DriveId, targetPanFilePath)
|
||||
if apierr != nil && apierr.Code != apierror.ApiCodeFileNotFoundCode {
|
||||
logger.Verbosef("上传文件错误: %s\n", apierr.String())
|
||||
return apierr
|
||||
}
|
||||
if strings.ToUpper(panFileId) == strings.ToUpper(sha1Str) {
|
||||
logger.Verbosef("检测到同名文件,文件内容完全一致,无需重复上传: %s\n", targetPanFilePath)
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return nil
|
||||
if efi != nil && efi.FileId != "" {
|
||||
panFileId = efi.FileId
|
||||
}
|
||||
|
||||
// 创建文件夹
|
||||
panDirPath := filepath.Dir(targetPanFilePath)
|
||||
panDirFileId := ""
|
||||
if panDirItem, er := f.panFileDb.Get(panDirPath); er == nil {
|
||||
if panDirItem != nil && panDirItem.IsFolder() {
|
||||
panDirFileId = panDirItem.FileId
|
||||
}
|
||||
} else {
|
||||
logger.Verbosef("创建云盘文件夹: %s\n", panDirPath)
|
||||
f.panFolderCreateMutex.Lock()
|
||||
rs, apierr1 := f.panClient.Mkdir(f.syncItem.DriveId, "root", panDirPath)
|
||||
f.panFolderCreateMutex.Unlock()
|
||||
if apierr1 != nil || rs.FileId == "" {
|
||||
return apierr1
|
||||
}
|
||||
panDirFileId = rs.FileId
|
||||
logger.Verbosef("创建云盘文件夹成功: %s\n", panDirPath)
|
||||
|
||||
// save into DB
|
||||
if panDirFile, e := f.panClient.FileInfoById(f.syncItem.DriveId, panDirFileId); e == nil {
|
||||
panDirFile.Path = panDirPath
|
||||
fItem := NewPanFileItem(panDirFile)
|
||||
fItem.ScanTimeAt = utils.NowTimeStr()
|
||||
f.panFileDb.Add(fItem)
|
||||
if panFileId != "" {
|
||||
if strings.ToUpper(panFileSha1Str) == strings.ToUpper(sha1Str) {
|
||||
logger.Verbosef("检测到同名文件,文件内容完全一致,无需重复上传: %s\n", targetPanFilePath)
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return nil
|
||||
} else {
|
||||
// 删除云盘文件
|
||||
dp := &aliyunpan.FileBatchActionParam{
|
||||
DriveId: f.syncItem.DriveId,
|
||||
FileId: panFileId,
|
||||
}
|
||||
if _, e := f.panClient.OpenapiPanClient().FileDeleteCompletely(dp); e != nil {
|
||||
logger.Verbosef(" 删除云盘旧文件失败: %s\n", targetPanFilePath)
|
||||
return e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算proof code
|
||||
proofCode := ""
|
||||
localFileEntity, _ := os.Open(localFile.Path.RealPath)
|
||||
localFileInfo, _ := localFileEntity.Stat()
|
||||
proofCode = aliyunpan.CalcProofCode(f.panClient.GetAccessToken(), rio.NewFileReaderAtLen64(localFileEntity), localFileInfo.Size())
|
||||
|
||||
// 自动调整BlockSize大小
|
||||
newBlockSize := utils.ResizeUploadBlockSize(localFile.Length, f.syncItem.UploadBlockSize)
|
||||
if newBlockSize != f.syncItem.UploadBlockSize {
|
||||
@@ -510,15 +497,15 @@ func (f *FileActionTask) uploadFile(ctx context.Context) error {
|
||||
Name: filepath.Base(targetPanFilePath),
|
||||
Size: localFile.Length,
|
||||
ContentHash: sha1Str,
|
||||
ContentHashName: "sha1",
|
||||
CheckNameMode: "overwrite", // 覆盖云盘文件
|
||||
ContentHashName: contentHashName,
|
||||
CheckNameMode: "refuse",
|
||||
ParentFileId: panDirFileId,
|
||||
BlockSize: f.syncItem.UploadBlockSize,
|
||||
ProofCode: proofCode,
|
||||
ProofVersion: "v1",
|
||||
}
|
||||
if uploadOpEntity, err := f.panClient.CreateUploadFile(appCreateUploadFileParam); err != nil {
|
||||
logger.Verbosef("创建云盘上传任务失败: %s\n", panDirPath)
|
||||
if uploadOpEntity, err := f.panClient.OpenapiPanClient().CreateUploadFile(appCreateUploadFileParam); err != nil {
|
||||
logger.Verbosef("创建云盘上传任务失败: %s\n", targetPanFilePath)
|
||||
return err
|
||||
} else {
|
||||
f.syncItem.UploadEntity = uploadOpEntity
|
||||
@@ -532,15 +519,13 @@ func (f *FileActionTask) uploadFile(ctx context.Context) error {
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
PromptPrintln("上传完毕:" + f.syncItem.getPanFileFullPath())
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// 检测链接是否过期
|
||||
// check url expired or not
|
||||
uploadUrl := f.syncItem.UploadEntity.PartInfoList[f.syncItem.UploadPartSeq].UploadURL
|
||||
if f.syncItem.UseInternalUrl {
|
||||
uploadUrl = f.syncItem.UploadEntity.PartInfoList[f.syncItem.UploadPartSeq].InternalUploadURL
|
||||
}
|
||||
if panupload.IsUrlExpired(uploadUrl) {
|
||||
// get renew upload url
|
||||
logger.Verbosef("链接过期,获取新的上传链接: %s\n", targetPanFilePath)
|
||||
@@ -556,7 +541,7 @@ func (f *FileActionTask) uploadFile(ctx context.Context) error {
|
||||
PartInfoList: infoList,
|
||||
UploadId: f.syncItem.UploadEntity.UploadId,
|
||||
}
|
||||
newUploadInfo, err1 := f.panClient.GetUploadUrl(refreshUploadParam)
|
||||
newUploadInfo, err1 := f.panClient.OpenapiPanClient().GetUploadUrl(refreshUploadParam)
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
@@ -568,19 +553,9 @@ func (f *FileActionTask) uploadFile(ctx context.Context) error {
|
||||
// 创建分片上传器
|
||||
// 阿里云盘默认就是分片上传,每一个分片对应一个part_info
|
||||
// 但是不支持分片同时上传,必须单线程,并且按照顺序从1开始一个一个上传
|
||||
worker := panupload.NewPanUpload(f.panClient, f.syncItem.getPanFileFullPath(), f.syncItem.DriveId, f.syncItem.UploadEntity, f.syncItem.UseInternalUrl)
|
||||
|
||||
// 限速配置
|
||||
var rateLimit *speeds.RateLimit
|
||||
if f.maxUploadRate > 0 {
|
||||
rateLimit = speeds.NewRateLimit(f.maxUploadRate)
|
||||
}
|
||||
|
||||
// 上传客户端
|
||||
uploadClient := requester.NewHTTPClient()
|
||||
uploadClient.SetTimeout(0)
|
||||
uploadClient.SetKeepAlive(true)
|
||||
worker := panupload.NewPanUpload(f.panClient, f.syncItem.getPanFileFullPath(), f.syncItem.DriveId, f.syncItem.UploadEntity)
|
||||
|
||||
// 初始化上传Range
|
||||
if f.syncItem.UploadRange == nil {
|
||||
f.syncItem.UploadRange = &transfer.Range{
|
||||
Begin: 0,
|
||||
@@ -588,19 +563,66 @@ func (f *FileActionTask) uploadFile(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// 限速配置
|
||||
var rateLimit *speeds.RateLimit
|
||||
if f.maxUploadRate > 0 {
|
||||
rateLimit = speeds.NewRateLimit(f.maxUploadRate)
|
||||
}
|
||||
// 速度指示器
|
||||
speedsStat := &speeds.Speeds{}
|
||||
// 进度指示器
|
||||
status := &uploader.UploadStatus{}
|
||||
status.SetTotalSize(f.syncItem.LocalFile.FileSize)
|
||||
completed := make(chan struct{}, 0)
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-completed:
|
||||
return
|
||||
case <-ticker.C:
|
||||
status.SetUploaded(f.syncItem.UploadRange.Begin)
|
||||
time.Sleep(time.Duration(rand.Intn(10)*33) * time.Millisecond) // 延迟随机时间
|
||||
builder := &strings.Builder{}
|
||||
uploadedPercentage := fmt.Sprintf("%.2f%%", float64(status.Uploaded())/float64(status.TotalSize())*100)
|
||||
fmt.Fprintf(builder, "\r上传到网盘:%s ↑ %s/%s(%s) %s/s............",
|
||||
f.syncItem.getPanFileFullPath(),
|
||||
converter.ConvertFileSize(status.Uploaded(), 2),
|
||||
converter.ConvertFileSize(status.TotalSize(), 2),
|
||||
uploadedPercentage,
|
||||
converter.ConvertFileSize(speedsStat.GetSpeeds(), 2),
|
||||
)
|
||||
PromptPrint(builder.String())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 上传客户端
|
||||
uploadClient := requester.NewHTTPClient()
|
||||
uploadClient.SetTimeout(0)
|
||||
uploadClient.SetKeepAlive(true)
|
||||
|
||||
// 标记上传状态
|
||||
f.syncItem.Status = SyncFileStatusUploading
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
|
||||
worker.Precreate()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// cancel routine & done
|
||||
logger.Verboseln("file upload routine done")
|
||||
return nil
|
||||
logger.Verboseln("file upload routine cancel")
|
||||
close(completed)
|
||||
return errors.New("file upload routine cancel")
|
||||
default:
|
||||
logger.Verboseln("do file upload process")
|
||||
if f.syncItem.UploadRange.End > f.syncItem.LocalFile.FileSize {
|
||||
f.syncItem.UploadRange.End = f.syncItem.LocalFile.FileSize
|
||||
}
|
||||
fileReader := uploader.NewBufioSplitUnit(rio.NewFileReaderAtLen64(localFile.GetFile()), *f.syncItem.UploadRange, nil, rateLimit, nil)
|
||||
fileReader := uploader.NewBufioSplitUnit(rio.NewFileReaderAtLen64(localFile.GetFile()), *f.syncItem.UploadRange, speedsStat, rateLimit, nil)
|
||||
|
||||
if uploadDone, terr := worker.UploadFile(ctx, f.syncItem.UploadPartSeq, f.syncItem.UploadRange.Begin, f.syncItem.UploadRange.End, fileReader, uploadClient); terr == nil {
|
||||
if uploadDone {
|
||||
@@ -613,6 +635,8 @@ func (f *FileActionTask) uploadFile(ctx context.Context) error {
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
close(completed)
|
||||
PromptPrintln("上传完毕:" + f.syncItem.getPanFileFullPath())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -635,132 +659,6 @@ func (f *FileActionTask) uploadFile(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// deleteLocalFile 删除本地文件
|
||||
func (f *FileActionTask) deleteLocalFile(ctx context.Context) error {
|
||||
isFolder := f.syncItem.PanFile.IsFolder()
|
||||
localFilePath := f.syncItem.getLocalFileFullPath()
|
||||
if b, e := utils.PathExists(localFilePath); e == nil {
|
||||
if !b {
|
||||
// 本地文件已经不存在
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
var e error
|
||||
if isFolder {
|
||||
e = os.RemoveAll(localFilePath)
|
||||
} else {
|
||||
e = os.Remove(localFilePath)
|
||||
}
|
||||
if e == nil {
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
} else {
|
||||
f.syncItem.Status = SyncFileStatusFailed
|
||||
}
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return e
|
||||
}
|
||||
|
||||
// deletePanFile 删除云盘文件
|
||||
func (f *FileActionTask) deletePanFile(ctx context.Context) error {
|
||||
panFilePath := f.syncItem.getPanFileFullPath()
|
||||
|
||||
driveId := f.syncItem.DriveId
|
||||
panFileId := ""
|
||||
if f.syncItem.PanFile != nil {
|
||||
panFileId = f.syncItem.PanFile.FileId
|
||||
} else {
|
||||
fi, er := f.panClient.FileInfoByPath(f.syncItem.DriveId, panFilePath)
|
||||
time.Sleep(1 * time.Second)
|
||||
if er != nil {
|
||||
if er.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
// 云盘文件已经不存在
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return nil
|
||||
} else {
|
||||
// error
|
||||
return er
|
||||
}
|
||||
}
|
||||
panFileId = fi.FileId
|
||||
}
|
||||
|
||||
// 删除
|
||||
var fileDeleteResult []*aliyunpan.FileBatchActionResult
|
||||
var err *apierror.ApiError = nil
|
||||
fileDeleteResult, err = f.panClient.FileDelete([]*aliyunpan.FileBatchActionParam{{DriveId: driveId, FileId: panFileId}})
|
||||
time.Sleep(1 * time.Second)
|
||||
if err != nil || len(fileDeleteResult) == 0 {
|
||||
f.syncItem.Status = SyncFileStatusFailed
|
||||
} else {
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
}
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *FileActionTask) createLocalFolder(ctx context.Context) error {
|
||||
localFilePath := f.syncItem.getLocalFileFullPath()
|
||||
if b, e := utils.PathExists(localFilePath); e == nil && b {
|
||||
// 本地文件夹已经存在
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建
|
||||
var er error
|
||||
if b, e := utils.PathExists(localFilePath); e == nil && !b {
|
||||
f.localFolderCreateMutex.Lock()
|
||||
er = os.MkdirAll(localFilePath, 0755)
|
||||
f.localFolderCreateMutex.Unlock()
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
if er == nil {
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
} else {
|
||||
f.syncItem.Status = SyncFileStatusFailed
|
||||
}
|
||||
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return er
|
||||
}
|
||||
|
||||
func (f *FileActionTask) createPanFolder(ctx context.Context) error {
|
||||
panDirPath := f.syncItem.getPanFileFullPath()
|
||||
// 创建文件夹
|
||||
logger.Verbosef("创建云盘文件夹: %s\n", panDirPath)
|
||||
f.panFolderCreateMutex.Lock()
|
||||
_, apierr1 := f.panClient.Mkdir(f.syncItem.DriveId, "root", panDirPath)
|
||||
f.panFolderCreateMutex.Unlock()
|
||||
if apierr1 == nil {
|
||||
logger.Verbosef("创建云盘文件夹成功: %s\n", panDirPath)
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return nil
|
||||
} else {
|
||||
f.syncItem.Status = SyncFileStatusFailed
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return apierr1
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileActionTask) appendRecord(item *log.FileRecordItem) error {
|
||||
if item == nil {
|
||||
return nil
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
package syncdrive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFileActionMgrStart(t *testing.T) {
|
||||
refreshToken := "1640cc2d4ea...cb4d6242161a7"
|
||||
webToken, err := aliyunpan.GetAccessTokenFromRefreshToken(refreshToken)
|
||||
if err != nil {
|
||||
fmt.Println("get acccess token error")
|
||||
return
|
||||
}
|
||||
|
||||
// pan client
|
||||
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{}, aliyunpan.AppConfig{
|
||||
AppId: "25dzX3vbYqktVxyX",
|
||||
DeviceId: "E75459EXhOTkI5ZI6S3qDHA3",
|
||||
UserId: "",
|
||||
Nonce: 0,
|
||||
PublicKey: "",
|
||||
}, aliyunpan.SessionConfig{
|
||||
DeviceName: "Chrome浏览器",
|
||||
ModelName: "Windows网页版",
|
||||
})
|
||||
user, _ := panClient.GetUserInfo()
|
||||
task := &SyncTask{
|
||||
Id: "5b2d7c10-e927-4e72-8f9d-5abb3bb04814",
|
||||
DriveId: user.FileDriveId,
|
||||
LocalFolderPath: "D:\\smb\\feny\\goprojects\\dev\\NS游戏备份",
|
||||
PanFolderPath: "/sync_drive",
|
||||
Mode: "sync",
|
||||
LastSyncTime: "2022-05-26 19:29:19",
|
||||
|
||||
syncDbFolderPath: "D:\\smb\\feny\\goprojects\\dev\\sync_drive",
|
||||
panClient: panClient,
|
||||
syncOption: SyncOption{
|
||||
FileDownloadParallel: 2,
|
||||
FileUploadParallel: 2,
|
||||
FileDownloadBlockSize: int64(256 * 1024),
|
||||
FileUploadBlockSize: aliyunpan.DefaultChunkSize,
|
||||
UseInternalUrl: false,
|
||||
MaxDownloadRate: 0,
|
||||
MaxUploadRate: 0,
|
||||
},
|
||||
}
|
||||
task.setupDb()
|
||||
|
||||
ft := NewFileActionTaskManager(task)
|
||||
ft.Start()
|
||||
|
||||
//go func() {
|
||||
// time.Sleep(10 * time.Second)
|
||||
// task.Stop()
|
||||
//}()
|
||||
time.Sleep(50 * time.Minute)
|
||||
ft.Stop()
|
||||
}
|
||||
|
||||
func TestFileTime(t *testing.T) {
|
||||
ts := utils.ParseTimeStr("2013-01-11 12:15:07")
|
||||
//ts = time.Now()
|
||||
if err := os.Chtimes("/Volumes/Downloads/dev/upload/password.key", ts, ts); err != nil {
|
||||
logger.Verbosef(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalFileTime(t *testing.T) {
|
||||
if file, er := os.Stat("/Volumes/Downloads/dev/upload/password.key"); er == nil {
|
||||
fmt.Println(file.ModTime().Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,6 @@ type (
|
||||
StatusUpdateTime string `json:"statusUpdateTime"`
|
||||
|
||||
DriveId string `json:"driveId"`
|
||||
UseInternalUrl bool `json:"useInternalUrl"`
|
||||
DownloadRange *transfer.Range `json:"downloadRange"`
|
||||
DownloadBlockSize int64 `json:"downloadBlockSize"`
|
||||
UploadRange *transfer.Range `json:"uploadRange"`
|
||||
@@ -349,7 +348,8 @@ func (item *SyncFileItem) getPanFileFullPath() string {
|
||||
localRootPath := strings.ReplaceAll(item.LocalFolderPath, "\\", "/")
|
||||
|
||||
relativePath := strings.TrimPrefix(localPath, localRootPath)
|
||||
return path.Join(path.Clean(item.PanFolderPath), relativePath)
|
||||
panPath := path.Join(path.Clean(item.PanFolderPath), relativePath)
|
||||
return strings.ReplaceAll(panPath, "\\", "/")
|
||||
}
|
||||
|
||||
// getLocalFullPath 获取本地文件的路径
|
||||
|
||||
@@ -2,64 +2,15 @@ package syncdrive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPanSyncDb(t *testing.T) {
|
||||
// get access token
|
||||
refreshToken := "d77420e4daa...9d384d7c44508"
|
||||
webToken, err := aliyunpan.GetAccessTokenFromRefreshToken(refreshToken)
|
||||
if err != nil {
|
||||
fmt.Println("get acccess token error")
|
||||
return
|
||||
}
|
||||
|
||||
// pan client
|
||||
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{}, aliyunpan.AppConfig{
|
||||
AppId: "25dzX3vbYqktVxyX",
|
||||
DeviceId: "E75459EXhOTkI5ZI6S3qDHA3",
|
||||
UserId: "",
|
||||
Nonce: 0,
|
||||
PublicKey: "",
|
||||
}, aliyunpan.SessionConfig{
|
||||
DeviceName: "Chrome浏览器",
|
||||
ModelName: "Windows网页版",
|
||||
})
|
||||
|
||||
// get user info
|
||||
ui, err := panClient.GetUserInfo()
|
||||
if err != nil {
|
||||
fmt.Println("get user info error")
|
||||
return
|
||||
}
|
||||
fmt.Println("当前登录用户:" + ui.Nickname)
|
||||
|
||||
b := NewPanSyncDb("D:\\smb\\feny\\goprojects\\dev\\pan.db")
|
||||
b.Open()
|
||||
defer b.Close()
|
||||
// do some file operation
|
||||
panClient.FilesDirectoriesRecurseList(ui.FileDriveId, "/Parallels Desktop", func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
|
||||
if apiError != nil {
|
||||
logger.Verbosef("%s\n", apiError)
|
||||
return true
|
||||
}
|
||||
fmt.Println("add file:" + fd.String())
|
||||
b.Add(NewPanFileItem(fd))
|
||||
time.Sleep(2 * time.Second)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
b := NewPanSyncDb("D:\\smb\\feny\\goprojects\\dev\\pan.db")
|
||||
b.Open()
|
||||
|
||||
@@ -20,8 +20,9 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
TaskStep string
|
||||
SyncMode string
|
||||
SyncMode string
|
||||
SyncPolicy string
|
||||
CycleMode string
|
||||
|
||||
// SyncTask 同步任务
|
||||
SyncTask struct {
|
||||
@@ -31,16 +32,22 @@ type (
|
||||
Id string `json:"id"`
|
||||
// UserId 账号ID
|
||||
UserId string `json:"userId"`
|
||||
// DriveName 网盘名称,backup-备份盘,resource-资源盘
|
||||
DriveName string `json:"driveName"`
|
||||
// DriveId 网盘ID,目前支持文件网盘
|
||||
DriveId string `json:"-"`
|
||||
// LocalFolderPath 本地目录
|
||||
LocalFolderPath string `json:"localFolderPath"`
|
||||
// PanFolderPath 云盘目录
|
||||
PanFolderPath string `json:"panFolderPath"`
|
||||
// Mode 同步模式
|
||||
// Mode 备份模式
|
||||
Mode SyncMode `json:"mode"`
|
||||
// Policy 备份策略
|
||||
Policy SyncPolicy `json:"policy"`
|
||||
// CycleMode 循环模式,OneTime-运行一次,InfiniteLoop-无限循环模式
|
||||
CycleModeType CycleMode `json:"-"`
|
||||
// Priority 优先级选项
|
||||
Priority SyncPriorityOption `json:"priority"`
|
||||
Priority SyncPriorityOption `json:"-"`
|
||||
// LastSyncTime 上一次同步时间
|
||||
LastSyncTime string `json:"lastSyncTime"`
|
||||
|
||||
@@ -54,11 +61,13 @@ type (
|
||||
cancelFunc context.CancelFunc
|
||||
|
||||
panUser *config.PanUser
|
||||
panClient *aliyunpan.PanClient
|
||||
panClient *config.PanClient
|
||||
|
||||
syncOption SyncOption
|
||||
|
||||
fileActionTaskManager *FileActionTaskManager
|
||||
resourceMutex *sync.Mutex
|
||||
scanLoopIsDone bool // 本次扫描对比文件进程是否已经完成
|
||||
|
||||
plugin plugins.Plugin
|
||||
pluginMutex *sync.Mutex
|
||||
@@ -66,19 +75,22 @@ type (
|
||||
)
|
||||
|
||||
const (
|
||||
// UploadOnly 单向上传,即备份本地文件到云盘
|
||||
UploadOnly SyncMode = "upload"
|
||||
// DownloadOnly 只下载,即备份云盘文件到本地
|
||||
DownloadOnly SyncMode = "download"
|
||||
// Upload 上传,即备份本地文件到云盘
|
||||
Upload SyncMode = "upload"
|
||||
// Download 下载,即备份云盘文件到本地
|
||||
Download SyncMode = "download"
|
||||
// SyncTwoWay 双向同步,本地和云盘文件完全保持一致
|
||||
SyncTwoWay SyncMode = "sync"
|
||||
|
||||
// StepScanFile 任务步骤,扫描文件建立同步数据库
|
||||
StepScanFile TaskStep = "scan"
|
||||
// StepDiffFile 任务步骤,对比文件
|
||||
StepDiffFile TaskStep = "diff"
|
||||
// StepSyncFile 任务步骤,同步文件
|
||||
StepSyncFile TaskStep = "sync"
|
||||
// SyncPolicyExclusive 备份策略,排他备份,保证本地和云盘一比一备份,目标目录多余的文件会被删除
|
||||
SyncPolicyExclusive SyncPolicy = "exclusive"
|
||||
// SyncPolicyIncrement 备份策略,增量备份,只会增量备份文件,目标目录多余的(旧的)文件不会被删除
|
||||
SyncPolicyIncrement SyncPolicy = "increment"
|
||||
|
||||
// CycleOneTime 只运行一次
|
||||
CycleOneTime CycleMode = "OneTime"
|
||||
// CycleInfiniteLoop 无限循环模式
|
||||
CycleInfiniteLoop CycleMode = "InfiniteLoop"
|
||||
)
|
||||
|
||||
func (t *SyncTask) NameLabel() string {
|
||||
@@ -89,13 +101,30 @@ func (t *SyncTask) String() string {
|
||||
builder := &strings.Builder{}
|
||||
builder.WriteString("任务: " + t.NameLabel() + "\n")
|
||||
mode := "双向备份"
|
||||
if t.Mode == UploadOnly {
|
||||
mode = "备份本地文件(只上传)"
|
||||
if t.Mode == Upload {
|
||||
mode = "备份本地文件(上传)"
|
||||
}
|
||||
if t.Mode == DownloadOnly {
|
||||
mode = "备份云盘文件(只下载)"
|
||||
if t.Mode == Download {
|
||||
mode = "备份云盘文件(下载)"
|
||||
}
|
||||
builder.WriteString("同步模式: " + mode + "\n")
|
||||
|
||||
policy := ""
|
||||
if t.Policy == SyncPolicyExclusive {
|
||||
if t.Mode == Upload {
|
||||
policy = "排他备份(上传&删除)"
|
||||
} else if t.Mode == Download {
|
||||
policy = "排他备份(下载&删除)"
|
||||
}
|
||||
}
|
||||
if t.Policy == SyncPolicyIncrement {
|
||||
if t.Mode == Upload {
|
||||
policy = "增量备份(只上传)"
|
||||
} else if t.Mode == Download {
|
||||
policy = "增量备份(只下载)"
|
||||
}
|
||||
}
|
||||
|
||||
if t.Mode == SyncTwoWay {
|
||||
priority := "时间优先"
|
||||
if t.syncOption.SyncPriority == SyncPriorityLocalFirst {
|
||||
@@ -105,10 +134,22 @@ func (t *SyncTask) String() string {
|
||||
} else {
|
||||
priority = "时间优先"
|
||||
}
|
||||
builder.WriteString("优先选项: " + priority + "\n")
|
||||
builder.WriteString("同步策略: " + priority + "\n")
|
||||
} else {
|
||||
builder.WriteString("同步策略: " + policy + "\n")
|
||||
}
|
||||
cycleModeStr := "永久循环"
|
||||
if t.CycleModeType == CycleOneTime {
|
||||
cycleModeStr = "运行一次"
|
||||
}
|
||||
builder.WriteString("运行周期: " + cycleModeStr + "\n")
|
||||
builder.WriteString("本地目录: " + t.LocalFolderPath + "\n")
|
||||
builder.WriteString("云盘目录: " + t.PanFolderPath + "\n")
|
||||
driveName := "备份盘"
|
||||
if strings.ToLower(t.DriveName) == "resource" {
|
||||
driveName = "资源盘"
|
||||
}
|
||||
builder.WriteString("目标网盘: " + driveName + "\n")
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
@@ -130,7 +171,7 @@ func (t *SyncTask) setupDb() error {
|
||||
|
||||
// Start 启动同步任务
|
||||
// 扫描本地和云盘文件信息并存储到本地数据库
|
||||
func (t *SyncTask) Start(step TaskStep) error {
|
||||
func (t *SyncTask) Start() error {
|
||||
if t.ctx != nil {
|
||||
return fmt.Errorf("task have starting")
|
||||
}
|
||||
@@ -157,18 +198,20 @@ func (t *SyncTask) Start(step TaskStep) error {
|
||||
os.MkdirAll(t.LocalFolderPath, 0755)
|
||||
}
|
||||
}
|
||||
if _, er := t.panClient.FileInfoByPath(t.DriveId, t.PanFolderPath); er != nil {
|
||||
if _, er := t.panClient.OpenapiPanClient().FileInfoByPath(t.DriveId, t.PanFolderPath); er != nil {
|
||||
if er.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
t.panClient.MkdirByFullPath(t.DriveId, t.PanFolderPath)
|
||||
t.panClient.OpenapiPanClient().MkdirByFullPath(t.DriveId, t.PanFolderPath)
|
||||
}
|
||||
}
|
||||
|
||||
// setup sync db file
|
||||
t.setupDb()
|
||||
|
||||
if t.fileActionTaskManager == nil {
|
||||
t.fileActionTaskManager = NewFileActionTaskManager(t)
|
||||
}
|
||||
if t.resourceMutex == nil {
|
||||
t.resourceMutex = &sync.Mutex{}
|
||||
}
|
||||
|
||||
if t.plugin == nil {
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
@@ -184,19 +227,28 @@ func (t *SyncTask) Start(step TaskStep) error {
|
||||
t.ctx, cancel = context.WithCancel(context.Background())
|
||||
t.cancelFunc = cancel
|
||||
|
||||
go t.scanLocalFile(t.ctx, step == StepScanFile)
|
||||
go t.scanPanFile(t.ctx, step == StepScanFile)
|
||||
|
||||
// 如果只扫描文件建立同步数据库,则无需进行文件对比这一步
|
||||
if step != StepScanFile {
|
||||
// start file sync manager
|
||||
if e := t.fileActionTaskManager.Start(); e != nil {
|
||||
return e
|
||||
}
|
||||
} else {
|
||||
// delay
|
||||
time.Sleep(1 * time.Second)
|
||||
// 初始化文件执行进程
|
||||
if e := t.fileActionTaskManager.InitMgr(); e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
// 策略
|
||||
if t.Policy == "" {
|
||||
t.Policy = SyncPolicyIncrement
|
||||
}
|
||||
|
||||
// 启动文件扫描进程
|
||||
t.SetScanLoopFlag(false)
|
||||
if t.Mode == Upload {
|
||||
go t.scanLocalFile(t.ctx)
|
||||
} else if t.Mode == Download {
|
||||
go t.scanPanFile(t.ctx)
|
||||
} else if t.Mode == SyncTwoWay {
|
||||
return fmt.Errorf("异常:暂不支持该模式。")
|
||||
} else {
|
||||
return fmt.Errorf("异常:暂不支持该模式。")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -242,6 +294,26 @@ func (t *SyncTask) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsScanLoopDone 获取文件扫描进程状态
|
||||
func (t *SyncTask) IsScanLoopDone() bool {
|
||||
t.resourceMutex.Lock()
|
||||
defer t.resourceMutex.Unlock()
|
||||
return t.scanLoopIsDone
|
||||
}
|
||||
|
||||
// SetScanLoopFlag 设置文件扫描进程状态标记
|
||||
func (t *SyncTask) SetScanLoopFlag(done bool) {
|
||||
t.resourceMutex.Lock()
|
||||
defer t.resourceMutex.Unlock()
|
||||
t.scanLoopIsDone = done
|
||||
}
|
||||
|
||||
// IsTaskCompletely 任务是否已经完成
|
||||
func (t *SyncTask) IsTaskCompletely() bool {
|
||||
// 扫描完成+执行完成+一次运行模式
|
||||
return t.IsScanLoopDone() && t.fileActionTaskManager.IsExecuteLoopIsDone() && t.CycleModeType == CycleOneTime
|
||||
}
|
||||
|
||||
// panSyncDbFullPath 云盘文件数据库
|
||||
func (t *SyncTask) panSyncDbFullPath() string {
|
||||
dir := path.Join(t.syncDbFolderPath, t.Id)
|
||||
@@ -297,7 +369,7 @@ func (t *SyncTask) discardLocalFileDb(filePath string, startTimeUnix int64) bool
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.ScanTimeAt == "" || file.ScanTimeUnix() < startTimeUnix {
|
||||
if t.Mode == DownloadOnly {
|
||||
if t.Mode == Download {
|
||||
// delete discard local file info directly
|
||||
t.localFileDb.Delete(file.Path)
|
||||
logger.Verboseln("label discard local file from DB: ", utils.ObjectToJsonStr(file, false))
|
||||
@@ -341,8 +413,8 @@ func (t *SyncTask) skipLocalFile(file *LocalFileItem) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// scanLocalFile 本地文件循环扫描进程
|
||||
func (t *SyncTask) scanLocalFile(ctx context.Context, scanFileOnly bool) {
|
||||
// scanLocalFile 本地文件扫描进程。上传备份模式是以本地文件为扫描对象,并对比云盘端对应目录文件,以决定是否需要上传新文件到云盘
|
||||
func (t *SyncTask) scanLocalFile(ctx context.Context) {
|
||||
t.wg.AddDelta()
|
||||
defer t.wg.Done()
|
||||
|
||||
@@ -372,6 +444,7 @@ func (t *SyncTask) scanLocalFile(ctx context.Context, scanFileOnly bool) {
|
||||
t.localFileDb.Add(newLocalFileItem(fi, fullPath))
|
||||
}
|
||||
|
||||
// 文件夹队列
|
||||
folderQueue := collection.NewFifoQueue()
|
||||
rootFolder, err := os.Stat(t.LocalFolderPath)
|
||||
if err != nil {
|
||||
@@ -381,18 +454,13 @@ func (t *SyncTask) scanLocalFile(ctx context.Context, scanFileOnly bool) {
|
||||
fileInfo: rootFolder,
|
||||
path: t.LocalFolderPath,
|
||||
})
|
||||
startTimeOfThisLoop := time.Now().Unix()
|
||||
delayTimeCount := int64(0)
|
||||
isLocalFolderModify := false
|
||||
|
||||
// 触发一次文件对比任务
|
||||
t.fileActionTaskManager.AddLocalFolderModifyCount()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// cancel routine & done
|
||||
logger.Verboseln("local file routine done")
|
||||
logger.Verboseln("local file routine done, exit loop")
|
||||
return
|
||||
default:
|
||||
// 采用广度优先遍历(BFS)进行文件遍历
|
||||
@@ -401,49 +469,35 @@ func (t *SyncTask) scanLocalFile(ctx context.Context, scanFileOnly bool) {
|
||||
delayTimeCount -= 1
|
||||
continue
|
||||
} else if delayTimeCount == 0 {
|
||||
delayTimeCount -= 1
|
||||
startTimeOfThisLoop = time.Now().Unix()
|
||||
logger.Verboseln("do scan local file process at ", utils.NowTimeStr())
|
||||
}
|
||||
|
||||
// check local sync dir
|
||||
if t.Mode == SyncTwoWay {
|
||||
// check local disk unplug issue
|
||||
if b, e := utils.PathExists(t.LocalFolderPath); e == nil {
|
||||
if !b {
|
||||
// maybe the local disk unplug, check
|
||||
fmt.Println("异常:本地同步目录不存在,本任务已经停止。如需继续同步请手动删除同步数据库再重试。")
|
||||
return
|
||||
}
|
||||
// 确认文件执行进程是否已完成
|
||||
if !t.fileActionTaskManager.IsExecuteLoopIsDone() {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue // 需要等待文件上传进程完成才能开启新一轮扫描
|
||||
}
|
||||
delayTimeCount -= 1
|
||||
logger.Verboseln("start scan local file process at ", utils.NowTimeStr())
|
||||
t.SetScanLoopFlag(false)
|
||||
t.fileActionTaskManager.StartFileActionTaskExecutor()
|
||||
PromptPrintln("开始进行文件扫描...")
|
||||
}
|
||||
|
||||
obj := folderQueue.Pop()
|
||||
if obj == nil {
|
||||
// label discard file from DB
|
||||
if t.discardLocalFileDb(t.LocalFolderPath, startTimeOfThisLoop) {
|
||||
logger.Verboseln("notify local folder modify, need to do file action task")
|
||||
t.fileActionTaskManager.AddLocalFolderModifyCount()
|
||||
t.fileActionTaskManager.AddPanFolderModifyCount()
|
||||
isLocalFolderModify = false // 重置标记
|
||||
}
|
||||
// 没有其他文件夹需要扫描了,已完成了一次全量文件夹的扫描了
|
||||
t.SetScanLoopFlag(true)
|
||||
|
||||
if scanFileOnly {
|
||||
// 全盘扫描文件一次,建立好文件同步数据库,然后退出
|
||||
if t.CycleModeType == CycleOneTime {
|
||||
// 只运行一次,全盘扫描一次后退出任务循环
|
||||
logger.Verboseln("file scan task is finish, exit normally")
|
||||
return
|
||||
}
|
||||
|
||||
// restart scan loop over again
|
||||
// 无限循环模式,继续下一次扫描
|
||||
folderQueue.Push(&folderItem{
|
||||
fileInfo: rootFolder,
|
||||
path: t.LocalFolderPath,
|
||||
})
|
||||
delayTimeCount = TimeSecondsOf30Seconds
|
||||
if isLocalFolderModify {
|
||||
logger.Verboseln("notify local folder modify, need to do file action task")
|
||||
t.fileActionTaskManager.AddLocalFolderModifyCount()
|
||||
isLocalFolderModify = false // 重置标记
|
||||
}
|
||||
delayTimeCount = TimeSecondsOfOneMinute
|
||||
continue
|
||||
}
|
||||
item := obj.(*folderItem)
|
||||
@@ -454,65 +508,93 @@ func (t *SyncTask) scanLocalFile(ctx context.Context, scanFileOnly bool) {
|
||||
if len(files) == 0 {
|
||||
continue
|
||||
}
|
||||
localFileScanList := LocalFileList{}
|
||||
localFileAppendList := LocalFileList{}
|
||||
for _, file := range files {
|
||||
for _, file := range files { // 逐个确认目录下面的每个文件的情况
|
||||
if strings.HasSuffix(file.Name(), DownloadingFileSuffix) {
|
||||
// 下载中文件,跳过
|
||||
// 下载中的文件,跳过
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查JS插件
|
||||
localFile := newLocalFileItem(file, item.path+"/"+file.Name())
|
||||
if t.skipLocalFile(localFile) {
|
||||
fmt.Println("插件禁止扫描本地文件: ", localFile.Path)
|
||||
PromptPrintln("插件禁止扫描本地文件: " + localFile.Path)
|
||||
continue
|
||||
}
|
||||
|
||||
if scanFileOnly {
|
||||
fmt.Println("扫描本地文件:" + item.path + "/" + file.Name())
|
||||
// 跳过软链接文件
|
||||
if IsSymlinkFile(file) {
|
||||
logger.Verboseln("软链接文件,跳过:" + item.path + "/" + file.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
localFileInDb, _ := t.localFileDb.Get(localFile.Path)
|
||||
if localFileInDb == nil {
|
||||
// append
|
||||
localFile.ScanTimeAt = utils.NowTimeStr()
|
||||
localFileAppendList = append(localFileAppendList, localFile)
|
||||
logger.Verboseln("add local file to db: ", utils.ObjectToJsonStr(localFile, false))
|
||||
isLocalFolderModify = true
|
||||
} else {
|
||||
// update newest info into DB
|
||||
if localFile.UpdateTimeUnix() > localFileInDb.UpdateTimeUnix() || localFile.FileSize != localFileInDb.FileSize {
|
||||
localFileInDb.Sha1Hash = ""
|
||||
isLocalFolderModify = true
|
||||
}
|
||||
|
||||
localFileInDb.UpdatedAt = localFile.UpdatedAt
|
||||
localFileInDb.CreatedAt = localFile.CreatedAt
|
||||
localFileInDb.FileSize = localFile.FileSize
|
||||
localFileInDb.FileType = localFile.FileType
|
||||
localFileInDb.ScanTimeAt = utils.NowTimeStr()
|
||||
localFileInDb.ScanStatus = ScanStatusNormal
|
||||
logger.Verboseln("update local file to db: ", utils.ObjectToJsonStr(localFileInDb, false))
|
||||
if _, er := t.localFileDb.Update(localFileInDb); er != nil {
|
||||
logger.Verboseln("local db update error ", er)
|
||||
}
|
||||
}
|
||||
|
||||
// for next term scan
|
||||
PromptPrintln("扫描到本地文件:" + item.path + "/" + file.Name())
|
||||
// 文件夹需要增加到扫描队列
|
||||
if file.IsDir() {
|
||||
folderQueue.Push(&folderItem{
|
||||
fileInfo: file,
|
||||
path: item.path + "/" + file.Name(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 查询本地扫描数据库
|
||||
localFileInDb, _ := t.localFileDb.Get(localFile.Path)
|
||||
if localFileInDb == nil {
|
||||
// 记录不存在,直接增加到本地数据库队列
|
||||
localFileAppendList = append(localFileAppendList, localFile)
|
||||
} else {
|
||||
// 记录存在,查看文件SHA1是否更改
|
||||
if localFile.UpdateTimeUnix() == localFileInDb.UpdateTimeUnix() && localFile.FileSize == localFileInDb.FileSize {
|
||||
// 文件大小没变,文件修改时间没变,假定文件内容也没变
|
||||
localFile.Sha1Hash = localFileInDb.Sha1Hash
|
||||
} else {
|
||||
// 文件已修改,更新文件信息到扫描数据库
|
||||
localFileInDb.Sha1Hash = localFile.Sha1Hash
|
||||
localFileInDb.UpdatedAt = localFile.UpdatedAt
|
||||
localFileInDb.CreatedAt = localFile.CreatedAt
|
||||
localFileInDb.FileSize = localFile.FileSize
|
||||
localFileInDb.FileType = localFile.FileType
|
||||
localFileInDb.ScanTimeAt = utils.NowTimeStr()
|
||||
localFileInDb.ScanStatus = ScanStatusNormal
|
||||
logger.Verboseln("update local file to db: ", utils.ObjectToJsonStr(localFileInDb, false))
|
||||
if _, er := t.localFileDb.Update(localFileInDb); er != nil {
|
||||
logger.Verboseln("local db update error ", er)
|
||||
}
|
||||
}
|
||||
}
|
||||
localFileScanList = append(localFileScanList, localFile)
|
||||
}
|
||||
if len(localFileAppendList) > 0 {
|
||||
//fmt.Println(utils.ObjectToJsonStr(localFileAppendList))
|
||||
if _, er := t.localFileDb.AddFileList(localFileAppendList); er != nil {
|
||||
logger.Verboseln("add files to local file db error {}", er)
|
||||
logger.Verboseln("add new files to local file db error {}", er)
|
||||
}
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// 获取云盘对应目录下的文件清单
|
||||
panFileInfo, er := t.panClient.OpenapiPanClient().FileInfoByPath(t.DriveId, GetPanFileFullPathFromLocalPath(item.path, t.LocalFolderPath, t.PanFolderPath))
|
||||
if er != nil {
|
||||
logger.Verboseln("query pan file info error: ", er)
|
||||
// do nothing
|
||||
continue
|
||||
}
|
||||
panFileList, er2 := t.panClient.OpenapiPanClient().FileListGetAll(&aliyunpan.FileListParam{
|
||||
DriveId: t.DriveId,
|
||||
ParentFileId: panFileInfo.FileId,
|
||||
}, 1500) // 延迟时间避免触发风控
|
||||
if er2 != nil {
|
||||
logger.Verboseln("query pan file list error: ", er)
|
||||
continue
|
||||
}
|
||||
panFileScanList := PanFileList{}
|
||||
for _, pf := range panFileList {
|
||||
pf.Path = path.Join(GetPanFileFullPathFromLocalPath(item.path, t.LocalFolderPath, t.PanFolderPath), pf.FileName)
|
||||
panFileScanList = append(panFileScanList, NewPanFileItem(pf))
|
||||
}
|
||||
|
||||
// 对比文件
|
||||
t.fileActionTaskManager.doFileDiffRoutine(localFileScanList, panFileScanList)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -526,7 +608,7 @@ func (t *SyncTask) discardPanFileDb(filePath string, startTimeUnix int64) bool {
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.ScanTimeUnix() < startTimeUnix {
|
||||
if t.Mode == UploadOnly {
|
||||
if t.Mode == Upload {
|
||||
// delete discard pan file info directly
|
||||
t.panFileDb.Delete(file.Path)
|
||||
logger.Verboseln("delete discard pan file from DB: ", utils.ObjectToJsonStr(file, false))
|
||||
@@ -571,8 +653,8 @@ func (t *SyncTask) skipPanFile(file *PanFileItem) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// scanPanFile 云盘文件循环扫描进程
|
||||
func (t *SyncTask) scanPanFile(ctx context.Context, scanFileOnly bool) {
|
||||
// scanPanFile 云盘文件循环扫描进程。下载备份模式是以云盘文件为扫描对象,并对比本地对应目录文件,以决定是否需要下载新文件到本地
|
||||
func (t *SyncTask) scanPanFile(ctx context.Context) {
|
||||
t.wg.AddDelta()
|
||||
defer t.wg.Done()
|
||||
|
||||
@@ -585,7 +667,7 @@ func (t *SyncTask) scanPanFile(ctx context.Context, scanFileOnly bool) {
|
||||
}
|
||||
fullPath += "/" + p
|
||||
}
|
||||
fi, err := t.panClient.FileInfoByPath(t.DriveId, fullPath)
|
||||
fi, err := t.panClient.OpenapiPanClient().FileInfoByPath(t.DriveId, fullPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -597,12 +679,7 @@ func (t *SyncTask) scanPanFile(ctx context.Context, scanFileOnly bool) {
|
||||
folderQueue := collection.NewFifoQueue()
|
||||
rootPanFile := fi
|
||||
folderQueue.Push(rootPanFile)
|
||||
startTimeOfThisLoop := time.Now().Unix()
|
||||
delayTimeCount := int64(0)
|
||||
isPanFolderModify := false
|
||||
|
||||
// 触发一次文件对比任务
|
||||
t.fileActionTaskManager.AddPanFolderModifyCount()
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -617,97 +694,112 @@ func (t *SyncTask) scanPanFile(ctx context.Context, scanFileOnly bool) {
|
||||
delayTimeCount -= 1
|
||||
continue
|
||||
} else if delayTimeCount == 0 {
|
||||
// 确认文件执行进程是否已完成
|
||||
if !t.fileActionTaskManager.IsExecuteLoopIsDone() {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue // 需要等待文件上传进程完成才能开启新一轮扫描
|
||||
}
|
||||
delayTimeCount -= 1
|
||||
startTimeOfThisLoop = time.Now().Unix()
|
||||
logger.Verboseln("do scan pan file process at ", utils.NowTimeStr())
|
||||
logger.Verboseln("start scan pan file process at ", utils.NowTimeStr())
|
||||
t.SetScanLoopFlag(false)
|
||||
t.fileActionTaskManager.StartFileActionTaskExecutor()
|
||||
PromptPrintln("开始进行文件扫描...")
|
||||
}
|
||||
obj := folderQueue.Pop()
|
||||
if obj == nil {
|
||||
// label discard file from DB
|
||||
if t.discardPanFileDb(t.PanFolderPath, startTimeOfThisLoop) {
|
||||
logger.Verboseln("notify pan folder modify, need to do file action task")
|
||||
t.fileActionTaskManager.AddPanFolderModifyCount()
|
||||
t.fileActionTaskManager.AddLocalFolderModifyCount()
|
||||
isPanFolderModify = false
|
||||
}
|
||||
// 没有其他文件夹需要扫描了,已完成了一次全量文件夹的扫描了
|
||||
t.SetScanLoopFlag(true)
|
||||
|
||||
if scanFileOnly {
|
||||
// 全盘扫描文件一次,建立好文件同步数据库,然后退出
|
||||
if t.CycleModeType == CycleOneTime {
|
||||
// 只运行一次,全盘扫描一次后退出任务循环
|
||||
logger.Verboseln("pan file scan task is finish, exit normally")
|
||||
return
|
||||
}
|
||||
|
||||
// restart scan loop over again
|
||||
// 无限循环模式,继续下一次扫描
|
||||
folderQueue.Push(rootPanFile)
|
||||
delayTimeCount = TimeSecondsOf2Minute
|
||||
if isPanFolderModify {
|
||||
logger.Verboseln("notify pan folder modify, need to do file action task")
|
||||
t.fileActionTaskManager.AddPanFolderModifyCount()
|
||||
isPanFolderModify = false
|
||||
}
|
||||
delayTimeCount = TimeSecondsOfOneMinute
|
||||
continue
|
||||
}
|
||||
item := obj.(*aliyunpan.FileEntity)
|
||||
files, err1 := t.panClient.FileListGetAll(&aliyunpan.FileListParam{
|
||||
files, err1 := t.panClient.OpenapiPanClient().FileListGetAll(&aliyunpan.FileListParam{
|
||||
DriveId: t.DriveId,
|
||||
ParentFileId: item.FileId,
|
||||
}, 1500) // 延迟时间避免触发风控
|
||||
if err1 != nil {
|
||||
// retry next term
|
||||
// 下一轮重试
|
||||
folderQueue.Push(item)
|
||||
time.Sleep(10 * time.Second)
|
||||
continue
|
||||
}
|
||||
panFileList := PanFileList{}
|
||||
panFileScanList := PanFileList{}
|
||||
for _, file := range files {
|
||||
file.Path = path.Join(item.Path, file.FileName)
|
||||
panFile := NewPanFileItem(file)
|
||||
|
||||
// 检查JS插件
|
||||
if t.skipPanFile(panFile) {
|
||||
logger.Verboseln("插件禁止扫描云盘文件: ", panFile.Path)
|
||||
PromptPrintln("插件禁止扫描云盘文件: " + panFile.Path)
|
||||
continue
|
||||
}
|
||||
if scanFileOnly {
|
||||
fmt.Println("扫描云盘文件:" + file.Path)
|
||||
}
|
||||
|
||||
panFileInDb, _ := t.panFileDb.Get(file.Path)
|
||||
if panFileInDb == nil {
|
||||
// append
|
||||
panFile.ScanTimeAt = utils.NowTimeStr()
|
||||
panFileList = append(panFileList, panFile)
|
||||
logger.Verboseln("add pan file to db: ", utils.ObjectToJsonStr(panFile, false))
|
||||
isPanFolderModify = true
|
||||
} else {
|
||||
// update newest info into DB
|
||||
if strings.ToLower(file.ContentHash) != strings.ToLower(panFileInDb.Sha1Hash) {
|
||||
isPanFolderModify = true
|
||||
|
||||
panFileInDb.DomainId = file.DomainId
|
||||
panFileInDb.FileId = file.FileId
|
||||
panFileInDb.FileType = file.FileType
|
||||
panFileInDb.Category = file.Category
|
||||
panFileInDb.Crc64Hash = file.Crc64Hash
|
||||
panFileInDb.Sha1Hash = file.ContentHash
|
||||
panFileInDb.FileSize = file.FileSize
|
||||
panFileInDb.UpdatedAt = file.UpdatedAt
|
||||
panFileInDb.CreatedAt = file.CreatedAt
|
||||
}
|
||||
// update scan time
|
||||
panFileInDb.ScanTimeAt = utils.NowTimeStr()
|
||||
panFileInDb.ScanStatus = ScanStatusNormal
|
||||
logger.Verboseln("update pan file to db: ", utils.ObjectToJsonStr(panFileInDb, false))
|
||||
t.panFileDb.Update(panFileInDb)
|
||||
}
|
||||
PromptPrintln("扫描到云盘文件:" + file.Path)
|
||||
panFile.ScanTimeAt = utils.NowTimeStr()
|
||||
panFileScanList = append(panFileScanList, panFile)
|
||||
logger.Verboseln("scan pan file: ", utils.ObjectToJsonStr(panFile, false))
|
||||
|
||||
if file.IsFolder() {
|
||||
folderQueue.Push(file)
|
||||
}
|
||||
}
|
||||
if len(panFileList) > 0 {
|
||||
if _, er := t.panFileDb.AddFileList(panFileList); er != nil {
|
||||
logger.Verboseln("add files to pan file db error {}", er)
|
||||
}
|
||||
if len(panFileScanList) == 0 {
|
||||
// empty dir
|
||||
continue
|
||||
}
|
||||
time.Sleep(5 * time.Second) // 延迟避免触发风控
|
||||
|
||||
// 获取本地对应目录下的文件清单
|
||||
localFolderPath := GetLocalFileFullPathFromPanPath(item.Path, t.LocalFolderPath, t.PanFolderPath)
|
||||
localFiles, err2 := ioutil.ReadDir(localFolderPath)
|
||||
if err2 != nil {
|
||||
logger.Verboseln("query local file list error: ", err2)
|
||||
continue
|
||||
}
|
||||
localFileScanList := LocalFileList{}
|
||||
for _, file := range localFiles { // 逐个确认目录下面的每个文件的情况
|
||||
if strings.HasSuffix(file.Name(), DownloadingFileSuffix) {
|
||||
// 下载中的文件,跳过
|
||||
continue
|
||||
}
|
||||
localFile := newLocalFileItem(file, localFolderPath+"/"+file.Name())
|
||||
logger.Verboseln("扫描到本地文件:" + localFile.Path)
|
||||
|
||||
// 查询本地扫描数据库
|
||||
localFileInDb, _ := t.localFileDb.Get(localFile.Path)
|
||||
if localFileInDb != nil {
|
||||
// 记录存在,查看文件SHA1是否更改
|
||||
if localFile.UpdateTimeUnix() == localFileInDb.UpdateTimeUnix() && localFile.FileSize == localFileInDb.FileSize {
|
||||
// 文件大小没变,文件修改时间没变,假定文件内容也没变
|
||||
localFile.Sha1Hash = localFileInDb.Sha1Hash
|
||||
} else {
|
||||
// 文件已修改,更新文件信息到扫描数据库
|
||||
localFileInDb.Sha1Hash = localFile.Sha1Hash
|
||||
localFileInDb.UpdatedAt = localFile.UpdatedAt
|
||||
localFileInDb.CreatedAt = localFile.CreatedAt
|
||||
localFileInDb.FileSize = localFile.FileSize
|
||||
localFileInDb.FileType = localFile.FileType
|
||||
localFileInDb.ScanTimeAt = utils.NowTimeStr()
|
||||
localFileInDb.ScanStatus = ScanStatusNormal
|
||||
logger.Verboseln("update local file to db: ", utils.ObjectToJsonStr(localFileInDb, false))
|
||||
if _, er := t.localFileDb.Update(localFileInDb); er != nil {
|
||||
logger.Verboseln("local db update error ", er)
|
||||
}
|
||||
}
|
||||
}
|
||||
localFileScanList = append(localFileScanList, localFile)
|
||||
}
|
||||
|
||||
// 对比文件
|
||||
t.fileActionTaskManager.doFileDiffRoutine(localFileScanList, panFileScanList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ package syncdrive
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -20,7 +20,6 @@ type (
|
||||
FileUploadParallel int // 文件上传并发数
|
||||
FileDownloadBlockSize int64 // 文件下载分片大小
|
||||
FileUploadBlockSize int64 // 文件上传分片大小
|
||||
UseInternalUrl bool // 是否使用阿里ECS内部链接
|
||||
|
||||
MaxDownloadRate int64 // 限制最大下载速度
|
||||
MaxUploadRate int64 // 限制最大上传速度
|
||||
@@ -40,8 +39,7 @@ type (
|
||||
syncDriveConfig *SyncDriveConfig
|
||||
syncOption SyncOption
|
||||
PanUser *config.PanUser
|
||||
DriveId string
|
||||
PanClient *aliyunpan.PanClient
|
||||
PanClient *config.PanClient
|
||||
SyncConfigFolderPath string
|
||||
|
||||
// useConfigFile 是否使用配置文件启动
|
||||
@@ -59,11 +57,10 @@ var (
|
||||
ErrSyncTaskListEmpty error = fmt.Errorf("no sync task")
|
||||
)
|
||||
|
||||
func NewSyncTaskManager(user *config.PanUser, driveId string, panClient *aliyunpan.PanClient, syncConfigFolderPath string,
|
||||
func NewSyncTaskManager(user *config.PanUser, panClient *config.PanClient, syncConfigFolderPath string,
|
||||
option SyncOption) *SyncTaskManager {
|
||||
return &SyncTaskManager{
|
||||
PanUser: user,
|
||||
DriveId: driveId,
|
||||
PanClient: panClient,
|
||||
SyncConfigFolderPath: syncConfigFolderPath,
|
||||
syncOption: option,
|
||||
@@ -82,6 +79,7 @@ func (m *SyncTaskManager) parseConfigFile() error {
|
||||
"localFolderPath": "D:\\smb\\datadisk\\game",
|
||||
"panFolderPath": "/sync_drive/game",
|
||||
"mode": "sync",
|
||||
"driveName": "backup",
|
||||
"lastSyncTime": ""
|
||||
}
|
||||
]
|
||||
@@ -118,7 +116,7 @@ func (m *SyncTaskManager) ConfigFilePath() string {
|
||||
}
|
||||
|
||||
// Start 启动同步进程
|
||||
func (m *SyncTaskManager) Start(tasks []*SyncTask, step TaskStep) (bool, error) {
|
||||
func (m *SyncTaskManager) Start(tasks []*SyncTask, cycleMode CycleMode) (bool, error) {
|
||||
if tasks != nil && len(tasks) > 0 {
|
||||
m.syncDriveConfig = &SyncDriveConfig{
|
||||
ConfigVer: "1.0",
|
||||
@@ -141,6 +139,19 @@ func (m *SyncTaskManager) Start(tasks []*SyncTask, step TaskStep) (bool, error)
|
||||
if len(task.Id) == 0 {
|
||||
task.Id = utils.UuidStr()
|
||||
}
|
||||
// cycle mode
|
||||
task.CycleModeType = cycleMode
|
||||
|
||||
// check driveId
|
||||
if strings.ToLower(task.DriveName) == "backup" {
|
||||
task.DriveId = m.PanUser.DriveList.GetFileDriveId()
|
||||
} else if strings.ToLower(task.DriveName) == "resource" {
|
||||
task.DriveId = m.PanUser.DriveList.GetResourceDriveId()
|
||||
}
|
||||
if len(task.DriveId) == 0 {
|
||||
task.DriveId = m.PanUser.DriveList.GetFileDriveId()
|
||||
}
|
||||
|
||||
// check userId
|
||||
if len(task.UserId) > 0 {
|
||||
if task.UserId != m.PanUser.UserId {
|
||||
@@ -163,7 +174,6 @@ func (m *SyncTaskManager) Start(tasks []*SyncTask, step TaskStep) (bool, error)
|
||||
continue
|
||||
}
|
||||
task.panUser = m.PanUser
|
||||
task.DriveId = m.DriveId
|
||||
task.syncDbFolderPath = m.SyncConfigFolderPath
|
||||
task.panClient = m.PanClient
|
||||
task.syncOption = m.syncOption
|
||||
@@ -173,9 +183,12 @@ func (m *SyncTaskManager) Start(tasks []*SyncTask, step TaskStep) (bool, error)
|
||||
task.Priority = SyncPriorityTimestampFirst
|
||||
task.syncOption.SyncPriority = SyncPriorityTimestampFirst
|
||||
}
|
||||
if task.Policy == "" {
|
||||
task.Policy = SyncPolicyIncrement
|
||||
}
|
||||
task.LocalFolderPath = path.Clean(task.LocalFolderPath)
|
||||
task.PanFolderPath = path.Clean(task.PanFolderPath)
|
||||
if e := task.Start(step); e != nil {
|
||||
if e := task.Start(); e != nil {
|
||||
logger.Verboseln(e)
|
||||
fmt.Printf("启动同步任务[%s]出错: %s\n", task.Id, e.Error())
|
||||
continue
|
||||
@@ -192,16 +205,11 @@ func (m *SyncTaskManager) Start(tasks []*SyncTask, step TaskStep) (bool, error)
|
||||
}
|
||||
|
||||
// Stop 停止同步进程
|
||||
func (m *SyncTaskManager) Stop(step TaskStep) (bool, error) {
|
||||
func (m *SyncTaskManager) Stop() (bool, error) {
|
||||
// stop task one by one
|
||||
for _, task := range m.syncDriveConfig.SyncTaskList {
|
||||
var e error
|
||||
if step == StepScanFile {
|
||||
e = task.WaitToStop() // 阻塞直到停止
|
||||
} else {
|
||||
e = task.Stop()
|
||||
}
|
||||
|
||||
e = task.Stop()
|
||||
if e != nil {
|
||||
logger.Verboseln(e)
|
||||
fmt.Println("stop sync task error: ", task.NameLabel())
|
||||
@@ -216,3 +224,12 @@ func (m *SyncTaskManager) Stop(step TaskStep) (bool, error) {
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *SyncTaskManager) IsAllTaskCompletely() bool {
|
||||
for _, task := range m.syncDriveConfig.SyncTaskList {
|
||||
if !task.IsTaskCompletely() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package syncdrive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStart(t *testing.T) {
|
||||
refreshToken := "1640cc2d4ea...6b8ccb4d6242161a7"
|
||||
webToken, err := aliyunpan.GetAccessTokenFromRefreshToken(refreshToken)
|
||||
if err != nil {
|
||||
fmt.Println("get acccess token error")
|
||||
return
|
||||
}
|
||||
|
||||
// pan client
|
||||
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{}, aliyunpan.AppConfig{
|
||||
AppId: "25dzX3vbYqktVxyX",
|
||||
DeviceId: "E75459EXhOTkI5ZI6S3qDHA3",
|
||||
UserId: "",
|
||||
Nonce: 0,
|
||||
PublicKey: "",
|
||||
}, aliyunpan.SessionConfig{
|
||||
DeviceName: "Chrome浏览器",
|
||||
ModelName: "Windows网页版",
|
||||
})
|
||||
user, _ := panClient.GetUserInfo()
|
||||
|
||||
manager := NewSyncTaskManager(
|
||||
nil,
|
||||
user.FileDriveId,
|
||||
panClient,
|
||||
"D:\\smb\\feny\\goprojects\\dev\\sync_drive",
|
||||
SyncOption{},
|
||||
)
|
||||
|
||||
manager.Start(nil, StepSyncFile)
|
||||
time.Sleep(30 * time.Minute)
|
||||
manager.Stop(StepSyncFile)
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package syncdrive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSyncTask(t *testing.T) {
|
||||
refreshToken := "84c6499b7...9a2fe4f6733c3afd"
|
||||
webToken, err := aliyunpan.GetAccessTokenFromRefreshToken(refreshToken)
|
||||
if err != nil {
|
||||
fmt.Println("get acccess token error")
|
||||
return
|
||||
}
|
||||
|
||||
// pan client
|
||||
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{}, aliyunpan.AppConfig{
|
||||
AppId: "25dzX3vbYqktVxyX",
|
||||
DeviceId: "E75459EXhOTkI5ZI6S3qDHA3",
|
||||
UserId: "",
|
||||
Nonce: 0,
|
||||
PublicKey: "",
|
||||
}, aliyunpan.SessionConfig{
|
||||
DeviceName: "Chrome浏览器",
|
||||
ModelName: "Windows网页版",
|
||||
})
|
||||
user, _ := panClient.GetUserInfo()
|
||||
task := SyncTask{
|
||||
Id: "5b2d7c10-e927-4e72-8f9d-5abb3bb04814",
|
||||
DriveId: user.FileDriveId,
|
||||
LocalFolderPath: "D:\\smb\\feny\\goprojects\\dev\\NS游戏备份",
|
||||
PanFolderPath: "/sync_drive",
|
||||
Mode: "sync",
|
||||
LastSyncTime: "",
|
||||
|
||||
syncDbFolderPath: "D:\\smb\\feny\\goprojects\\dev\\sync_drive",
|
||||
panClient: panClient,
|
||||
}
|
||||
task.Start(StepSyncFile)
|
||||
//go func() {
|
||||
// time.Sleep(10 * time.Second)
|
||||
// task.Stop()
|
||||
//}()
|
||||
time.Sleep(60 * time.Second)
|
||||
task.Stop()
|
||||
}
|
||||
|
||||
func TestSyncLocker(t *testing.T) {
|
||||
locker := &sync.Mutex{}
|
||||
locker.Lock()
|
||||
fmt.Println("lock")
|
||||
//locker.Unlock()
|
||||
defer locker.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package syncdrive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetPanFileFullPathFromLocalPath 获取网盘文件的路径
|
||||
func GetPanFileFullPathFromLocalPath(localFilePath, localRootPath, panRootPath string) string {
|
||||
localFilePath = strings.ReplaceAll(localFilePath, "\\", "/")
|
||||
localRootPath = strings.ReplaceAll(localRootPath, "\\", "/")
|
||||
|
||||
relativePath := strings.TrimPrefix(localFilePath, localRootPath)
|
||||
panPath := path.Join(path.Clean(panRootPath), relativePath)
|
||||
return strings.ReplaceAll(panPath, "\\", "/")
|
||||
}
|
||||
|
||||
// GetLocalFileFullPathFromPanPath 获取本地文件的路径
|
||||
func GetLocalFileFullPathFromPanPath(panFilePath, localRootPath, panRootPath string) string {
|
||||
panFilePath = strings.ReplaceAll(panFilePath, "\\", "/")
|
||||
panRootPath = strings.ReplaceAll(panRootPath, "\\", "/")
|
||||
|
||||
relativePath := strings.TrimPrefix(panFilePath, panRootPath)
|
||||
return path.Join(path.Clean(localRootPath), relativePath)
|
||||
}
|
||||
|
||||
// IsSymlinkFile 是否是软链接文件
|
||||
func IsSymlinkFile(file fs.FileInfo) bool {
|
||||
if file.Mode()&os.ModeSymlink != 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PromptPrintln 输出提示消息到控制台
|
||||
func PromptPrintln(msg string) {
|
||||
if LogPrompt {
|
||||
//fmt.Println("[" + utils.NowTimeStr() + "] " + msg)
|
||||
fmt.Println(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func PromptPrint(msg string) {
|
||||
if LogPrompt {
|
||||
fmt.Print(msg)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -18,24 +18,24 @@ import "time"
|
||||
type (
|
||||
TaskUnit interface {
|
||||
SetTaskInfo(info *TaskInfo)
|
||||
// 执行任务
|
||||
// Run 执行任务
|
||||
Run() (result *TaskUnitRunResult)
|
||||
// 重试任务执行的方法
|
||||
// OnRetry 重试任务执行的方法
|
||||
// 当达到最大重试次数, 执行失败
|
||||
OnRetry(lastRunResult *TaskUnitRunResult)
|
||||
// 每次执行成功执行的方法
|
||||
// OnSuccess 每次执行成功执行的方法
|
||||
OnSuccess(lastRunResult *TaskUnitRunResult)
|
||||
// 每次执行失败执行的方法
|
||||
// OnFailed 每次执行失败执行的方法
|
||||
OnFailed(lastRunResult *TaskUnitRunResult)
|
||||
// 每次执行结束执行的方法, 不管成功失败
|
||||
// OnComplete 每次执行结束执行的方法, 不管成功失败
|
||||
OnComplete(lastRunResult *TaskUnitRunResult)
|
||||
// 取消下载
|
||||
// OnCancel 取消下载
|
||||
OnCancel(lastRunResult *TaskUnitRunResult)
|
||||
// 重试等待的时间
|
||||
// RetryWait 重试等待的时间
|
||||
RetryWait() time.Duration
|
||||
}
|
||||
|
||||
// 任务单元执行结果
|
||||
// TaskUnitRunResult 任务单元执行结果
|
||||
TaskUnitRunResult struct {
|
||||
Succeed bool // 是否执行成功
|
||||
NeedRetry bool // 是否需要重试
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package webdav
|
||||
|
||||
const (
|
||||
KeySessionId = "sessionId"
|
||||
KeyContentLength = "contentLength"
|
||||
KeyUserId = "userId"
|
||||
)
|
||||
@@ -1,750 +0,0 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/expires"
|
||||
"github.com/tickstep/library-go/expires/cachemap"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FileDownloadStream struct {
|
||||
readOffset int64
|
||||
resp *http.Response
|
||||
timestamp int64
|
||||
}
|
||||
|
||||
type FileUploadStream struct {
|
||||
fileUploadInfoEntity *aliyunpan.CreateFileUploadResult
|
||||
|
||||
filePath string
|
||||
fileSize int64
|
||||
fileId string
|
||||
fileWritePos int64
|
||||
fileUploadUrlIndex int
|
||||
|
||||
chunkBuffer []byte
|
||||
chunkPos int64
|
||||
chunkSize int64
|
||||
|
||||
timestamp int64
|
||||
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
type PanClientProxy struct {
|
||||
PanUser *config.PanUser
|
||||
PanDriveId string
|
||||
PanTransferUrlType int
|
||||
|
||||
mutex sync.Mutex
|
||||
|
||||
// 网盘文件路径到网盘文件信息实体映射缓存
|
||||
filePathCacheMap cachemap.CacheOpMap
|
||||
|
||||
// 网盘文件夹路径到文件夹下面所有子文件映射缓存
|
||||
fileDirectoryListCacheMap cachemap.CacheOpMap
|
||||
|
||||
// 网盘文件ID到文件下载链接映射缓存
|
||||
fileIdDownloadUrlCacheMap cachemap.CacheOpMap
|
||||
|
||||
// 网盘文件ID到文件下载数据流映射缓存
|
||||
fileIdDownloadStreamCacheMap cachemap.CacheOpMap
|
||||
|
||||
// 网盘文件到文件上传数据流映射缓存
|
||||
filePathUploadStreamCacheMap cachemap.CacheOpMap
|
||||
}
|
||||
|
||||
// DefaultChunkSize 默认上传的文件块大小,10MB
|
||||
const DefaultChunkSize = 10 * 1024 * 1024
|
||||
|
||||
// CacheExpiredMinute 缓存过期分钟
|
||||
const CacheExpiredMinute = 60
|
||||
|
||||
// FileDownloadUrlExpiredSeconds 文件下载URL过期时间
|
||||
const FileDownloadUrlExpiredSeconds = 14400
|
||||
|
||||
// FileUploadExpiredMinute 文件上传数据流过期时间
|
||||
const FileUploadExpiredMinute = 1440 // 24小时
|
||||
|
||||
// getDownloadFileUrl 获取文件下载URL
|
||||
func (p *PanClientProxy) getFileDownloadUrl(urlResult *aliyunpan.GetFileDownloadUrlResult) string {
|
||||
if urlResult == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if p.PanTransferUrlType == 2 { // 阿里ECS内网链接
|
||||
return urlResult.InternalUrl
|
||||
}
|
||||
return urlResult.Url
|
||||
}
|
||||
|
||||
// getFileUploadUrl 获取文件上传URL
|
||||
func (p *PanClientProxy) getFileUploadUrl(urlResult aliyunpan.FileUploadPartInfoResult) string {
|
||||
if p.PanTransferUrlType == 2 { // 阿里ECS内网链接
|
||||
return urlResult.InternalUploadURL
|
||||
}
|
||||
return urlResult.UploadURL
|
||||
}
|
||||
|
||||
// DeleteCache 删除含有 dirs 的缓存
|
||||
func (p *PanClientProxy) deleteFilesDirectoriesListCache(dirs []string) {
|
||||
cache := p.fileDirectoryListCacheMap.LazyInitCachePoolOp(p.PanDriveId)
|
||||
for _, v := range dirs {
|
||||
key := formatPathStyle(v)
|
||||
_, ok := cache.Load(key)
|
||||
if ok {
|
||||
cache.Delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteOneCache 删除缓存
|
||||
func (p *PanClientProxy) deleteOneFilesDirectoriesListCache(dirPath string) {
|
||||
dirPath = formatPathStyle(dirPath)
|
||||
ps := []string{dirPath}
|
||||
p.deleteFilesDirectoriesListCache(ps)
|
||||
}
|
||||
|
||||
// cacheFilesDirectoriesList 缓存文件夹下面的所有文件列表
|
||||
func (p *PanClientProxy) cacheFilesDirectoriesList(pathStr string) (fdl aliyunpan.FileList, apiError *apierror.ApiError) {
|
||||
pathStr = formatPathStyle(pathStr)
|
||||
data := p.fileDirectoryListCacheMap.CacheOperation(p.PanDriveId, pathStr, func() expires.DataExpires {
|
||||
fi, er := p.cacheFilePath(pathStr)
|
||||
if er != nil {
|
||||
return nil
|
||||
}
|
||||
fileListParam := &aliyunpan.FileListParam{
|
||||
DriveId: p.PanDriveId,
|
||||
ParentFileId: fi.FileId,
|
||||
Limit: 200,
|
||||
}
|
||||
fdl, apiError = p.PanUser.PanClient().FileListGetAll(fileListParam, 200)
|
||||
if apiError != nil {
|
||||
return nil
|
||||
}
|
||||
if len(fdl) == 0 {
|
||||
// 空目录不缓存
|
||||
return nil
|
||||
}
|
||||
// construct full path
|
||||
for _, f := range fdl {
|
||||
f.Path = path.Join(pathStr, f.FileName)
|
||||
}
|
||||
p.cacheFilePathEntityList(fdl)
|
||||
return expires.NewDataExpires(fdl, CacheExpiredMinute*time.Minute)
|
||||
})
|
||||
if apiError != nil {
|
||||
return
|
||||
}
|
||||
if data == nil {
|
||||
return aliyunpan.FileList{}, nil
|
||||
}
|
||||
return data.Data().(aliyunpan.FileList), nil
|
||||
}
|
||||
|
||||
// deleteOneFilePathCache 删除缓存
|
||||
func (p *PanClientProxy) deleteOneFilePathCache(pathStr string) {
|
||||
key := formatPathStyle(pathStr)
|
||||
cache := p.filePathCacheMap.LazyInitCachePoolOp(p.PanDriveId)
|
||||
_, ok := cache.Load(key)
|
||||
if ok {
|
||||
cache.Delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
// cacheFilePath 缓存文件绝对路径到网盘文件信息
|
||||
func (p *PanClientProxy) cacheFilePath(pathStr string) (fe *aliyunpan.FileEntity, apiError *apierror.ApiError) {
|
||||
pathStr = formatPathStyle(pathStr)
|
||||
data := p.filePathCacheMap.CacheOperation(p.PanDriveId, pathStr, func() expires.DataExpires {
|
||||
var fi *aliyunpan.FileEntity
|
||||
fi, apiError = p.PanUser.PanClient().FileInfoByPath(p.PanDriveId, pathStr)
|
||||
if apiError != nil {
|
||||
return nil
|
||||
}
|
||||
return expires.NewDataExpires(fi, CacheExpiredMinute*time.Minute)
|
||||
})
|
||||
if apiError != nil {
|
||||
return nil, apiError
|
||||
}
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return data.Data().(*aliyunpan.FileEntity), nil
|
||||
}
|
||||
|
||||
func (p *PanClientProxy) cacheFilePathEntity(fe *aliyunpan.FileEntity) {
|
||||
pathStr := formatPathStyle(fe.Path)
|
||||
p.filePathCacheMap.CacheOperation(p.PanDriveId, pathStr, func() expires.DataExpires {
|
||||
return expires.NewDataExpires(fe, CacheExpiredMinute*time.Minute)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *PanClientProxy) cacheFilePathEntityList(fdl aliyunpan.FileList) {
|
||||
for _, entity := range fdl {
|
||||
pathStr := formatPathStyle(entity.Path)
|
||||
p.filePathCacheMap.CacheOperation(p.PanDriveId, pathStr, func() expires.DataExpires {
|
||||
return expires.NewDataExpires(entity, CacheExpiredMinute*time.Minute)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// cacheFileDownloadStream 缓存文件下载路径
|
||||
func (p *PanClientProxy) cacheFileDownloadUrl(sessionId, fileId string) (urlResult *aliyunpan.GetFileDownloadUrlResult, apiError *apierror.ApiError) {
|
||||
k := sessionId + "-" + fileId
|
||||
data := p.fileIdDownloadUrlCacheMap.CacheOperation(p.PanDriveId, k, func() expires.DataExpires {
|
||||
urlResult, err1 := p.PanUser.PanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
|
||||
DriveId: p.PanDriveId,
|
||||
FileId: fileId,
|
||||
ExpireSec: FileDownloadUrlExpiredSeconds,
|
||||
})
|
||||
if err1 != nil {
|
||||
return nil
|
||||
}
|
||||
return expires.NewDataExpires(urlResult, (FileDownloadUrlExpiredSeconds-60)*time.Second)
|
||||
})
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return data.Data().(*aliyunpan.GetFileDownloadUrlResult), nil
|
||||
}
|
||||
|
||||
// deleteOneFileDownloadStreamCache 删除缓存文件下载流缓存
|
||||
func (p *PanClientProxy) deleteOneFileDownloadStreamCache(sessionId, fileId string) {
|
||||
key := sessionId + "-" + fileId
|
||||
cache := p.fileIdDownloadStreamCacheMap.LazyInitCachePoolOp(p.PanDriveId)
|
||||
_, ok := cache.Load(key)
|
||||
if ok {
|
||||
cache.Delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
// cacheFileDownloadStream 缓存文件下载流
|
||||
func (p *PanClientProxy) cacheFileDownloadStream(sessionId, fileId string, offset int64) (fds *FileDownloadStream, apiError *apierror.ApiError) {
|
||||
k := sessionId + "-" + fileId
|
||||
data := p.fileIdDownloadStreamCacheMap.CacheOperation(p.PanDriveId, k, func() expires.DataExpires {
|
||||
urlResult, err1 := p.cacheFileDownloadUrl(sessionId, fileId)
|
||||
if err1 != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
var err error
|
||||
var client = requester.NewHTTPClient()
|
||||
// set to no timeout
|
||||
client.Timeout = 0
|
||||
apierr := p.PanUser.PanClient().DownloadFileData(
|
||||
p.getFileDownloadUrl(urlResult),
|
||||
aliyunpan.FileDownloadRange{
|
||||
Offset: offset,
|
||||
End: 0,
|
||||
},
|
||||
func(httpMethod, fullUrl string, headers map[string]string) (*http.Response, error) {
|
||||
resp, err = client.Req(httpMethod, fullUrl, nil, headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, err
|
||||
})
|
||||
|
||||
if apierr != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case 200, 206:
|
||||
// do nothing, continue
|
||||
break
|
||||
case 416: //Requested Range Not Satisfiable
|
||||
fallthrough
|
||||
case 403: // Forbidden
|
||||
fallthrough
|
||||
case 406: // Not Acceptable
|
||||
return nil
|
||||
case 404:
|
||||
return nil
|
||||
case 429, 509: // Too Many Requests
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Verboseln(sessionId + " create new cache for offset = " + strconv.Itoa(int(offset)))
|
||||
return expires.NewDataExpires(&FileDownloadStream{
|
||||
readOffset: offset,
|
||||
resp: resp,
|
||||
timestamp: time.Now().Unix(),
|
||||
}, CacheExpiredMinute*time.Minute)
|
||||
})
|
||||
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return data.Data().(*FileDownloadStream), nil
|
||||
}
|
||||
|
||||
// deleteOneFileUploadStreamCache 删除缓存文件下载流缓存
|
||||
func (p *PanClientProxy) deleteOneFileUploadStreamCache(userId, pathStr string) {
|
||||
pathStr = formatPathStyle(pathStr)
|
||||
key := userId + "-" + pathStr
|
||||
cache := p.filePathUploadStreamCacheMap.LazyInitCachePoolOp(p.PanDriveId)
|
||||
_, ok := cache.Load(key)
|
||||
if ok {
|
||||
cache.Delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
// cacheFileUploadStream 缓存创建的文件上传流
|
||||
func (p *PanClientProxy) cacheFileUploadStream(userId, pathStr string, fileSize int64, chunkSize int64) (*FileUploadStream, *apierror.ApiError) {
|
||||
pathStr = formatPathStyle(pathStr)
|
||||
k := userId + "-" + pathStr
|
||||
// TODO: add locker for upload file create
|
||||
data := p.filePathUploadStreamCacheMap.CacheOperation(p.PanDriveId, k, func() expires.DataExpires {
|
||||
// check parent dir is existed or not
|
||||
parentFileId := ""
|
||||
parentFileEntity, err1 := p.cacheFilePath(path.Dir(pathStr))
|
||||
if err1 != nil {
|
||||
return nil
|
||||
}
|
||||
if parentFileEntity == nil {
|
||||
// create parent folder
|
||||
mkr, err2 := p.mkdir(path.Dir(pathStr), 0)
|
||||
if err2 != nil {
|
||||
return nil
|
||||
}
|
||||
parentFileId = mkr.FileId
|
||||
} else {
|
||||
parentFileId = parentFileEntity.FileId
|
||||
}
|
||||
|
||||
// 检查同名文件是否存在
|
||||
efi, apierr := p.PanUser.PanClient().FileInfoByPath(p.PanDriveId, pathStr)
|
||||
if apierr != nil {
|
||||
if apierr.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
// file not existed
|
||||
logger.Verbosef("%s 没有存在同名文件,直接上传: %s\n", userId, pathStr)
|
||||
} else {
|
||||
// TODO: handle error
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if efi != nil && efi.FileId != "" {
|
||||
// existed, delete it
|
||||
var fileDeleteResult []*aliyunpan.FileBatchActionResult
|
||||
var err *apierror.ApiError
|
||||
fileDeleteResult, err = p.PanUser.PanClient().FileDelete([]*aliyunpan.FileBatchActionParam{{DriveId: efi.DriveId, FileId: efi.FileId}})
|
||||
if err != nil || len(fileDeleteResult) == 0 {
|
||||
logger.Verbosef("%s 同名无法删除文件,请稍后重试: %s\n", userId, pathStr)
|
||||
return nil
|
||||
}
|
||||
time.Sleep(time.Duration(500) * time.Millisecond)
|
||||
logger.Verbosef("%s 检测到同名文件,已移动到回收站: %s\n", userId, pathStr)
|
||||
|
||||
// clear cache
|
||||
p.deleteOneFilePathCache(pathStr)
|
||||
p.deleteOneFilesDirectoriesListCache(path.Dir(pathStr))
|
||||
}
|
||||
}
|
||||
|
||||
// create new upload file
|
||||
appCreateUploadFileParam := &aliyunpan.CreateFileUploadParam{
|
||||
DriveId: p.PanDriveId,
|
||||
Name: filepath.Base(pathStr),
|
||||
Size: fileSize,
|
||||
ContentHash: "",
|
||||
ContentHashName: "none",
|
||||
CheckNameMode: "refuse",
|
||||
ParentFileId: parentFileId,
|
||||
BlockSize: chunkSize,
|
||||
ProofCode: "",
|
||||
ProofVersion: "v1",
|
||||
}
|
||||
|
||||
uploadOpEntity, apierr := p.PanUser.PanClient().CreateUploadFile(appCreateUploadFileParam)
|
||||
if apierr != nil {
|
||||
logger.Verbosef("%s 创建上传任务失败: %s\n", userId, pathStr)
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Verbosef("%s create new upload cache for path = %s\n", userId, pathStr)
|
||||
return expires.NewDataExpires(&FileUploadStream{
|
||||
fileUploadInfoEntity: uploadOpEntity,
|
||||
filePath: pathStr,
|
||||
fileSize: fileSize,
|
||||
fileId: uploadOpEntity.FileId,
|
||||
fileWritePos: 0,
|
||||
fileUploadUrlIndex: 0,
|
||||
chunkBuffer: make([]byte, chunkSize, chunkSize),
|
||||
chunkPos: 0,
|
||||
chunkSize: chunkSize,
|
||||
timestamp: time.Now().Unix(),
|
||||
}, FileUploadExpiredMinute*time.Minute)
|
||||
})
|
||||
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return data.Data().(*FileUploadStream), nil
|
||||
}
|
||||
|
||||
// FileInfoByPath 通过文件路径获取网盘文件信息
|
||||
func (p *PanClientProxy) FileInfoByPath(pathStr string) (fileInfo *aliyunpan.FileEntity, error *apierror.ApiError) {
|
||||
return p.cacheFilePath(pathStr)
|
||||
}
|
||||
|
||||
// FileListGetAll 获取文件路径下的所有子文件列表
|
||||
func (p *PanClientProxy) FileListGetAll(pathStr string) (aliyunpan.FileList, *apierror.ApiError) {
|
||||
return p.cacheFilesDirectoriesList(pathStr)
|
||||
}
|
||||
|
||||
func (p *PanClientProxy) mkdir(pathStr string, perm os.FileMode) (*aliyunpan.MkdirResult, error) {
|
||||
pathStr = formatPathStyle(pathStr)
|
||||
r, er := p.PanUser.PanClient().MkdirByFullPath(p.PanDriveId, pathStr)
|
||||
if er != nil {
|
||||
return nil, er
|
||||
}
|
||||
|
||||
// invalidate cache
|
||||
p.deleteOneFilesDirectoriesListCache(path.Dir(pathStr))
|
||||
|
||||
if r.FileId != "" {
|
||||
fe, _ := p.PanUser.PanClient().FileInfoById(p.PanDriveId, r.FileId)
|
||||
if fe != nil {
|
||||
fe.Path = pathStr
|
||||
p.cacheFilePathEntity(fe)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unknown error")
|
||||
}
|
||||
|
||||
// Mkdir 创建目录
|
||||
func (p *PanClientProxy) Mkdir(pathStr string, perm os.FileMode) error {
|
||||
if pathStr == "" {
|
||||
return fmt.Errorf("unknown error")
|
||||
}
|
||||
pathStr = formatPathStyle(pathStr)
|
||||
_, er := p.mkdir(pathStr, perm)
|
||||
return er
|
||||
}
|
||||
|
||||
// Rename 重命名文件
|
||||
func (p *PanClientProxy) Rename(oldpath, newpath string) error {
|
||||
oldpath = formatPathStyle(oldpath)
|
||||
newpath = formatPathStyle(newpath)
|
||||
|
||||
oldFile, er := p.cacheFilePath(oldpath)
|
||||
if er != nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
_, e := p.PanUser.PanClient().FileRename(p.PanDriveId, oldFile.FileId, path.Base(newpath))
|
||||
if e != nil {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
// invalidate parent folder cache
|
||||
p.deleteOneFilesDirectoriesListCache(path.Dir(oldpath))
|
||||
|
||||
// add new name cache
|
||||
oldFile.Path = newpath
|
||||
oldFile.FileName = path.Base(newpath)
|
||||
p.cacheFilePathEntity(oldFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Move 移动文件
|
||||
func (p *PanClientProxy) Move(oldpath, newpath string) error {
|
||||
oldpath = formatPathStyle(oldpath)
|
||||
newpath = formatPathStyle(newpath)
|
||||
|
||||
oldFile, er := p.cacheFilePath(oldpath)
|
||||
if er != nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
newFileParentDir, er := p.cacheFilePath(path.Dir(newpath))
|
||||
if er != nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
param := aliyunpan.FileMoveParam{
|
||||
DriveId: p.PanDriveId,
|
||||
FileId: oldFile.FileId,
|
||||
ToDriveId: p.PanDriveId,
|
||||
ToParentFileId: newFileParentDir.FileId,
|
||||
}
|
||||
params := []*aliyunpan.FileMoveParam{}
|
||||
params = append(params, ¶m)
|
||||
_, e := p.PanUser.PanClient().FileMove(params)
|
||||
if e != nil {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
// invalidate parent folder cache
|
||||
p.deleteOneFilesDirectoriesListCache(path.Dir(oldpath))
|
||||
p.deleteOneFilesDirectoriesListCache(path.Dir(newpath))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloadFilePart 下载文件指定数据片段
|
||||
func (p *PanClientProxy) DownloadFilePart(sessionId, fileId string, offset int64, buffer []byte) (int, error) {
|
||||
fds, err1 := p.cacheFileDownloadStream(sessionId, fileId, offset)
|
||||
if err1 != nil {
|
||||
return 0, err1
|
||||
}
|
||||
|
||||
if fds.readOffset != offset {
|
||||
// delete old one
|
||||
if fds.resp != nil {
|
||||
fds.resp.Body.Close()
|
||||
}
|
||||
p.deleteOneFileDownloadStreamCache(sessionId, fileId)
|
||||
logger.Verboseln(sessionId + " offset mismatch offset = " + strconv.Itoa(int(offset)) + " cache offset = " + strconv.Itoa(int(fds.readOffset)))
|
||||
|
||||
// create new one
|
||||
fds, err1 = p.cacheFileDownloadStream(sessionId, fileId, offset)
|
||||
if err1 != nil {
|
||||
return 0, err1
|
||||
}
|
||||
}
|
||||
|
||||
if fds.resp.Close {
|
||||
// delete old one
|
||||
p.deleteOneFileDownloadStreamCache(sessionId, fileId)
|
||||
logger.Verboseln(sessionId + "remote data stream close, stream offset = " + strconv.Itoa(int(fds.readOffset)))
|
||||
|
||||
// create new one
|
||||
fds, err1 = p.cacheFileDownloadStream(sessionId, fileId, offset)
|
||||
if err1 != nil {
|
||||
return 0, err1
|
||||
}
|
||||
}
|
||||
|
||||
readByteCount, readErr := fds.resp.Body.Read(buffer)
|
||||
if readErr != nil {
|
||||
if readErr.Error() == "EOF" {
|
||||
logger.Verboseln(sessionId + " read EOF last offset = " + strconv.Itoa(int(offset)))
|
||||
// end of file
|
||||
if fds.resp != nil {
|
||||
fds.resp.Body.Close()
|
||||
}
|
||||
p.deleteOneFileDownloadStreamCache(sessionId, fileId)
|
||||
} else {
|
||||
// TODO: handler other error
|
||||
return 0, readErr
|
||||
}
|
||||
}
|
||||
fds.readOffset += int64(readByteCount)
|
||||
return readByteCount, nil
|
||||
}
|
||||
|
||||
// RemoveAll 删除文件
|
||||
func (p *PanClientProxy) RemoveAll(pathStr string) error {
|
||||
fi, er := p.FileInfoByPath(pathStr)
|
||||
if er != nil {
|
||||
return er
|
||||
}
|
||||
if fi == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
param := &aliyunpan.FileBatchActionParam{
|
||||
DriveId: p.PanDriveId,
|
||||
FileId: fi.FileId,
|
||||
}
|
||||
_, e := p.PanUser.PanClient().FileDelete(append([]*aliyunpan.FileBatchActionParam{}, param))
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
// delete cache
|
||||
p.deleteOneFilesDirectoriesListCache(path.Dir(pathStr))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UploadFilePrepare 创建文件上传
|
||||
func (p *PanClientProxy) UploadFilePrepare(userId, pathStr string, fileSize int64, chunkSize int64) (*FileUploadStream, error) {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
cs := chunkSize
|
||||
if cs == 0 {
|
||||
cs = DefaultChunkSize
|
||||
}
|
||||
|
||||
// remove old file cache
|
||||
oldFus, err := p.UploadFileCache(userId, pathStr)
|
||||
if err != nil {
|
||||
logger.Verboseln("query upload file cache error: ", err)
|
||||
}
|
||||
if oldFus != nil {
|
||||
// remove old upload stream cache
|
||||
oldFus.mutex.Lock()
|
||||
p.deleteOneFileUploadStreamCache(userId, pathStr)
|
||||
oldFus.mutex.Unlock()
|
||||
}
|
||||
|
||||
// create new one
|
||||
fus, er := p.cacheFileUploadStream(userId, pathStr, fileSize, cs)
|
||||
if er != nil {
|
||||
return nil, er
|
||||
}
|
||||
return fus, nil
|
||||
}
|
||||
|
||||
func (p *PanClientProxy) UploadFileCache(userId, pathStr string) (*FileUploadStream, error) {
|
||||
key := userId + "-" + formatPathStyle(pathStr)
|
||||
cache := p.filePathUploadStreamCacheMap.LazyInitCachePoolOp(p.PanDriveId)
|
||||
v, ok := cache.Load(key)
|
||||
if ok {
|
||||
return v.Data().(*FileUploadStream), nil
|
||||
}
|
||||
return nil, fmt.Errorf("upload file not found")
|
||||
}
|
||||
|
||||
func (p *PanClientProxy) needToUploadChunk(fus *FileUploadStream) bool {
|
||||
if fus.chunkPos == fus.chunkSize {
|
||||
return true
|
||||
}
|
||||
|
||||
// maybe the final part
|
||||
if fus.fileUploadUrlIndex == (len(fus.fileUploadInfoEntity.PartInfoList) - 1) {
|
||||
finalPartSize := fus.fileSize % fus.chunkSize
|
||||
if finalPartSize == 0 {
|
||||
finalPartSize = fus.chunkSize
|
||||
}
|
||||
if fus.chunkPos == finalPartSize {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isUrlExpired 上传链接是否已过期。过期返回True
|
||||
func (p *PanClientProxy) isUrlExpired(urlStr string) bool {
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
expiredTimeSecStr := u.Query().Get("x-oss-expires")
|
||||
expiredTimeSec, _ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
|
||||
if (time.Now().Unix() - 10) >= expiredTimeSec {
|
||||
// expired
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UploadFilePart 上传文件数据块
|
||||
func (p *PanClientProxy) UploadFilePart(userId, pathStr string, offset int64, buffer []byte) (int, error) {
|
||||
fus, err := p.UploadFileCache(userId, pathStr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
fus.mutex.Lock()
|
||||
defer fus.mutex.Unlock()
|
||||
|
||||
if fus.fileWritePos != offset {
|
||||
// error
|
||||
return 0, fmt.Errorf("file write offset position mismatch")
|
||||
}
|
||||
|
||||
// write buffer to chunk buffer
|
||||
uploadCount := 0
|
||||
for _, b := range buffer {
|
||||
fus.chunkBuffer[fus.chunkPos] = b
|
||||
fus.chunkPos += 1
|
||||
fus.fileWritePos += 1
|
||||
uploadCount += 1
|
||||
|
||||
if p.needToUploadChunk(fus) {
|
||||
// upload chunk to drive
|
||||
uploadBuffer := fus.chunkBuffer
|
||||
if fus.chunkPos < fus.chunkSize {
|
||||
uploadBuffer = make([]byte, fus.chunkPos)
|
||||
copy(uploadBuffer, fus.chunkBuffer)
|
||||
}
|
||||
uploadChunk := bytes.NewReader(uploadBuffer)
|
||||
if fus.fileUploadUrlIndex >= len(fus.fileUploadInfoEntity.PartInfoList) {
|
||||
return uploadCount, fmt.Errorf("upload file uploading status mismatch")
|
||||
}
|
||||
uploadPartInfo := fus.fileUploadInfoEntity.PartInfoList[fus.fileUploadUrlIndex]
|
||||
cd := &aliyunpan.FileUploadChunkData{
|
||||
Reader: uploadChunk,
|
||||
ChunkSize: uploadChunk.Size(),
|
||||
}
|
||||
urlStr := p.getFileUploadUrl(uploadPartInfo)
|
||||
if p.isUrlExpired(urlStr) {
|
||||
// get renew upload url
|
||||
infoList := make([]aliyunpan.FileUploadPartInfoParam, len(fus.fileUploadInfoEntity.PartInfoList))
|
||||
for _, item := range fus.fileUploadInfoEntity.PartInfoList {
|
||||
infoList = append(infoList, aliyunpan.FileUploadPartInfoParam{
|
||||
PartNumber: item.PartNumber,
|
||||
})
|
||||
}
|
||||
refreshUploadParam := &aliyunpan.GetUploadUrlParam{
|
||||
DriveId: fus.fileUploadInfoEntity.DriveId,
|
||||
FileId: fus.fileUploadInfoEntity.FileId,
|
||||
PartInfoList: infoList,
|
||||
UploadId: fus.fileUploadInfoEntity.UploadId,
|
||||
}
|
||||
newUploadInfo, err := p.PanUser.PanClient().GetUploadUrl(refreshUploadParam)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
fus.fileUploadInfoEntity.PartInfoList = newUploadInfo.PartInfoList
|
||||
|
||||
// use new upload url
|
||||
urlStr = p.getFileUploadUrl(fus.fileUploadInfoEntity.PartInfoList[fus.fileUploadUrlIndex])
|
||||
}
|
||||
e := p.PanUser.PanClient().UploadDataChunk(urlStr, cd)
|
||||
if e != nil {
|
||||
// upload error
|
||||
// TODO: handle error, retry upload
|
||||
return uploadCount, nil
|
||||
}
|
||||
fus.fileUploadUrlIndex += 1
|
||||
|
||||
// reset chunk buffer
|
||||
fus.chunkPos = 0
|
||||
}
|
||||
}
|
||||
|
||||
// check file upload completely or not
|
||||
if fus.fileSize == fus.fileWritePos {
|
||||
// complete file upload
|
||||
cufr, err := p.PanUser.PanClient().CompleteUploadFile(&aliyunpan.CompleteUploadFileParam{
|
||||
DriveId: p.PanDriveId,
|
||||
FileId: fus.fileId,
|
||||
UploadId: fus.fileUploadInfoEntity.UploadId,
|
||||
})
|
||||
logger.Verbosef("%s complete upload file: %+v\n", userId, cufr)
|
||||
|
||||
if err != nil {
|
||||
logger.Verbosef("%s complete upload file error: %s\n", userId, err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// remove cache
|
||||
p.deleteOneFileUploadStreamCache(userId, pathStr)
|
||||
p.deleteOneFilePathCache(pathStr)
|
||||
p.deleteOneFilesDirectoriesListCache(path.Dir(pathStr))
|
||||
}
|
||||
|
||||
return uploadCount, nil
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
// Rule is a dissalow/allow rule.
|
||||
type Rule struct {
|
||||
Regex bool
|
||||
Allow bool
|
||||
Modify bool
|
||||
Path string
|
||||
Regexp *regexp.Regexp
|
||||
}
|
||||
|
||||
// User contains the settings of each user.
|
||||
type User struct {
|
||||
Username string
|
||||
Password string
|
||||
Scope string
|
||||
Modify bool // 用户是否具备修改权限
|
||||
Rules []*Rule // 根据访问路径进行精细化权限控制
|
||||
Handler *webdav.Handler
|
||||
}
|
||||
|
||||
// Allowed checks if the user has permission to access a directory/file
|
||||
func (u User) Allowed(url string, noModification bool) bool {
|
||||
var rule *Rule
|
||||
i := len(u.Rules) - 1
|
||||
|
||||
for i >= 0 {
|
||||
rule = u.Rules[i]
|
||||
|
||||
isAllowed := rule.Allow && (noModification || rule.Modify)
|
||||
if rule.Regex {
|
||||
if rule.Regexp.MatchString(url) {
|
||||
return isAllowed
|
||||
}
|
||||
} else if strings.HasPrefix(url, rule.Path) {
|
||||
return isAllowed
|
||||
}
|
||||
|
||||
i--
|
||||
}
|
||||
|
||||
return noModification || u.Modify
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func checkPassword(saved, input string) bool {
|
||||
if strings.HasPrefix(saved, "{bcrypt}") {
|
||||
savedPassword := strings.TrimPrefix(saved, "{bcrypt}")
|
||||
return bcrypt.CompareHashAndPassword([]byte(savedPassword), []byte(input)) == nil
|
||||
}
|
||||
|
||||
return saved == input
|
||||
}
|
||||
|
||||
func isAllowedHost(allowedHosts []string, origin string) bool {
|
||||
for _, host := range allowedHosts {
|
||||
if host == origin {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func formatPathStyle(pathStr string) string {
|
||||
pathStr = strings.ReplaceAll(pathStr, "\\", "/")
|
||||
if pathStr != "/" {
|
||||
pathStr = strings.TrimSuffix(pathStr, "/")
|
||||
}
|
||||
return pathStr
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"golang.org/x/net/webdav"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CorsCfg is the CORS config.
|
||||
type CorsCfg struct {
|
||||
Enabled bool
|
||||
Credentials bool
|
||||
AllowedHeaders []string
|
||||
AllowedHosts []string
|
||||
AllowedMethods []string
|
||||
ExposedHeaders []string
|
||||
}
|
||||
|
||||
// Config is the configuration of a WebDAV instance.
|
||||
type Config struct {
|
||||
*User
|
||||
Auth bool
|
||||
NoSniff bool
|
||||
Cors CorsCfg
|
||||
Users map[string]*User
|
||||
LogFormat string
|
||||
}
|
||||
|
||||
// ServeHTTP determines if the request is for this plugins, and if all prerequisites are met.
|
||||
func (c *Config) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
u := c.User
|
||||
requestOrigin := r.Header.Get("Origin")
|
||||
|
||||
// Add CORS headers before any operation so even on a 401 unauthorized status, CORS will work.
|
||||
if c.Cors.Enabled && requestOrigin != "" {
|
||||
headers := w.Header()
|
||||
|
||||
allowedHeaders := strings.Join(c.Cors.AllowedHeaders, ", ")
|
||||
allowedMethods := strings.Join(c.Cors.AllowedMethods, ", ")
|
||||
exposedHeaders := strings.Join(c.Cors.ExposedHeaders, ", ")
|
||||
|
||||
allowAllHosts := len(c.Cors.AllowedHosts) == 1 && c.Cors.AllowedHosts[0] == "*"
|
||||
allowedHost := isAllowedHost(c.Cors.AllowedHosts, requestOrigin)
|
||||
|
||||
if allowAllHosts {
|
||||
headers.Set("Access-Control-Allow-Origin", "*")
|
||||
} else if allowedHost {
|
||||
headers.Set("Access-Control-Allow-Origin", requestOrigin)
|
||||
}
|
||||
|
||||
if allowAllHosts || allowedHost {
|
||||
headers.Set("Access-Control-Allow-Headers", allowedHeaders)
|
||||
headers.Set("Access-Control-Allow-Methods", allowedMethods)
|
||||
|
||||
if c.Cors.Credentials {
|
||||
headers.Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
if len(c.Cors.ExposedHeaders) > 0 {
|
||||
headers.Set("Access-Control-Expose-Headers", exposedHeaders)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r.Method == "OPTIONS" && c.Cors.Enabled && requestOrigin != "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Authentication
|
||||
if c.Auth {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
|
||||
// Gets the correct user for this request.
|
||||
username, password, ok := r.BasicAuth()
|
||||
logger.Verboseln("login attempt", "username = "+username, "remote_address = "+r.RemoteAddr)
|
||||
if !ok {
|
||||
http.Error(w, "Not authorized", 401)
|
||||
return
|
||||
}
|
||||
|
||||
user, ok := c.Users[username]
|
||||
if !ok {
|
||||
http.Error(w, "Not authorized", 401)
|
||||
return
|
||||
}
|
||||
|
||||
if !checkPassword(user.Password, password) {
|
||||
logger.Verboseln("invalid password", "username = "+username, "remote_address = "+r.RemoteAddr)
|
||||
http.Error(w, "Not authorized", 401)
|
||||
return
|
||||
}
|
||||
|
||||
u = user
|
||||
logger.Verboseln("user authorized", "username = "+username)
|
||||
} else {
|
||||
// Even if Auth is disabled, we might want to get
|
||||
// the user from the Basic Auth header. Useful for Caddy
|
||||
// plugins implementation.
|
||||
username, _, ok := r.BasicAuth()
|
||||
if ok {
|
||||
if user, ok := c.Users[username]; ok {
|
||||
u = user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Checks for user permissions relatively to this PATH.
|
||||
noModification := r.Method == "GET" ||
|
||||
r.Method == "HEAD" ||
|
||||
r.Method == "OPTIONS" ||
|
||||
r.Method == "PROPFIND" ||
|
||||
r.Method == "LOCK" ||
|
||||
r.Method == "UNLOCK"
|
||||
//r.Method == "PUT" ||
|
||||
//r.Method == "MOVE" ||
|
||||
//r.Method == "DELETE"
|
||||
|
||||
if !u.Allowed(r.URL.Path, noModification) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "HEAD" {
|
||||
w = newResponseWriterNoBody(w)
|
||||
}
|
||||
|
||||
// Excerpt from RFC4918, section 9.4:
|
||||
//
|
||||
// GET, when applied to a collection, may return the contents of an
|
||||
// "index.html" resource, a human-readable view of the contents of
|
||||
// the collection, or something else altogether.
|
||||
//
|
||||
// Get, when applied to collection, will return the same as PROPFIND method.
|
||||
if r.Method == "GET" && strings.HasPrefix(r.URL.Path, u.Handler.Prefix) {
|
||||
info, err := u.Handler.FileSystem.Stat(context.TODO(), strings.TrimPrefix(r.URL.Path, u.Handler.Prefix))
|
||||
if err == nil && info.IsDir() {
|
||||
r.Method = "PROPFIND"
|
||||
|
||||
if r.Header.Get("Depth") == "" {
|
||||
r.Header.Add("Depth", "1")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Runs the WebDAV.
|
||||
u.Handler.LockSystem = webdav.NewMemLS()
|
||||
u.Handler.ServeHTTP(w, addContextValue(r))
|
||||
}
|
||||
|
||||
// addContextValue 增加context键值对
|
||||
func addContextValue(r *http.Request) *http.Request {
|
||||
// add sessionId
|
||||
ctx := context.WithValue(r.Context(), KeySessionId, r.RemoteAddr)
|
||||
|
||||
// add userId
|
||||
username, _, _ := r.BasicAuth()
|
||||
ctx = context.WithValue(ctx, KeyUserId, username)
|
||||
|
||||
// add context length
|
||||
length := r.ContentLength
|
||||
if length == 0 {
|
||||
contentLength := r.Header.Get("content-length")
|
||||
if contentLength == "" {
|
||||
contentLength = r.Header.Get("X-Expected-Entity-Length")
|
||||
if contentLength == "" {
|
||||
contentLength = "0"
|
||||
}
|
||||
}
|
||||
if cl, ok := strconv.ParseInt(contentLength, 10, 64); ok == nil {
|
||||
length = cl
|
||||
}
|
||||
}
|
||||
ctx = context.WithValue(ctx, KeyContentLength, length)
|
||||
|
||||
req := r.WithContext(ctx)
|
||||
return req
|
||||
}
|
||||
|
||||
// responseWriterNoBody is a wrapper used to suprress the body of the response
|
||||
// to a request. Mainly used for HEAD requests.
|
||||
type responseWriterNoBody struct {
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
// newResponseWriterNoBody creates a new responseWriterNoBody.
|
||||
func newResponseWriterNoBody(w http.ResponseWriter) *responseWriterNoBody {
|
||||
return &responseWriterNoBody{w}
|
||||
}
|
||||
|
||||
// Header executes the Header method from the http.ResponseWriter.
|
||||
func (w responseWriterNoBody) Header() http.Header {
|
||||
return w.ResponseWriter.Header()
|
||||
}
|
||||
|
||||
// Write suprresses the body.
|
||||
func (w responseWriterNoBody) Write(data []byte) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// WriteHeader writes the header to the http.ResponseWriter.
|
||||
func (w responseWriterNoBody) WriteHeader(statusCode int) {
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"golang.org/x/net/webdav"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type WebdavUser struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Scope string `json:"scope"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
type WebdavConfig struct {
|
||||
// 指定Webdav使用哪个账号的云盘资源
|
||||
PanUserId string `json:"panUserId"`
|
||||
PanDriveId string `json:"panDriveId"`
|
||||
PanUser *config.PanUser `json:"-"`
|
||||
UploadChunkSize int `json:"uploadChunkSize"` // 上传文件分片数据块大小,该数值不能太小,建议大于等于512KB
|
||||
TransferUrlType int `json:"transferUrlType"` // 上传/下载URL类别,1-默认,2-阿里云ECS
|
||||
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
Prefix string `json:"prefix"`
|
||||
Users []WebdavUser `json:"users"`
|
||||
}
|
||||
|
||||
func (w *WebdavConfig) StartServer() {
|
||||
users := map[string]*User{}
|
||||
for _, u := range w.Users {
|
||||
fileItem, e := w.PanUser.PanClient().FileInfoByPath(w.PanDriveId, u.Scope)
|
||||
if e != nil {
|
||||
logger.Verboseln("scope not existed, shutting server")
|
||||
return
|
||||
}
|
||||
wdfi := NewWebDavFileInfo(fileItem)
|
||||
if wdfi.fullPath != "/" && strings.Index(wdfi.fullPath, "/") != 0 {
|
||||
wdfi.fullPath = "/" + wdfi.fullPath
|
||||
}
|
||||
panClientProxy := &PanClientProxy{
|
||||
PanUser: w.PanUser,
|
||||
PanDriveId: w.PanDriveId,
|
||||
PanTransferUrlType: w.TransferUrlType,
|
||||
}
|
||||
users[u.Username] = &User{
|
||||
Username: u.Username,
|
||||
Password: u.Password,
|
||||
Scope: u.Scope,
|
||||
Modify: u.Mode == "rw",
|
||||
Rules: nil,
|
||||
Handler: &webdav.Handler{
|
||||
Prefix: w.Prefix,
|
||||
FileSystem: WebDavDir{
|
||||
Dir: webdav.Dir(u.Scope),
|
||||
NoSniff: false,
|
||||
panClientProxy: panClientProxy,
|
||||
fileInfo: wdfi,
|
||||
uploadChunkSize: w.UploadChunkSize,
|
||||
},
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
},
|
||||
}
|
||||
// load & cache root folder info
|
||||
_, _ = panClientProxy.FileListGetAll(u.Scope)
|
||||
}
|
||||
cfg := &Config{
|
||||
Auth: true,
|
||||
NoSniff: false,
|
||||
Cors: CorsCfg{
|
||||
Enabled: false,
|
||||
Credentials: false,
|
||||
},
|
||||
Users: users,
|
||||
LogFormat: "",
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", w.Address+":"+strconv.Itoa(w.Port))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := http.Serve(listener, cfg); err != nil {
|
||||
logger.Verboseln("shutting server", err)
|
||||
}
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"io"
|
||||
"mime"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
// NoSniffFileInfo wraps any generic FileInfo interface and bypasses mime type sniffing.
|
||||
type NoSniffFileInfo struct {
|
||||
os.FileInfo
|
||||
}
|
||||
|
||||
func (w NoSniffFileInfo) ContentType(ctx context.Context) (contentType string, err error) {
|
||||
if mimeType := mime.TypeByExtension(path.Ext(w.FileInfo.Name())); mimeType != "" {
|
||||
// We can figure out the mime from the extension.
|
||||
return mimeType, nil
|
||||
} else {
|
||||
// We can't figure out the mime type without sniffing, call it an octet stream.
|
||||
return "application/octet-stream", nil
|
||||
}
|
||||
}
|
||||
|
||||
// 文件系统
|
||||
type WebDavDir struct {
|
||||
webdav.Dir
|
||||
NoSniff bool
|
||||
panClientProxy *PanClientProxy
|
||||
fileInfo WebDavFileInfo
|
||||
uploadChunkSize int
|
||||
}
|
||||
|
||||
// sliceClean is equivalent to but slightly more efficient than
|
||||
// path.Clean("/" + name).
|
||||
func sliceClean(name string) string {
|
||||
if name == "" || name[0] != '/' {
|
||||
name = "/" + name
|
||||
}
|
||||
return path.Clean(name)
|
||||
}
|
||||
|
||||
// formatAbsoluteName 将name名称更改为绝对路径
|
||||
func (d WebDavDir) formatAbsoluteName(pathStr string) string {
|
||||
if strings.Index(pathStr, "/") != 0 {
|
||||
if d.fileInfo.fullPath != "/" {
|
||||
pathStr = d.fileInfo.fullPath + "/" + pathStr
|
||||
} else {
|
||||
pathStr = d.fileInfo.fullPath + pathStr
|
||||
}
|
||||
}
|
||||
return pathStr
|
||||
}
|
||||
|
||||
func (d WebDavDir) getSessionId(ctx context.Context) string {
|
||||
v := ctx.Value(KeySessionId)
|
||||
if v != nil{
|
||||
return v.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d WebDavDir) getContentLength(ctx context.Context) int64 {
|
||||
v := ctx.Value(KeyContentLength)
|
||||
if v != nil{
|
||||
return v.(int64)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (d WebDavDir) getUserId(ctx context.Context) string {
|
||||
v := ctx.Value(KeyUserId)
|
||||
if v != nil{
|
||||
return v.(string)
|
||||
}
|
||||
return "anonymous"
|
||||
}
|
||||
|
||||
func (d WebDavDir) resolve(name string) string {
|
||||
// This implementation is based on Dir.Open's code in the standard net/http package.
|
||||
if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 ||
|
||||
strings.Contains(name, "\x00") {
|
||||
return ""
|
||||
}
|
||||
dir := string(d.Dir)
|
||||
if dir == "" {
|
||||
dir = "."
|
||||
}
|
||||
return formatPathStyle(filepath.Join(dir, filepath.FromSlash(sliceClean(name))))
|
||||
}
|
||||
|
||||
func (d WebDavDir) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
|
||||
if name = d.resolve(name); name == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
return d.panClientProxy.Mkdir(name, perm)
|
||||
}
|
||||
|
||||
func (d WebDavDir) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
|
||||
if name = d.resolve(name); name == "" {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
name = d.formatAbsoluteName(name)
|
||||
|
||||
//logger.Verbosef("OpenFile file %s flag:\n O_RDONLY=%t\n O_WRONLY=%t\n O_RDWR=%t\n O_APPEND=%t\n O_CREATE=%t\n O_EXCL=%t\n O_SYNC=%t\n O_TRUNC=%t\n",
|
||||
// name,
|
||||
// flag&os.O_RDONLY == 0, flag&os.O_WRONLY != 0, flag&os.O_RDWR != 0, flag&os.O_APPEND != 0,
|
||||
// flag&os.O_CREATE != 0, flag&os.O_EXCL != 0, flag&os.O_SYNC != 0, flag&os.O_TRUNC != 0)
|
||||
|
||||
if name == d.fileInfo.fullPath {
|
||||
return &WebDavFile{
|
||||
panClientProxy: d.panClientProxy,
|
||||
nameSnapshot: d.fileInfo,
|
||||
childrenSnapshot: nil,
|
||||
listPos: 0,
|
||||
readPos: 0,
|
||||
writePos: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if flag&(os.O_SYNC|os.O_APPEND) != 0 {
|
||||
// doesn't support these flags
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
fileSize := d.getContentLength(ctx)
|
||||
if flag&os.O_CREATE != 0 {
|
||||
if flag&os.O_EXCL != 0 {
|
||||
return nil, os.ErrExist
|
||||
}
|
||||
// create file instance for writing
|
||||
_, e := d.panClientProxy.UploadFilePrepare(d.getUserId(ctx), name, fileSize, int64(d.uploadChunkSize))
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
}
|
||||
if flag&(os.O_WRONLY|os.O_RDWR) != 0 && flag&os.O_TRUNC != 0 {
|
||||
// file must be created ready
|
||||
// get ready to write data to file stream
|
||||
logger.Verboseln("get ready to write data to file stream")
|
||||
fus,err2 := d.panClientProxy.UploadFileCache(d.getUserId(ctx), name)
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
|
||||
// check zero-file or not
|
||||
if fileSize == 0 {
|
||||
// zero file, upload and completely
|
||||
d.panClientProxy.UploadFilePart(d.getUserId(ctx), name, 0, []byte{})
|
||||
}
|
||||
|
||||
return &WebDavFile{
|
||||
panClientProxy: d.panClientProxy,
|
||||
nameSnapshot: WebDavFileInfo{
|
||||
fileId: fus.fileId,
|
||||
name: path.Base(fus.filePath),
|
||||
size: fus.fileSize,
|
||||
mode: 0,
|
||||
modTime: time.Unix(fus.timestamp, 0),
|
||||
fullPath: fus.filePath,
|
||||
},
|
||||
childrenSnapshot: nil,
|
||||
listPos: 0,
|
||||
readPos: 0,
|
||||
writePos: fus.fileWritePos,
|
||||
sessionId: d.getSessionId(ctx),
|
||||
userId: d.getUserId(ctx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// default action, open file to read
|
||||
fileItem,e := d.panClientProxy.FileInfoByPath(name)
|
||||
if e != nil {
|
||||
logger.Verboseln("OpenFile failed, file path not existed: " + name)
|
||||
return nil, e
|
||||
}
|
||||
wdfi := NewWebDavFileInfo(fileItem)
|
||||
wdfi.fullPath = d.formatAbsoluteName(name)
|
||||
return &WebDavFile{
|
||||
panClientProxy: d.panClientProxy,
|
||||
nameSnapshot: wdfi,
|
||||
childrenSnapshot: nil,
|
||||
listPos: 0,
|
||||
readPos: 0,
|
||||
writePos: 0,
|
||||
sessionId: d.getSessionId(ctx),
|
||||
userId: d.getUserId(ctx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d WebDavDir) RemoveAll(ctx context.Context, name string) error {
|
||||
if name = d.resolve(name); name == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
if name == filepath.Clean(string(d.Dir)) {
|
||||
// Prohibit removing the virtual root directory.
|
||||
return os.ErrInvalid
|
||||
}
|
||||
return d.panClientProxy.RemoveAll(name)
|
||||
}
|
||||
|
||||
func (d WebDavDir) Rename(ctx context.Context, oldName, newName string) error {
|
||||
if oldName = d.resolve(oldName); oldName == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
if newName = d.resolve(newName); newName == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
if root := filepath.Clean(string(d.Dir)); root == oldName || root == newName {
|
||||
// Prohibit renaming from or to the virtual root directory.
|
||||
return os.ErrInvalid
|
||||
}
|
||||
if path.Dir(oldName) == path.Dir(newName) {
|
||||
// rename
|
||||
return d.panClientProxy.Rename(oldName, newName)
|
||||
} else {
|
||||
// move file
|
||||
return d.panClientProxy.Move(oldName, newName)
|
||||
}
|
||||
}
|
||||
|
||||
func (d WebDavDir) Stat(ctx context.Context, name string) (os.FileInfo, error) {
|
||||
f := &d.fileInfo
|
||||
if name != "" {
|
||||
fileItem,e := d.panClientProxy.FileInfoByPath(d.formatAbsoluteName(name))
|
||||
if e != nil {
|
||||
logger.Verboseln("file path not existed: " + d.formatAbsoluteName(name))
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
*f = NewWebDavFileInfo(fileItem)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// WebDavFile 文件实例
|
||||
type WebDavFile struct {
|
||||
webdav.File
|
||||
|
||||
// 网盘Client
|
||||
panClientProxy *PanClientProxy
|
||||
|
||||
// nameSnapshot 文件关联的网盘文件信息
|
||||
nameSnapshot WebDavFileInfo
|
||||
|
||||
// childrenSnapshot 目录下的子文件信息列表
|
||||
childrenSnapshot []WebDavFileInfo
|
||||
|
||||
listPos int
|
||||
|
||||
// 读取偏移值
|
||||
readPos int64
|
||||
|
||||
// 写入偏移值
|
||||
writePos int64
|
||||
|
||||
// 会话ID
|
||||
sessionId string
|
||||
|
||||
// 用户ID
|
||||
userId string
|
||||
}
|
||||
|
||||
func (f *WebDavFile) Close() error {
|
||||
f.readPos = 0
|
||||
f.writePos = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *WebDavFile) Read(p []byte) (int, error) {
|
||||
count, err := f.panClientProxy.DownloadFilePart(f.sessionId, f.nameSnapshot.fileId, f.readPos, p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
f.readPos += int64(count)
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Readdir 获取文件目录
|
||||
func (f *WebDavFile) Readdir(count int) (fis []os.FileInfo, err error) {
|
||||
if f.childrenSnapshot == nil || len(f.childrenSnapshot) == 0 {
|
||||
fileList, e := f.panClientProxy.FileListGetAll(f.nameSnapshot.fullPath)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
for _,fileItem := range fileList {
|
||||
wdfi := NewWebDavFileInfo(fileItem)
|
||||
wdfi.fullPath = f.nameSnapshot.fullPath + "/" + wdfi.name
|
||||
f.childrenSnapshot = append(f.childrenSnapshot, wdfi)
|
||||
}
|
||||
}
|
||||
|
||||
realCount := count
|
||||
if (f.listPos + realCount) > len(f.childrenSnapshot) {
|
||||
realCount = len(f.childrenSnapshot) - f.listPos
|
||||
}
|
||||
if realCount == 0 {
|
||||
realCount = len(f.childrenSnapshot)
|
||||
}
|
||||
|
||||
fis = make([]os.FileInfo, realCount)
|
||||
idx := 0
|
||||
for idx < realCount {
|
||||
fis[idx] = &f.childrenSnapshot[f.listPos + idx]
|
||||
idx ++
|
||||
}
|
||||
return fis, nil
|
||||
}
|
||||
|
||||
func (f *WebDavFile) Seek(offset int64, whence int) (int64, error) {
|
||||
var abs int64
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
abs = offset
|
||||
logger.Verboseln(f.sessionId + " SeekStart offset = " + strconv.Itoa(int(offset)) + " return offset = " + strconv.Itoa(int(abs)))
|
||||
case io.SeekCurrent:
|
||||
abs = f.readPos + offset
|
||||
logger.Verboseln(f.sessionId + " SeekCurrent offset = " + strconv.Itoa(int(offset)) + " return offset = " + strconv.Itoa(int(abs)))
|
||||
case io.SeekEnd:
|
||||
abs = f.nameSnapshot.size + offset
|
||||
logger.Verboseln(f.sessionId + " SeekEnd offset = " + strconv.Itoa(int(offset)) + " return offset = " + strconv.Itoa(int(abs)))
|
||||
default:
|
||||
return 0, errors.New("invalid whence")
|
||||
}
|
||||
if abs < 0 {
|
||||
return 0, os.ErrInvalid
|
||||
}
|
||||
f.readPos = abs
|
||||
return f.readPos, nil
|
||||
}
|
||||
|
||||
func (f *WebDavFile) Stat() (os.FileInfo, error) {
|
||||
return &f.nameSnapshot, nil
|
||||
}
|
||||
|
||||
func (f *WebDavFile) Write(p []byte) (int, error) {
|
||||
count,err := f.panClientProxy.UploadFilePart(f.userId, f.nameSnapshot.fullPath, f.writePos, p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
f.writePos += int64(count)
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// WebDavFileInfo 文件信息
|
||||
type WebDavFileInfo struct {
|
||||
os.FileInfo
|
||||
fileId string
|
||||
name string
|
||||
size int64
|
||||
mode os.FileMode
|
||||
modTime time.Time
|
||||
fullPath string
|
||||
}
|
||||
|
||||
func NewWebDavFileInfo(fileItem *aliyunpan.FileEntity) WebDavFileInfo {
|
||||
//var LOC, _ = time.LoadLocation("Asia/Shanghai")
|
||||
LOC := time.FixedZone("CST", 8*3600) // 东8区
|
||||
t,_ := time.ParseInLocation("2006-01-02 15:04:05", fileItem.UpdatedAt, LOC)
|
||||
fm := os.FileMode(0)
|
||||
if fileItem.IsFolder() {
|
||||
fm = os.ModeDir
|
||||
}
|
||||
return WebDavFileInfo{
|
||||
fileId: fileItem.FileId,
|
||||
name: fileItem.FileName,
|
||||
size: fileItem.FileSize,
|
||||
mode: fm,
|
||||
modTime: t,
|
||||
fullPath: fileItem.Path,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *WebDavFileInfo) Name() string { return f.name }
|
||||
func (f *WebDavFileInfo) Size() int64 { return f.size }
|
||||
func (f *WebDavFileInfo) Mode() os.FileMode { return f.mode }
|
||||
func (f *WebDavFileInfo) ModTime() time.Time { return f.modTime }
|
||||
func (f *WebDavFileInfo) IsDir() bool { return f.mode.IsDir() }
|
||||
func (f *WebDavFileInfo) Sys() interface{} { return nil }
|
||||
func (f *WebDavFileInfo) ContentType(ctx context.Context) (contentType string, err error) {
|
||||
if mimeType := mime.TypeByExtension(path.Ext(f.Name())); mimeType != "" {
|
||||
// We can figure out the mime from the extension.
|
||||
return mimeType, nil
|
||||
} else {
|
||||
// We can't figure out the mime type without sniffing, call it an octet stream.
|
||||
return "application/octet-stream", nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package nets
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type NetInterfaceInfoList []*NetInterfaceInfo
|
||||
type NetInterfaceInfo struct {
|
||||
Name string `json:"name"`
|
||||
Mac string `json:"mac"`
|
||||
IPv4 string `json:"ipv4"`
|
||||
IPv6 string `json:"ipv6"`
|
||||
}
|
||||
|
||||
func (n *NetInterfaceInfoList) GetByName(name string) *NetInterfaceInfo {
|
||||
for _, v := range *n {
|
||||
if v.Name == name {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLocalNetInterfaceAddress 获取本地接口地址信息
|
||||
func GetLocalNetInterfaceAddress() (NetInterfaceInfoList, error) {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
netList := NetInterfaceInfoList{}
|
||||
for _, inter := range interfaces {
|
||||
netInfo := &NetInterfaceInfo{
|
||||
Name: inter.Name,
|
||||
Mac: inter.HardwareAddr.String(),
|
||||
IPv4: "",
|
||||
IPv6: "",
|
||||
}
|
||||
addrs, err2 := inter.Addrs()
|
||||
if err2 != nil {
|
||||
continue
|
||||
}
|
||||
for _, address := range addrs {
|
||||
if ipnet, ok := address.(*net.IPNet); ok {
|
||||
if ipnet.IP.To4() != nil { // ipv4
|
||||
if ipnet.IP.String() != "" {
|
||||
netInfo.IPv4 = ipnet.IP.String()
|
||||
}
|
||||
} else if ipnet.IP.To16() != nil && !ipnet.IP.IsLoopback() { // ipv6
|
||||
if ipnet.IP.String() != "" && !strings.HasPrefix(ipnet.IP.String(), "fe80") { // 去掉本地IPv6地址
|
||||
netInfo.IPv6 = ipnet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
netList = append(netList, netInfo)
|
||||
}
|
||||
return netList, nil
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -48,7 +48,7 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
//NewDownloadStatus 初始化DownloadStatus
|
||||
// NewDownloadStatus 初始化DownloadStatus
|
||||
func NewDownloadStatus() *DownloadStatus {
|
||||
return &DownloadStatus{
|
||||
startTime: time.Now(),
|
||||
@@ -60,22 +60,22 @@ func (ds *DownloadStatus) SetRateLimit(rl *speeds.RateLimit) {
|
||||
ds.rateLimit = rl
|
||||
}
|
||||
|
||||
//SetTotalSize 返回总大小
|
||||
// SetTotalSize 设置总大小
|
||||
func (ds *DownloadStatus) SetTotalSize(size int64) {
|
||||
ds.totalSize = size
|
||||
}
|
||||
|
||||
//AddDownloaded 增加已下载数据量
|
||||
// AddDownloaded 增加已下载数据量
|
||||
func (ds *DownloadStatus) AddDownloaded(d int64) {
|
||||
atomic.AddInt64(&ds.downloaded, d)
|
||||
}
|
||||
|
||||
//AddTotalSize 增加总大小 (不支持多线程)
|
||||
// AddTotalSize 增加总大小 (不支持多线程)
|
||||
func (ds *DownloadStatus) AddTotalSize(size int64) {
|
||||
ds.totalSize += size
|
||||
}
|
||||
|
||||
//AddSpeedsDownloaded 增加已下载数据量, 用于统计速度
|
||||
// AddSpeedsDownloaded 增加已下载数据量, 用于统计速度
|
||||
func (ds *DownloadStatus) AddSpeedsDownloaded(d int64) {
|
||||
if ds.rateLimit != nil {
|
||||
ds.rateLimit.Add(d)
|
||||
@@ -83,24 +83,24 @@ func (ds *DownloadStatus) AddSpeedsDownloaded(d int64) {
|
||||
ds.speedsStat.Add(d)
|
||||
}
|
||||
|
||||
//SetMaxSpeeds 设置最大速度, 原子操作
|
||||
// SetMaxSpeeds 设置最大速度, 原子操作
|
||||
func (ds *DownloadStatus) SetMaxSpeeds(speeds int64) {
|
||||
if speeds > atomic.LoadInt64(&ds.maxSpeeds) {
|
||||
atomic.StoreInt64(&ds.maxSpeeds, speeds)
|
||||
}
|
||||
}
|
||||
|
||||
//ClearMaxSpeeds 清空统计最大速度, 原子操作
|
||||
// ClearMaxSpeeds 清空统计最大速度, 原子操作
|
||||
func (ds *DownloadStatus) ClearMaxSpeeds() {
|
||||
atomic.StoreInt64(&ds.maxSpeeds, 0)
|
||||
}
|
||||
|
||||
//TotalSize 返回总大小
|
||||
// TotalSize 返回总大小
|
||||
func (ds *DownloadStatus) TotalSize() int64 {
|
||||
return ds.totalSize
|
||||
}
|
||||
|
||||
//Downloaded 返回已下载数据量
|
||||
// Downloaded 返回已下载数据量
|
||||
func (ds *DownloadStatus) Downloaded() int64 {
|
||||
return atomic.LoadInt64(&ds.downloaded)
|
||||
}
|
||||
@@ -110,22 +110,22 @@ func (ds *DownloadStatus) UpdateSpeeds() {
|
||||
atomic.StoreInt64(&ds.tmpSpeeds, ds.speedsStat.GetSpeeds())
|
||||
}
|
||||
|
||||
//SpeedsPerSecond 返回每秒速度
|
||||
// SpeedsPerSecond 返回每秒速度
|
||||
func (ds *DownloadStatus) SpeedsPerSecond() int64 {
|
||||
return atomic.LoadInt64(&ds.tmpSpeeds)
|
||||
}
|
||||
|
||||
//MaxSpeeds 返回最大速度
|
||||
// MaxSpeeds 返回最大速度
|
||||
func (ds *DownloadStatus) MaxSpeeds() int64 {
|
||||
return atomic.LoadInt64(&ds.maxSpeeds)
|
||||
}
|
||||
|
||||
//TimeElapsed 返回花费的时间
|
||||
// TimeElapsed 返回花费的时间
|
||||
func (ds *DownloadStatus) TimeElapsed() (elapsed time.Duration) {
|
||||
return time.Since(ds.startTime)
|
||||
}
|
||||
|
||||
//TimeLeft 返回预计剩余时间
|
||||
// TimeLeft 返回预计剩余时间
|
||||
func (ds *DownloadStatus) TimeLeft() (left time.Duration) {
|
||||
speeds := atomic.LoadInt64(&ds.tmpSpeeds)
|
||||
if speeds <= 0 {
|
||||
|
||||
@@ -15,9 +15,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -30,9 +28,12 @@ import (
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/peterh/liner"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdliner"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdliner/args"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdutil"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdutil/escaper"
|
||||
"github.com/tickstep/aliyunpan/internal/command"
|
||||
@@ -51,7 +52,7 @@ const (
|
||||
|
||||
var (
|
||||
// Version 版本号
|
||||
Version = "v0.2.9"
|
||||
Version = "v0.3.2"
|
||||
|
||||
// 命令历史文件
|
||||
historyFilePath = filepath.Join(config.GetConfigDir(), "aliyunpan_command_history.txt")
|
||||
@@ -61,7 +62,12 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
config.AppVersion = Version
|
||||
b, ok := os.LookupEnv("ALIYUNPAN_NONE_OPENAPI")
|
||||
if ok && b == "1" {
|
||||
global.IsSupportNoneOpenApiCommands = true
|
||||
}
|
||||
|
||||
global.AppVersion = Version
|
||||
cmdutil.ChWorkDir()
|
||||
|
||||
err := config.Config.Init()
|
||||
@@ -83,7 +89,8 @@ func checkLoginExpiredAndRelogin() {
|
||||
command.TryLogin()
|
||||
} else {
|
||||
// 刷新过期Token并保存到配置文件
|
||||
command.RefreshTokenInNeed(activeUser, config.Config.DeviceName)
|
||||
command.RefreshWebTokenInNeed(activeUser, config.Config.DeviceName)
|
||||
command.RefreshOpenTokenInNeed(activeUser)
|
||||
}
|
||||
command.SaveConfigFunc(nil)
|
||||
}
|
||||
@@ -100,7 +107,13 @@ func main() {
|
||||
// Token刷新进程,不管是CLI命令行模式,还是直接命令模式,本刷新任务都会执行
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
//time.Sleep(time.Duration(5) * time.Second)
|
||||
checkLoginExpiredAndRelogin()
|
||||
for _, u := range config.Config.UserList {
|
||||
if u.UserId == config.Config.ActiveUID {
|
||||
if u.PanClient() != nil {
|
||||
checkLoginExpiredAndRelogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -147,7 +160,7 @@ func main() {
|
||||
|
||||
os.Setenv(config.EnvVerbose, c.String("verbose"))
|
||||
isCli = true
|
||||
config.IsAppInCliMode = true
|
||||
global.IsAppInCliMode = true
|
||||
logger.Verbosef("提示: 你已经开启VERBOSE调试日志\n\n")
|
||||
|
||||
var (
|
||||
@@ -172,7 +185,7 @@ func main() {
|
||||
lineArgs = args.Parse(line)
|
||||
numArgs = len(lineArgs)
|
||||
acceptCompleteFileCommands = []string{
|
||||
"cd", "cp", "xcp", "download", "ls", "mkdir", "mv", "pwd", "rename", "rm", "share", "upload", "login", "loglist", "logout",
|
||||
"cd", "cp", "xcp", "download", "ls", "mkdir", "mv", "pwd", "rename", "rm", "share", "save", "upload", "login", "loglist", "logout",
|
||||
"clear", "quit", "exit", "quota", "who", "sign", "update", "who", "su", "config",
|
||||
"drive", "export", "import", "sync", "tree",
|
||||
}
|
||||
@@ -262,6 +275,8 @@ func main() {
|
||||
targetDir = path.Dir(targetDir)
|
||||
}
|
||||
}
|
||||
|
||||
// tab键补全路径
|
||||
files, err := activeUser.CacheFilesDirectoriesList(targetDir)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -309,9 +324,9 @@ func main() {
|
||||
// check update
|
||||
command.ReloadConfigFunc(c)
|
||||
if config.Config.UpdateCheckInfo.LatestVer != "" {
|
||||
if utils.ParseVersionNum(config.Config.UpdateCheckInfo.LatestVer) > utils.ParseVersionNum(config.AppVersion) {
|
||||
if utils.ParseVersionNum(config.Config.UpdateCheckInfo.LatestVer) > utils.ParseVersionNum(global.AppVersion) {
|
||||
fmt.Printf("\n当前的软件版本为:%s, 现在有新版本 %s 可供更新,强烈推荐进行更新!(可以输入 update 命令进行更新)\n\n",
|
||||
config.AppVersion, config.Config.UpdateCheckInfo.LatestVer)
|
||||
global.AppVersion, config.Config.UpdateCheckInfo.LatestVer)
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
@@ -414,9 +429,6 @@ func main() {
|
||||
// 获取当前帐号空间配额 quota
|
||||
command.CmdQuota(),
|
||||
|
||||
// Token操作
|
||||
command.CmdToken(),
|
||||
|
||||
// 切换工作目录 cd
|
||||
command.CmdCd(),
|
||||
|
||||
@@ -435,11 +447,8 @@ func main() {
|
||||
// 删除文件/目录 rm
|
||||
command.CmdRm(),
|
||||
|
||||
//// 拷贝文件/目录 cp
|
||||
//command.CmdCp(),
|
||||
|
||||
// 备份盘和资源库之间拷贝文件 xcp
|
||||
command.CmdXcp(),
|
||||
// 复制文件/目录 cp
|
||||
command.CmdCp(),
|
||||
|
||||
// 移动文件/目录 mv
|
||||
command.CmdMv(),
|
||||
@@ -447,45 +456,21 @@ func main() {
|
||||
// 重命名文件 rename
|
||||
command.CmdRename(),
|
||||
|
||||
// 分享文件/目录 share
|
||||
command.CmdShare(),
|
||||
|
||||
// 同步备份 sync
|
||||
command.CmdSync(),
|
||||
|
||||
// 上传文件/目录 upload
|
||||
command.CmdUpload(),
|
||||
|
||||
// 手动秒传
|
||||
//command.CmdRapidUpload(),
|
||||
|
||||
// 下载文件/目录 download
|
||||
command.CmdDownload(),
|
||||
|
||||
// 获取文件下载链接
|
||||
//command.CmdLocateUrl(),
|
||||
|
||||
// 导出文件/目录元数据 export
|
||||
//command.CmdExport(),
|
||||
|
||||
// 导入文件 import
|
||||
//command.CmdImport(),
|
||||
|
||||
// webdav服务(depressed)
|
||||
//command.CmdWebdav(),
|
||||
|
||||
// 回收站
|
||||
command.CmdRecycle(),
|
||||
|
||||
// 显示和修改程序配置项 config
|
||||
command.CmdConfig(),
|
||||
|
||||
// 工具箱 tool
|
||||
command.CmdTool(),
|
||||
|
||||
// 相簿
|
||||
command.CmdAlbum(),
|
||||
|
||||
// 显示命令历史
|
||||
{
|
||||
Name: "history",
|
||||
@@ -660,6 +645,26 @@ func main() {
|
||||
//},
|
||||
}
|
||||
|
||||
// 隐藏不支持的命令
|
||||
if global.IsSupportNoneOpenApiCommands {
|
||||
hiddenCommands := []cli.Command{
|
||||
// 备份盘和资源库之间拷贝文件 xcp
|
||||
command.CmdXcp(),
|
||||
|
||||
// 分享文件/目录 share
|
||||
command.CmdShare(),
|
||||
|
||||
// 保存分享文件/目录 save
|
||||
command.CmdSave(),
|
||||
|
||||
// 回收站
|
||||
command.CmdRecycle(),
|
||||
|
||||
// 相簿
|
||||
//command.CmdAlbum(),
|
||||
}
|
||||
app.Commands = append(app.Commands, hiddenCommands...)
|
||||
}
|
||||
sort.Sort(cli.FlagsByName(app.Flags))
|
||||
sort.Sort(cli.CommandsByName(app.Commands))
|
||||
app.Run(os.Args)
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
"FixedFileInfo": {
|
||||
"FileVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 2,
|
||||
"Patch": 9,
|
||||
"Minor": 3,
|
||||
"Patch": 2,
|
||||
"Build": 0
|
||||
},
|
||||
"ProductVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 2,
|
||||
"Patch": 9,
|
||||
"Minor": 3,
|
||||
"Patch": 2,
|
||||
"Build": 0
|
||||
},
|
||||
"FileFlagsMask": "3f",
|
||||
@@ -22,14 +22,14 @@
|
||||
"Comments": "",
|
||||
"CompanyName": "tickstep",
|
||||
"FileDescription": "阿里云盘客户端",
|
||||
"FileVersion": "v0.2.9",
|
||||
"FileVersion": "v0.3.2",
|
||||
"InternalName": "",
|
||||
"LegalCopyright": "© 2021-2024 tickstep.",
|
||||
"LegalTrademarks": "",
|
||||
"OriginalFilename": "",
|
||||
"PrivateBuild": "",
|
||||
"ProductName": "aliyunpan",
|
||||
"ProductVersion": "v0.2.9",
|
||||
"ProductVersion": "v0.3.2",
|
||||
"SpecialBuild": ""
|
||||
},
|
||||
"VarFileInfo": {
|
||||
|
||||