mirror of
https://gitee.com/zhangsken/aliyunpan.git
synced 2026-10-16 10:29:42 +08:00
Compare commits
207 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 238d692f73 | |||
| b85be20cba | |||
| a283320eff | |||
| c9eb8e06db | |||
| bd8b1f43e6 | |||
| 65b0dedb9b | |||
| f3b1b84439 | |||
| fe370ca953 | |||
| 43f7883155 | |||
| 2668e79564 | |||
| 25c550b4d8 | |||
| 4c5f217257 | |||
| 93b55014d0 | |||
| 7307c7f37f | |||
| 955a563b94 | |||
| 666842f972 | |||
| fe22d18aa7 | |||
| 66d4b14ff9 | |||
| 08c9d58ef8 | |||
| 4e7545e6e6 | |||
| ee9c5db8b3 | |||
| 9c965b5b64 | |||
| 83d2895f78 | |||
| e8c86672ff | |||
| cb4c9fbeb7 | |||
| 0c80fca74e | |||
| 3faf4d3f43 | |||
| d7e4772417 | |||
| 8b1515c2cf | |||
| fa3ffeefd9 | |||
| e8f73e5a37 | |||
| 1cf3db39b1 | |||
| 15bef9fcc3 | |||
| 3e1b87f5f2 | |||
| 5c19a07a31 | |||
| 5782bd7cd9 | |||
| a8850e2479 | |||
| 7978788ccd | |||
| 8a6723cb06 | |||
| f303cc7338 | |||
| a2afb9990c | |||
| 9016c12c26 | |||
| d0ad94aa65 | |||
| 594e635cde | |||
| 6787269751 | |||
| 5f10365604 | |||
| 0a7f520a8c | |||
| 2d3874ea7f | |||
| 2e35149ac5 | |||
| 74e5a6f409 | |||
| f98d201f1e | |||
| aa17816ed1 | |||
| 0668e743f5 | |||
| d8da7cefc3 | |||
| bf75461a8b | |||
| 8f7d4920ae | |||
| fa475768fa | |||
| c74848b5bc | |||
| 83914ad5e1 | |||
| 43f4b23981 | |||
| 4833c787ac | |||
| f13177214e | |||
| d90894898f | |||
| ac5eb94305 | |||
| ba3ae4028b | |||
| 416b2e2e6a | |||
| 23fa29ef54 | |||
| 6031beee6d | |||
| 5f62a93ce4 | |||
| 811ebdf805 | |||
| a218f47f9e | |||
| b22a79fdb2 | |||
| 8456aa7b35 | |||
| 797239361c | |||
| b3a7a375d0 | |||
| 3fa8c3eee4 | |||
| 8dd7c6b7cd | |||
| 99cfb7d09d | |||
| bf4027601c | |||
| 714c912c54 | |||
| ab49739513 | |||
| b4e405d0f8 | |||
| 6ce355ead8 | |||
| 884d8d356c | |||
| dd4f8931bf | |||
| f6daf215b9 | |||
| 7d0da94671 | |||
| 95dcd3307f | |||
| d376c19ede | |||
| b8949b11f0 | |||
| e8ca54cf4b | |||
| 8632c268f0 | |||
| 1c04ab9c7b | |||
| 779f21de65 | |||
| 7b720dbb5e | |||
| a867359da3 | |||
| f003ff9f44 | |||
| 84a833db5b | |||
| b64c07f079 | |||
| ac93c7e184 | |||
| bc2ab0625f | |||
| 3d1aa1c8bb | |||
| 6b0c815597 | |||
| a914eb5338 | |||
| d707895acc | |||
| 5b877c8fd8 | |||
| 8db035d4e5 | |||
| 5efbd9c306 | |||
| 6c1ab9d6e0 | |||
| dd72192db6 | |||
| 621925709d | |||
| 57e2ecdb72 | |||
| 6669b64027 | |||
| 117a4f73db | |||
| ed11869074 | |||
| 379ce404e4 | |||
| b36b14e03a | |||
| 8ac48bbda5 | |||
| ba407cb74b | |||
| 3b802d5cc6 | |||
| 09051e1e7a | |||
| f458dc5353 | |||
| 1d8e16f5c9 | |||
| 3ef5e67d76 | |||
| a88487dffd | |||
| 855b640d9f | |||
| 911b07cc8a | |||
| bc86c25549 | |||
| 13daf9480b | |||
| 9f1e565dae | |||
| e5bf8da4de | |||
| 37f3b67002 | |||
| 86ae72f76e | |||
| 5a018dd4ce | |||
| 09740df140 | |||
| b5dde87145 | |||
| d1836d3df8 | |||
| 38a7f63cc6 | |||
| 350c879f66 | |||
| 12a598015f | |||
| b4049b35e5 | |||
| 1b2c8ae1ce | |||
| 232ab535ed | |||
| 979b3d9bde | |||
| debaf655f0 | |||
| f1c6379e33 | |||
| f692e2d18b | |||
| d0e7715f4d | |||
| dbae151f22 | |||
| ad5c44aaf9 | |||
| 2ec8592685 | |||
| f804a42a19 | |||
| ab6290e7a3 | |||
| 8955eeec92 | |||
| f2bcc977cc | |||
| 437c2e31b9 | |||
| 25d7ff406c | |||
| 37f4bf05e4 | |||
| a582cba76a | |||
| f9dbb6ffa2 | |||
| d7434d29a6 | |||
| 7f4675331f | |||
| 24765ba2af | |||
| 01b9929c4b | |||
| 557b36617a | |||
| 232155c5f3 | |||
| 847546424b | |||
| 8e422c85a4 | |||
| 7c6145aee4 | |||
| b645f6caba | |||
| 45c4a30f22 | |||
| 594ae354e1 | |||
| 2da542fc22 | |||
| 52fbe51499 | |||
| 90cec4b827 | |||
| f6bdb4ae60 | |||
| 85c9cf0ecb | |||
| 32b57bf71f | |||
| 9c7f9a81f6 | |||
| c8404260cc | |||
| 24083e190f | |||
| 311f1fdd39 | |||
| ab15ddf54e | |||
| b0a2ee41ae | |||
| 6c89a01900 | |||
| 5a5ab464be | |||
| 8750761aee | |||
| a318796f3b | |||
| 5572f844ed | |||
| 0887b9f160 | |||
| 182c28fdb3 | |||
| 56a06d495e | |||
| c1115dfcc7 | |||
| 70443455de | |||
| e8c370cc86 | |||
| e2fd95c557 | |||
| 85eb4ce2c4 | |||
| 37e7bd07cf | |||
| cc1fc526ef | |||
| 9bffdc1cff | |||
| b910776209 | |||
| ebbba140e3 | |||
| b6b71b45e3 | |||
| ea82ec8320 | |||
| 04e414b65d | |||
| 7e0d2155a8 | |||
| 35ca753ad6 |
+3
-1
@@ -41,4 +41,6 @@ main_test.go
|
||||
license_header.sh
|
||||
account.txt
|
||||
git_push.sh
|
||||
git_pull.sh
|
||||
git_pull.sh
|
||||
docker/sync/data
|
||||
clear_docker.sh
|
||||
@@ -1,27 +1,48 @@
|
||||
# 关于
|
||||
阿里云盘CLI。仿 Linux shell 文件处理命令的阿里云盘命令行客户端,支持webdav文件协议。
|
||||
阿里云盘CLI。仿 Linux shell 文件处理命令的阿里云盘命令行客户端,支持JavaScript插件,支持webdav文件协议,支持同步备份功能。
|
||||
|
||||
# 特色
|
||||
1. 多平台支持, 支持 Windows, macOS, linux(x86/x64/arm), android, iOS 等
|
||||
2. 阿里云盘多用户支持
|
||||
3. 支持文件网盘,相册网盘无缝切换
|
||||
4. ~~支持导入/导出功能,快速备份(导出)和恢复(导入)网盘的文件~~
|
||||
5. [下载](#下载文件目录)网盘内文件, 支持多个文件或目录下载, 支持断点续传和单文件并行下载
|
||||
6. [上传](#上传文件目录)本地文件, 支持多个文件或目录上传,支持排除指定文件夹/文件(正则表达式)功能
|
||||
7. [备份本地文件](#备份本地文件目录)支持备份本地文件夹到网盘中,保持本地文件和网盘文件同步。常用于嵌入式或者NAS等设备。
|
||||
8. 命令和文件路径输入支持Tab键自动补全
|
||||
9. 支持阿里云ECS环境下使用内网链接上传/下载,速度更快(只支持阿里经典网络,最高可达100MB/s),还可以节省公网带宽流量(配置transfer_url_type=2即可)
|
||||
10. 支持webdav文件协议,可以将阿里云盘当做webdav文件网盘挂载到Windows, macOS, linux的磁盘中进行使用。webdav部署支持docker镜像,镜像只有不到10MB非常小巧。
|
||||
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. 支持[webdav文件服务](docs/manual.md#webdav文件服务),可以将阿里云盘当做webdav文件网盘挂载到Windows, macOS, linux的磁盘中进行使用。webdav部署支持docker镜像,镜像只有不到10MB非常小巧。
|
||||
10. 支持[JavaScript插件](docs/manual.md#JavaScript插件),你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求
|
||||
|
||||
# 重要提示
|
||||
***由于阿里上传接口更改,目前上传文件需要原始文件的片段信息,故之前的"手动秒传","导入","导出","秒传链接分享"功能全部无法使用。新版本暂时屏蔽了该功能,请知悉。***
|
||||
# 目录
|
||||
- [如何安装](#如何安装)
|
||||
* [直接下载安装](#直接下载安装)
|
||||
* [apt安装](#apt安装)
|
||||
* [yum安装](#yum安装)
|
||||
* [docker安装](#docker安装)
|
||||
+ [sync同步盘](#sync同步盘)
|
||||
+ [webdav共享盘](#webdav共享盘)
|
||||
- [如何使用](#如何使用)
|
||||
* [基本使用](#基本使用)
|
||||
+ [修改配置目录](#修改配置目录)
|
||||
+ [启动程序](#启动程序)
|
||||
+ [查看帮助](#查看帮助)
|
||||
+ [登录](#登录)
|
||||
+ [查看文件列表](#查看文件列表)
|
||||
+ [下载文件](#下载文件)
|
||||
+ [上传文件](#上传文件)
|
||||
+ [同步备份文件](#同步备份文件)
|
||||
* [更多命令](#更多命令)
|
||||
- [常见问题](#常见问题)
|
||||
* [如何获取RefreshToken](#如何获取RefreshToken)
|
||||
* [如何开启Debug调试日志](#如何开启Debug调试日志)
|
||||
- [交流反馈](#交流反馈)
|
||||
- [鸣谢](#鸣谢)
|
||||
|
||||
# 相关说明
|
||||
1. 本项目还处于开发阶段,未经过充分的测试,如有bug或者好的建议欢迎提交issue
|
||||
2. 由于阿里云盘还在内测中,后面功能和接口随时会被修改,相对应的,本工具相关功能也会被影响
|
||||
3. 目前阶段优先处理Bug,功能增强的开发会相对往后安排
|
||||
|
||||
# 版本标签说明
|
||||
# 如何安装
|
||||
## 直接下载安装
|
||||
可以直接在本仓库 [发布页](https://github.com/tickstep/aliyunpan/releases) 下载安装包,解压后使用。
|
||||
|
||||
要特别注意安装包的标签,不同的标签对应不同架构的系统,相关版本文件的标签说明如下:
|
||||
1. arm / armv5 / armv7 : 适用32位ARM系统
|
||||
2. arm64 : 适用64位ARM系统
|
||||
3. 386 / x86 : 适用32系统,包括Intel和AMD的CPU系统
|
||||
@@ -30,716 +51,244 @@
|
||||
6. macOS amd64适用Intel CPU的机器,macOS arm64目前主要是适用苹果M1芯片的机器
|
||||
7. iOS arm64适用iPhone手机,并且必须是越狱的手机才能正常运行
|
||||
|
||||
# 目录
|
||||
- [特色](#特色)
|
||||
- [下载/运行 说明](#下载运行说明)
|
||||
* [Windows](#windows)
|
||||
* [Linux / macOS](#linux--macos)
|
||||
- [命令列表及说明](#命令列表及说明)
|
||||
* [注意](#注意)
|
||||
* [修改配置文件存储路径](#修改配置文件存储路径)
|
||||
* [检测程序更新](#检测程序更新)
|
||||
* [查看帮助](#查看帮助)
|
||||
* [登录阿里云盘帐号](#登录阿里云盘帐号)
|
||||
* [列出帐号列表](#列出帐号列表)
|
||||
* [获取当前帐号](#获取当前帐号)
|
||||
* [切换阿里云盘帐号](#切换阿里云盘帐号)
|
||||
* [退出阿里云盘帐号](#退出阿里云盘帐号)
|
||||
* [刷新Token](#刷新Token)
|
||||
* [切换网盘(文件/相册)](#切换网盘)
|
||||
* [获取网盘配额](#获取网盘配额)
|
||||
* [切换工作目录](#切换工作目录)
|
||||
* [输出工作目录](#输出工作目录)
|
||||
* [列出目录](#列出目录)
|
||||
* [下载文件/目录](#下载文件目录)
|
||||
* [上传文件/目录](#上传文件目录)
|
||||
* [备份本地文件/目录](#备份本地文件目录)
|
||||
* [手动秒传文件](#手动秒传文件)
|
||||
* [创建目录](#创建目录)
|
||||
* [删除文件/目录](#删除文件目录)
|
||||
* [移动文件/目录](#移动文件目录)
|
||||
* [重命名文件/目录](#重命名文件目录)
|
||||
* [导出文件](#导出文件)
|
||||
* [导入文件](#导入文件)
|
||||
* [分享文件/目录](#分享文件目录)
|
||||
+ [设置分享文件/目录](#设置分享文件目录)
|
||||
+ [列出已分享文件/目录](#列出已分享文件目录)
|
||||
+ [取消分享文件/目录](#取消分享文件目录)
|
||||
+ [分享秒传链接](#分享秒传链接)
|
||||
* [webdav文件服务](#webdav文件服务)
|
||||
+ [常用命令说明](#常用命令说明)
|
||||
+ [命令行启动](#命令行启动)
|
||||
+ [Linux后台启动](#Linux后台启动)
|
||||
+ [Docker运行](#Docker运行)
|
||||
+ [HTTPS配置](#HTTPS配置)
|
||||
* [显示和修改程序配置项](#显示和修改程序配置项)
|
||||
- [常见问题Q&A](#常见问题Q&A)
|
||||
* [1. 如何获取RefreshToken](#1-如何获取RefreshToken)
|
||||
* [2. 如何开启Debug调试日志](#2-如何开启Debug调试日志)
|
||||
* [3. 解决 time: missing Location in call to Date 问题](#3-解决-missing-Location-in-call-to-Date-问题)
|
||||
- [交流反馈](#交流反馈)
|
||||
- [鸣谢](#鸣谢)
|
||||
|
||||
# 下载/运行说明
|
||||
|
||||
可以直接在[发布页](https://github.com/tickstep/aliyunpan/releases)下载使用.
|
||||
|
||||
也可以使用这个天翼云盘分享链接(阿里云盘还不支持zip压缩包分享)进行下载:[下载链接](https://cloud.189.cn/t/z2auuiFfYb2i)(访问码:5o9b)
|
||||
|
||||
如果程序运行时输出乱码, 请检查下终端的编码方式是否为 `UTF-8`.
|
||||
|
||||
使用本程序之前, 非常建议先学习一些 linux 基础命令知识.
|
||||
|
||||
如果没有带任何参数运行程序, 程序将会进入仿Linux shell系统用户界面的cli交互模式, 可直接运行相关命令.
|
||||
|
||||
cli交互模式下, 光标所在行的前缀应为 `aliyunpan >`, 如果登录了帐号则格式为 `aliyunpan:<工作目录> <用户昵称>$ `
|
||||
|
||||
程序会提供相关命令的使用说明.
|
||||
|
||||
## Windows
|
||||
|
||||
程序应在 命令提示符 (Command Prompt) 或 PowerShell 中运行.
|
||||
|
||||
也可直接双击程序运行, 具体使用方法请参见 [命令列表及说明](#命令列表及说明).
|
||||
|
||||
## Linux / macOS
|
||||
|
||||
程序应在 终端 (Terminal) 运行.
|
||||
|
||||
具体使用方法请参见 [命令列表及说明](#命令列表及说明) .
|
||||
|
||||
|
||||
# 命令列表及说明
|
||||
|
||||
## 注意
|
||||
|
||||
命令的前缀 `aliyunpan` 为指向程序运行的全路径名 (ARGv 的第一个参数)
|
||||
|
||||
直接运行程序时, 未带任何其他参数, 则程序进入cli交互模式, 进入cli模式运行以下命令时要把命令的前缀 `aliyunpan` 去掉! 即不需要输入`aliyunpan`。
|
||||
|
||||
cli交互模式支持按tab键自动补全命令.
|
||||
|
||||
## 修改配置文件存储路径
|
||||
设置环境变量ALIYUNPAN_CONFIG_DIR并指定一个存在目录即可,注意目录需要是绝对路径
|
||||
参考例子:
|
||||
```shell
|
||||
wget https://github.com/tickstep/aliyunpan/releases/download/v0.2.2/aliyunpan-v0.2.2-linux-amd64.zip
|
||||
unzip aliyunpan-v0.2.2-linux-amd64.zip
|
||||
cd aliyunpan-v0.2.2-linux-amd64
|
||||
./aliyunpan
|
||||
```
|
||||
例如linux下面可以这样指定
|
||||
|
||||
## apt安装
|
||||
适用于apt包管理器的系统,例如Ubuntu,国产deepin深度操作系统等。目前只支持amd64和arm64架构的机器。
|
||||
```shell
|
||||
sudo curl -fsSL http://file.tickstep.com/apt/pgp | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/tickstep-packages-archive-keyring.gpg > /dev/null && echo "deb [signed-by=/etc/apt/trusted.gpg.d/tickstep-packages-archive-keyring.gpg] http://file.tickstep.com/apt aliyunpan main" | sudo tee /etc/apt/sources.list.d/tickstep-aliyunpan.list > /dev/null && sudo apt-get update && sudo apt-get install -y aliyunpan
|
||||
|
||||
```
|
||||
|
||||
## yum安装
|
||||
适用于yum包管理器的系统,例如CentOS、RockyLinux等。目前只支持amd64和arm64架构的机器。
|
||||
```shell
|
||||
sudo curl -fsSL http://file.tickstep.com/rpm/aliyunpan/aliyunpan.repo | sudo tee /etc/yum.repos.d/tickstep-aliyunpan.repo > /dev/null && sudo yum install aliyunpan -y
|
||||
|
||||
```
|
||||
|
||||
## docker安装
|
||||
### sync同步盘
|
||||
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份三种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
备份功能支持以下三种模式:
|
||||
1. 备份本地文件,即上传本地文件到网盘,始终保持本地文件有一个完整的备份在网盘
|
||||
2. 备份云盘文件,即下载网盘文件到本地,始终保持网盘的文件有一个完整的备份在本地
|
||||
3. 双向备份,保持网盘文件和本地文件严格一致
|
||||
```
|
||||
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.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步骤,然后再正常启动文件同步任务
|
||||
```
|
||||
更详情文档请参考dockerhub网址:[tickstep/aliyunpan-sync](https://hub.docker.com/r/tickstep/aliyunpan-sync)
|
||||
|
||||
### webdav共享盘
|
||||
让阿里云盘变身为webdav协议的文件服务器。这样使用webdav客户端软件,你可以把阿里云盘挂载为Windows、Linux、Mac系统的磁盘,可以通过NAS系统做文件管理或文件同步等等。
|
||||
```
|
||||
docker run -d --name=aliyunpan-webdav --restart=always -p 23077:23077 -e TZ="Asia/Shanghai" -e ALIYUNPAN_REFRESH_TOKEN="<your refreshToken>" -e ALIYUNPAN_AUTH_USER="admin" -e ALIYUNPAN_AUTH_PASSWORD="admin" -e ALIYUNPAN_WEBDAV_MODE="rw" -e ALIYUNPAN_PAN_DRIVE="File" -e ALIYUNPAN_PAN_DIR="/" tickstep/aliyunpan-webdav:v0.2.2
|
||||
|
||||
|
||||
ALIYUNPAN_REFRESH_TOKEN RefreshToken
|
||||
ALIYUNPAN_AUTH_USER webdav登录用户名
|
||||
ALIYUNPAN_AUTH_PASSWORD webdav登录密码
|
||||
ALIYUNPAN_WEBDAV_MODE webdav模式,支持:rw-读写,ro-只读
|
||||
ALIYUNPAN_PAN_DRIVE 网盘类型,可选: File-文件 Album-相册
|
||||
ALIYUNPAN_PAN_DIR 网盘文件夹的webdav服务根目录
|
||||
```
|
||||
更详情文档请参考dockerhub网址:[tickstep/aliyunpan-webdav](https://hub.docker.com/r/tickstep/aliyunpan-webdav)
|
||||
|
||||
# 如何使用
|
||||
完整和详细的命令说明请查看手册:[命令手册](docs/manual.md)
|
||||
|
||||
1. Windows
|
||||
程序应在 命令提示符 (Command Prompt) 或 PowerShell 中运行.
|
||||
也可直接双击程序运行, 具体使用方法请参见 [命令列表及说明](docs/manual.md#命令列表及说明)
|
||||
|
||||
2. Linux / macOS
|
||||
程序应在 终端 (Terminal) 运行.
|
||||
具体使用方法请参见 [命令列表及说明](docs/manual.md#命令列表及说明)
|
||||
|
||||
如果程序运行时输出乱码, 请检查下终端的编码方式是否为 `UTF-8`.
|
||||
|
||||
如果没有带任何参数运行程序, 程序将会进入仿Linux shell系统用户界面的CLI交互模式, 可直接运行相关命令.
|
||||
在交互模式下, 光标所在行的前缀应为 `aliyunpan >`, 如果登录了帐号则格式为 `aliyunpan:<工作目录> <用户昵称>$ `
|
||||
|
||||
程序内置了相关命令的使用说明,你可以通过运行`命令 -h`的方式获取命令的使用说明,例如:`upload -h`获取上传命令的使用说明。
|
||||
|
||||
## 基本使用
|
||||
本程序支持阿里云盘大多数命令操作,这里只介绍基本的使用,更多更详细的命令请查看手册:[命令手册](docs/manual.md)。
|
||||
|
||||
### 修改配置目录
|
||||
你可以指定程序配置文件的存储路径,如果没有指定,程序会使用默认的目录。
|
||||
方法为设置环境变量`ALIYUNPAN_CONFIG_DIR`并指定一个存在的目录,例如linux下面可以这样指定
|
||||
```shell
|
||||
export ALIYUNPAN_CONFIG_DIR=/home/tickstep/tools/aliyunpan/config
|
||||
```
|
||||
|
||||
## 检测程序更新
|
||||
```
|
||||
aliyunpan update
|
||||
### 启动程序
|
||||
直接启动进入交互命令行
|
||||
```shell
|
||||
[tickstep@MacPro ~]$ aliyunpan
|
||||
提示: 方向键上下可切换历史命令.
|
||||
提示: Ctrl + A / E 跳转命令 首 / 尾.
|
||||
提示: 输入 help 获取帮助.
|
||||
aliyunpan >
|
||||
```
|
||||
|
||||
## 查看帮助
|
||||
```
|
||||
aliyunpan help
|
||||
```
|
||||
### 例子
|
||||
```
|
||||
列出程序支持的命令
|
||||
aliyunpan help
|
||||
### 查看帮助
|
||||
```shell
|
||||
aliyunpan > help
|
||||
|
||||
查看login命令的帮助手册
|
||||
aliyunpan help login
|
||||
...
|
||||
阿里云盘:
|
||||
album, abm 相簿(Beta)
|
||||
cd 切换工作目录
|
||||
download, d 下载文件/目录
|
||||
ls, l, ll 列出目录
|
||||
mkdir 创建目录
|
||||
mv 移动文件/目录
|
||||
pwd 输出工作目录
|
||||
recycle 回收站
|
||||
rename 重命名文件
|
||||
rm 删除文件/目录
|
||||
share 分享文件/目录
|
||||
sync 同步备份功能
|
||||
upload, u 上传文件/目录
|
||||
webdav 在线网盘服务
|
||||
...
|
||||
```
|
||||
|
||||
## 登录阿里云盘帐号
|
||||
|
||||
### 登录
|
||||
当前支持使用RefreshToken进行登录。RefreshToken请参考 [1. 如何获取RefreshToken](#1-如何获取RefreshToken) 获取
|
||||
```
|
||||
aliyunpan login
|
||||
需要先登录,已经登录过的可以跳过此步。
|
||||
RefreshToken获取教程请查看:[如何获取RefreshToken](#如何获取RefreshToken)
|
||||
```shell
|
||||
aliyunpan > login -RefreshToken=32994cd2c43...4d505fa79
|
||||
|
||||
阿里云盘登录成功: tickstep
|
||||
aliyunpan:/ tickstep$
|
||||
```
|
||||
|
||||
### 例子
|
||||
```
|
||||
按照引导步骤登录
|
||||
aliyunpan login
|
||||
请输入RefreshToken, 回车键提交 > 626a27b6193f4c5ca6ef0.......
|
||||
### 查看文件列表
|
||||
```shell
|
||||
aliyunpan:/ tickstep$ ls
|
||||
|
||||
命令行指定RefreshToken登录
|
||||
aliyunpan login -RefreshToken=626a27b6193f4c5ca6ef0.......
|
||||
# 文件大小 修改日期 文件(目录)
|
||||
0 - 2021-11-03 13:32:22 临时/
|
||||
1 - 2021-07-10 07:44:34 好友的分享/
|
||||
2 - 2021-07-09 22:11:22 我的项目/
|
||||
3 - 2021-07-09 22:10:37 我的游戏/
|
||||
4 - 2021-07-09 22:10:10 我的文档/
|
||||
5 349.86KB 2021-06-06 11:46:02 使用统计.xls
|
||||
6 503.57KB 2021-06-06 11:46:02 IMG_0098.JPG
|
||||
7 72.20KB 2021-06-06 11:46:02 IMG_0103.PNG
|
||||
总: 3.20MB 文件总数: 3, 目录总数: 7
|
||||
----
|
||||
```
|
||||
|
||||
### 下载文件
|
||||
```shell
|
||||
aliyunpan:/ tickstep$ download IMG_0106.JPG
|
||||
|
||||
## 列出帐号列表
|
||||
[0] 当前文件下载最大并发量为: 5, 下载缓存为: 64.00KB
|
||||
[1] 加入下载队列: /IMG_0106.JPG
|
||||
[1] ----
|
||||
文件ID: 60bc44f855814e19692a4958b4a8823a1a06e5de
|
||||
文件名: IMG_0106.JPG
|
||||
文件类型: 文件
|
||||
文件路径: /IMG_0106.JPG
|
||||
|
||||
```
|
||||
aliyunpan loglist
|
||||
```
|
||||
|
||||
列出所有已登录的帐号
|
||||
|
||||
## 获取当前帐号
|
||||
[1] 准备下载: /IMG_0106.JPG
|
||||
[1] 将会下载到路径: /root/Downloads/4d001d48564f43b..62874f04bbe6/IMG_0106.JPG
|
||||
[1] 下载开始
|
||||
[1] ↓ 704.00KB/1.48MB 0B/s(1.69MB/s) in 1.88s, left - ............
|
||||
[1] 下载完成, 保存位置: /root/Downloads/4d001d48564f43b..62874f04bbe6/IMG_0106.JPG
|
||||
[1] 检验文件有效性成功: /root/Downloads/4d001d48564f43b..62874f04bbe6/IMG_0106.JPG
|
||||
|
||||
下载结束, 时间: 4秒, 数据总量: 1.48MB
|
||||
aliyunpan:/ tickstep$
|
||||
```
|
||||
aliyunpan who
|
||||
```
|
||||
|
||||
## 切换阿里云盘帐号
|
||||
|
||||
切换已登录的帐号
|
||||
```
|
||||
aliyunpan su <uid>
|
||||
```
|
||||
```
|
||||
aliyunpan su
|
||||
|
||||
请输入要切换帐号的 # 值 >
|
||||
```
|
||||
|
||||
## 退出阿里云盘帐号
|
||||
|
||||
退出当前登录的帐号
|
||||
```
|
||||
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>
|
||||
```
|
||||
```
|
||||
aliyunpan drive
|
||||
|
||||
输入要切换的网盘 # 值 >
|
||||
```
|
||||
|
||||
## 获取网盘配额
|
||||
|
||||
```
|
||||
aliyunpan quota
|
||||
```
|
||||
获取网盘的总储存空间, 和已使用的储存空间
|
||||
|
||||
## 切换工作目录
|
||||
```
|
||||
aliyunpan cd <目录>
|
||||
```
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 切换 /我的文档 工作目录
|
||||
aliyunpan cd /我的文档
|
||||
|
||||
# 切换 上级目录
|
||||
aliyunpan cd ..
|
||||
|
||||
# 切换 根目录
|
||||
aliyunpan cd /
|
||||
|
||||
```
|
||||
|
||||
## 输出工作目录
|
||||
```
|
||||
aliyunpan pwd
|
||||
```
|
||||
|
||||
## 列出目录
|
||||
|
||||
列出当前工作目录的文件和目录或指定目录
|
||||
```
|
||||
aliyunpan ls
|
||||
```
|
||||
```
|
||||
aliyunpan ls <目录>
|
||||
```
|
||||
|
||||
### 可选参数
|
||||
```
|
||||
-driveId value 网盘ID
|
||||
```
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 列出 我的文档 内的文件和目录
|
||||
aliyunpan ls 我的文档
|
||||
|
||||
# 绝对路径
|
||||
aliyunpan ls /我的文档
|
||||
|
||||
# 详细列出 我的文档 内的文件和目录
|
||||
aliyunpan ll /我的文档
|
||||
```
|
||||
|
||||
## 下载文件/目录
|
||||
下载支持两种链接类型:1-默认类型 2-阿里ECS环境类型
|
||||
在普通网络下,下载速度可以达到10MB/s,在阿里ECS(必须是"经典网络"类型的机器)环境下,下载速度单文件可以轻松达到20MB/s,多文件可以达到100MB/s
|
||||

|
||||

|
||||
|
||||
### 上传文件
|
||||
```shell
|
||||
aliyunpan:/ tickstep$ upload /Users/tickstep/Downloads/apt.zip /tmp
|
||||
|
||||
[0] 当前文件上传最大并发量为: 10, 上传分片大小为: 10.00MB
|
||||
[1] 加入上传队列: /Users/tickstep/Downloads/apt.zip
|
||||
[1] 2022-08-13 13:41:22 准备上传: /Users/tickstep/Downloads/apt.zip => /tmp/apt.zip
|
||||
[1] 2022-08-13 13:41:22 正在检测和创建云盘文件夹: /tmp
|
||||
[1] 2022-08-13 13:41:22 正在计算文件SHA1: /Users/tickstep/Downloads/apt.zip
|
||||
[1] 2022-08-13 13:41:22 检测秒传中, 请稍候...
|
||||
[1] 2022-08-13 13:41:22 秒传失败,开始正常上传文件
|
||||
[1] ↑ 21.00MB/21.00MB 702.53KB/s(702.70KB/s) in 15s ............
|
||||
[1] 2022-08-13 13:41:22 上传文件成功, 保存到网盘路径: /tmp/apt.zip
|
||||
[1] 2022-08-13 13:41:22 文件上传结果: 成功! 耗时 18秒
|
||||
|
||||
上传结束, 时间: 18秒, 数据总量: 21.00MB
|
||||
```
|
||||
aliyunpan download <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
|
||||
aliyunpan d <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
|
||||
```
|
||||
|
||||
### 可选参数
|
||||
```
|
||||
--ow overwrite, 覆盖已存在的文件
|
||||
--status 输出所有线程的工作状态
|
||||
--save 将下载的文件直接保存到当前工作目录
|
||||
--saveto value 将下载的文件直接保存到指定的目录
|
||||
-x 为文件加上执行权限, (windows系统无效)
|
||||
-p value 指定下载线程数 (default: 0)
|
||||
-l value 指定同时进行下载文件的数量 (default: 0)
|
||||
--retry value 下载失败最大重试次数 (default: 3)
|
||||
--nocheck 下载文件完成后不校验文件
|
||||
--exn value 指定排除的文件夹或者文件的名称,只支持正则表达式。支持排除多个名称,每一个名称就是一个exn参数
|
||||
```
|
||||
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 设置保存目录, 保存到 D:\Downloads
|
||||
# 注意区别反斜杠 "\" 和 斜杠 "/" !!!
|
||||
aliyunpan config set -savedir D:/Downloads
|
||||
|
||||
# 下载 /我的文档/1.mp4
|
||||
aliyunpan d /我的文档/1.mp4
|
||||
|
||||
# 下载 /我的文档 整个目录!!
|
||||
aliyunpan d /我的文档
|
||||
```
|
||||
|
||||
下载的文件默认保存到 **程序所在目录** 的 download/ 目录, 支持设置指定目录, 重名的文件会自动跳过!
|
||||
|
||||
通过 `aliyunpan config set -savedir <savedir>` 可以自定义保存的目录.
|
||||
|
||||
支持多个文件或目录下载.
|
||||
|
||||
自动跳过下载重名的文件!
|
||||
|
||||
## 上传文件/目录
|
||||
上传支持两种链接类型:1-默认类型 2-阿里ECS环境类型
|
||||
在阿里ECS(必须是"经典网络"类型的机器)环境下,上传速度单文件可以轻松达到30MB/s,多文件可以达到100MB/s
|
||||

|
||||
|
||||
```
|
||||
aliyunpan upload <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>
|
||||
aliyunpan u <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>
|
||||
```
|
||||
|
||||
### 例子:
|
||||
```
|
||||
# 将本地的 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录
|
||||
# 注意区别反斜杠 "\" 和 斜杠 "/" !!!
|
||||
aliyunpan upload C:/Users/Administrator/Desktop/1.mp4 /视频
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Desktop\1.mp4 和 C:\Users\Administrator\Desktop\2.mp4 上传到网盘 /视频 目录
|
||||
aliyunpan upload C:/Users/Administrator/Desktop/1.mp4 C:/Users/Administrator/Desktop/2.mp4 /视频
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录
|
||||
aliyunpan upload C:/Users/Administrator/Desktop /视频
|
||||
|
||||
## 下面演示文件或者文件夹排除功能
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
|
||||
aliyunpan upload -exn "\.jpg$" C:/Users/Administrator/Video /视频
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
|
||||
aliyunpan upload -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视频
|
||||
|
||||
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式
|
||||
1)排除@eadir文件或者文件夹:-exn "^@eadir$"
|
||||
2)排除.jpg文件:-exn "\.jpg$"
|
||||
3)排除.号开头的文件:-exn "^\."
|
||||
4)排除~号开头的文件:-exn "^~"
|
||||
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
|
||||
```
|
||||
|
||||
## 备份本地文件/目录
|
||||
|
||||
本地备份功能一般用于NAS等系统,日常只进行增量备份操作,默认情况下本地删除不影响网盘文件。
|
||||
|
||||
比如在手机上备分照片目录,就可以使用这个功能定时备份,备份完成后本地文件可安全删除。
|
||||
|
||||
基本用法和`upload`命令一样,额外增加两个参数
|
||||
|
||||
>1. delete 用于同步删除操作(只通过本地数据库记录对比)。
|
||||
>2. sync 使网盘上的文件和本地文件同步(本地为主),会删除网盘中不存在于本地的文件或目录(速度比较慢)。
|
||||
>sync 和 delete 的区别在于sync是通过网盘文件列表和本地文件进行判断。
|
||||
>delete 只通过本地数据库进行判断,正常情况下使用 delete 就够用了。
|
||||
|
||||
和 `upload` 相比由于增加了本地数据库,可以快速判断文件是否有更新等,大大减少了 API 的调用,操作速度更快。
|
||||
|
||||
```
|
||||
aliyunpan backup <本地目录1> <目录2> <目录3> ... <目标目录>
|
||||
注:
|
||||
1. 默认不删除网盘文件,可用 delete 或 sync 参数进行同步删除。
|
||||
```
|
||||
|
||||
### 例子:
|
||||
```
|
||||
# 将本地的 C:\Users\Administrator\Desktop 备份到网盘 /test 目录
|
||||
# 注意区别反斜杠 "\" 和 斜杠 "/" !!!
|
||||
aliyunpan backup C:/Users/Administrator/Desktop /test
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Desktop 备份到网盘 /test 目录,但排除所有 @eadir 目录
|
||||
aliyunpan backup -exn "^@eadir$" C:/Users/Administrator/Desktop /test
|
||||
```
|
||||
|
||||
## 手动秒传上传文件
|
||||
通过秒传链接上传文件到网盘,秒传链接可以通过share命令获取
|
||||
```
|
||||
aliyunpan rapidupload <秒传链接1> <秒传链接2> <秒传链接3> ...
|
||||
```
|
||||
|
||||
### 例子:
|
||||
```
|
||||
# 如果秒传成功, 则保存到网盘路径 /file.dmg
|
||||
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|"
|
||||
|
||||
# 如果秒传成功, 则保存到网盘路径 /pan_folder/file.dmg
|
||||
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder"
|
||||
|
||||
```
|
||||
|
||||
## 创建目录
|
||||
```
|
||||
aliyunpan mkdir <目录>
|
||||
```
|
||||
|
||||
### 例子
|
||||
```
|
||||
aliyunpan mkdir test123
|
||||
```
|
||||
|
||||
## 删除文件/目录
|
||||
```
|
||||
aliyunpan rm <网盘文件或目录的路径1> <文件或目录2> <文件或目录3> ...
|
||||
```
|
||||
|
||||
注意: 删除多个文件和目录时, 请确保每一个文件和目录都存在, 否则删除操作会失败.
|
||||
|
||||
被删除的文件或目录可在网盘文件回收站找回.
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 删除 /我的文档/1.mp4
|
||||
aliyunpan rm /我的文档/1.mp4
|
||||
|
||||
# 删除 /我的文档/1.mp4 和 /我的文档/2.mp4
|
||||
aliyunpan rm /我的文档/1.mp4 /我的文档/2.mp4
|
||||
|
||||
# 删除 /我的文档 整个目录 !!
|
||||
aliyunpan rm /我的文档
|
||||
```
|
||||
|
||||
|
||||
## 移动文件/目录
|
||||
```
|
||||
aliyunpan mv <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>
|
||||
```
|
||||
|
||||
注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败.
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 将 /我的文档/1.mp4 移动到 根目录 /
|
||||
aliyunpan mv /我的文档/1.mp4 /
|
||||
```
|
||||
|
||||
## 重命名文件/目录
|
||||
```
|
||||
aliyunpan rename <旧文件/目录名> <新文件/目录名>
|
||||
```
|
||||
|
||||
注意: 重命名的文件/目录,如果指定的是绝对路径,则必须保证新旧的绝对路径在同一个文件夹内,否则重命名失败!
|
||||
### 同步备份文件
|
||||
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份三种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 将 /我的文档/1.mp4 重命名为 /我的文档/2.mp4
|
||||
aliyunpan rename /我的文档/1.mp4 /我的文档/2.mp4
|
||||
```
|
||||
|
||||
## 导出文件
|
||||
导出文件主要是用于备份网盘的文件。通过该命令将网盘的文件元数据信息导出并保存到本地文件中,等到以后需要的时候再通过import命令导入到网盘中。
|
||||
```
|
||||
aliyunpan export <网盘文件/目录的路径1> <文件/目录2> <文件/目录3> ... <本地保存文件路径>
|
||||
```
|
||||
导出指定文件/目录下面的所有文件的元数据信息,并保存到指定的本地文件里面。导出的文件元信息可以使用 import 命令(秒传文件功能)导入到网盘中。
|
||||
***注意:如果同步目录下有非常多的文件,最好在首次备份前先运行一次scan任务,等scan任务完成并建立起同步数据库后,再正常启动同步任务。这样同步任务可以更加快速同步并且能有效避免同步重复文件。***
|
||||
|
||||
### 例子
|
||||
```
|
||||
导出 /我的资源 整个目录 元数据到文件 /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
|
||||
```
|
||||
例如:将本地目录 `/tickstep/Documents/设计文档` 中的文件备份上传到云盘目录 `/备份盘/我的文档`
|
||||
```shell
|
||||
首次运行建议先扫描并构建同步数据库,如下:
|
||||
aliyunpan:/ tickstep$ sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload" -step scan
|
||||
|
||||
## 导入文件
|
||||
```
|
||||
aliyunpan export <本地元数据文件路径>
|
||||
```
|
||||
导入文件中记录的元数据文件到网盘。保存到网盘的文件会使用文件元数据记录的路径位置,如果没有指定云盘目录(saveto)则默认导入到目录 aliyunpan 中。
|
||||
导入的文件可以使用 export 命令获得。
|
||||
|
||||
导入文件每一行是一个文件元数据,样例如下:
|
||||
```
|
||||
aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|
|
||||
```
|
||||
|
||||
### 例子
|
||||
```
|
||||
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项
|
||||
aliyunpan import /Users/tickstep/Downloads/export_files.txt
|
||||
然后再正常启动同步任务,如下:
|
||||
aliyunpan:/ tickstep$ sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload"
|
||||
|
||||
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项并保存到目录 /my2020 中
|
||||
aliyunpan import -saveto=/my2020 /Users/tickstep/Downloads/export_files.txt
|
||||
启动同步备份进程
|
||||
备份配置文件:(使用命令行配置)
|
||||
链接类型:默认链接
|
||||
下载并发:2
|
||||
上传并发:2
|
||||
下载分片大小:1.00MB
|
||||
上传分片大小:10.00MB
|
||||
|
||||
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项并保存到网盘根目录 / 中
|
||||
aliyunpan import -saveto=/ /Users/tickstep/Downloads/export_files.txt
|
||||
启动同步任务
|
||||
任务: 设计文档(de3d6b69a607497b73624bcca0845f19)
|
||||
同步模式: 备份本地文件(只上传)
|
||||
本地目录: /tickstep/Documents/设计文档
|
||||
云盘目录: /备份盘/我的文档
|
||||
```
|
||||
|
||||
## 分享文件/目录
|
||||
```
|
||||
aliyunpan share
|
||||
```
|
||||
|
||||
### 设置分享文件/目录
|
||||
阿里目前之支持少数文件类型的分享,不支持的文件分享会提示分享失败
|
||||
```
|
||||
aliyunpan share set <文件/目录1> <文件/目录2> ...
|
||||
aliyunpan share s <文件/目录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/
|
||||
```
|
||||
|
||||
|
||||
## webdav文件服务
|
||||
本文命令可以让阿里云盘变身为webdav协议的文件服务器。这样你可以把阿里云盘挂载为Windows、Linux、Mac系统的磁盘,可以通过NAS系统做文件管理或文件同步等等。
|
||||
当把阿里云盘作为webdav文件服务器进行使用的时候,上传文件是不支持秒传的,所以当你挂载为网络磁盘使用的时候,不建议在webdav挂载目录中上传、下载过大的文件,不然体验会非常差。
|
||||
建议作为文档,图片等小文件的同步网盘。
|
||||
效果图如下所示:
|
||||

|
||||
|
||||
### 常用命令说明
|
||||
```
|
||||
查看webdav说明
|
||||
aliyunpan webdav
|
||||
|
||||
查看webdav如何启动说明
|
||||
aliyunpan webdav start -h
|
||||
|
||||
使用默认配置启动webdav服务。
|
||||
aliyunpan webdav start
|
||||
|
||||
启动webdav服务,并配置IP为127.0.0.1,端口为23077,登录用户名为admin,登录密码为admin123,网盘目录 /webdav_folder 作为服务的根目录
|
||||
aliyunpan webdav start -ip "127.0.0.1" -port 23077 -webdav_user "admin" -webdav_password "admin123" -pan_dir_path "/webdav_folder"
|
||||
|
||||
正常启动后会打印出webdav链接参数,然后使用支持webdav的客户端填入下面对应的参数进行链接即可
|
||||
----------------------------------------
|
||||
webdav网盘信息:
|
||||
链接:http://localhost:23077
|
||||
用户名:admin
|
||||
密码:admin123
|
||||
网盘服务目录:/webdav_folder
|
||||
----------------------------------------
|
||||
```
|
||||
|
||||
### 命令行启动
|
||||
需要先进行登录。然后使用以下命令运行即可,该命令是阻塞的不会退出,除非停止webdav服务。
|
||||
|
||||
```
|
||||
./aliyunpan webdav start -port 23077 -webdav_user "admin" -webdav_password "admin123" -pan_dir_path "/webdav_folder"
|
||||
|
||||
参数说明
|
||||
port:绑定端口
|
||||
webdav_user: webdav客户端登录用户名
|
||||
webdav_password: webdav客户端登录密码
|
||||
pan_dir_path:指定webdav使用那个阿里云盘目录作为服务根目录
|
||||
```
|
||||
|
||||
### Linux后台启动
|
||||
建议结合nohup进行启动。
|
||||
|
||||
先创建webdav.sh脚本,内容如下
|
||||
```
|
||||
# 请更改成你自己的目录
|
||||
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
|
||||
|
||||
# 指定webdav启动参数并进行启动
|
||||
./aliyunpan webdav start -ip "0.0.0.0" -port 23077 -webdav_user "admin" -webdav_password "admin" -pan_dir_path "/" -bs 1024
|
||||
```
|
||||
|
||||
增加脚本执行权限
|
||||
```
|
||||
$ chmod +x webdav.sh
|
||||
```
|
||||
|
||||
然后启动该脚本进行后台运行
|
||||
```
|
||||
$ nohup ./webdav.sh >/dev/null 2>&1 &
|
||||
```
|
||||
|
||||
### Docker运行
|
||||
详情文档请参考dockerhub网址:[tickstep/aliyunpan-webdav](https://hub.docker.com/r/tickstep/aliyunpan-webdav)
|
||||
|
||||
1. 直接运行
|
||||
```
|
||||
docker run -d --name=aliyunpan-webdav --restart=always -p 23077:23077 -e TZ="Asia/Shanghai" -e ALIYUNPAN_REFRESH_TOKEN="<your refreshToken>" -e ALIYUNPAN_AUTH_USER="admin" -e ALIYUNPAN_AUTH_PASSWORD="admin" -e ALIYUNPAN_PAN_DIR="/" tickstep/aliyunpan-webdav
|
||||
|
||||
# ALIYUNPAN_REFRESH_TOKEN RefreshToken
|
||||
# ALIYUNPAN_AUTH_USER webdav登录用户名
|
||||
# ALIYUNPAN_AUTH_PASSWORD webdav登录密码
|
||||
# ALIYUNPAN_PAN_DIR 网盘文件夹的webdav服务根目录
|
||||
```
|
||||
|
||||
2. docker-compose运行
|
||||
docker-compose.yml 文件如下所示,为了方便说明增加了相关的注释,部署的时候可以去掉注释。
|
||||
```
|
||||
version: '3'
|
||||
services:
|
||||
webdav:
|
||||
image: tickstep/aliyunpan-webdav
|
||||
container_name: aliyunpan-webdav
|
||||
restart: always
|
||||
ports:
|
||||
- 23077:23077
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
# refresh token用于登录
|
||||
- ALIYUNPAN_REFRESH_TOKEN=b9123...13e66a1
|
||||
# webdav 登录用户名
|
||||
- ALIYUNPAN_AUTH_USER=admin
|
||||
# webdav 登录密码
|
||||
- ALIYUNPAN_AUTH_PASSWORD=admin
|
||||
# 指定网盘文件夹作为webdav服务根目录
|
||||
- ALIYUNPAN_PAN_DIR=/
|
||||
# 上传下载链接类型:1-默认 2-阿里ECS环境
|
||||
- ALIYUNPAN_TRANSFER_URL_TYPE=1
|
||||
# 上传数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
|
||||
- ALIYUNPAN_BLOCK_SIZE=10240
|
||||
```
|
||||
|
||||
### HTTPS配置
|
||||
建议使用nginx进行https的配置。样例如下:
|
||||
```
|
||||
server {
|
||||
listen 443;
|
||||
server_name your.host.com;
|
||||
ssl on;
|
||||
root html;
|
||||
index index.html index.htm;
|
||||
ssl_certificate /path/to/your/file.pem;
|
||||
ssl_certificate_key /path/to/your/file.key;
|
||||
|
||||
ssl_session_timeout 5m;
|
||||
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
# webdav server
|
||||
location /{
|
||||
root html;
|
||||
proxy_pass http://127.0.0.1:23077;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header REMOTE-HOST $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_redirect off;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 显示和修改程序配置项
|
||||
```
|
||||
# 显示配置
|
||||
aliyunpan config
|
||||
|
||||
# 设置配置
|
||||
aliyunpan config set
|
||||
```
|
||||
|
||||
|
||||
### 例子
|
||||
```
|
||||
# 显示所有可以设置的值
|
||||
aliyunpan config -h
|
||||
aliyunpan config set -h
|
||||
|
||||
# 设置下载文件的储存目录
|
||||
aliyunpan config set -savedir D:/Downloads
|
||||
|
||||
# 设置下载最大并发量为 15
|
||||
aliyunpan config set -max_download_parallel 15
|
||||
|
||||
# 组合设置
|
||||
aliyunpan config set -max_download_parallel 15 -savedir D:/Downloads
|
||||
|
||||
# 设置使用阿里云内部URL链接,专供阿里云ECS环境使用
|
||||
# 开启内部URL链接可以使用阿里云ECS私网带宽流量,而不用使用宝贵的公网带宽流量,如果你在阿里ECS环境中使用本工具,建议开启
|
||||
aliyunpan config set -transfer_url_type 2
|
||||
```
|
||||
## 更多命令
|
||||
更多更详细的命令请查看手册:[命令手册](docs/manual.md)。
|
||||
|
||||
# 常见问题Q&A
|
||||
## 1 如何获取RefreshToken
|
||||
# 常见问题
|
||||
## 如何获取RefreshToken
|
||||
需要通过浏览器获取refresh_token。这里以Chrome浏览器为例,其他浏览器类似。
|
||||
打开 [阿里云盘网页](https://www.aliyundrive.com/drive) 并进行登录,然后F12按键打开浏览器调试菜单,按照下面步骤进行
|
||||

|
||||
|
||||
## 2 如何开启Debug调试日志
|
||||
或者直接在控制台输入以下命令获取
|
||||
```
|
||||
JSON.parse(localStorage.getItem("token")).refresh_token
|
||||
```
|
||||

|
||||
|
||||
|
||||
## 如何开启Debug调试日志
|
||||
当需要定位问题,或者提交issue的时候抓取log,则需要开启debug日志。步骤如下:
|
||||
|
||||
### 第一步
|
||||
@@ -758,19 +307,7 @@ Windows
|
||||
打开aliyunpan命令行程序,任何云盘命令都有类似如下日志输出
|
||||

|
||||
|
||||
## 3 解决 missing Location in call to Date 问题
|
||||
在使用webdav文件服务的过程中可能会出现该问题。
|
||||
原因是golang中的time.LoadLocation()依赖于 IANA Time Zone Database,一般linux系统都带了,但是有部分系统没有带有这个数据文件则出现该问题。
|
||||
解决方法:
|
||||
1. 下载文件 ./assets/binary/tzdata.zip 文件并保存到系统中,记下保存的文件路径
|
||||
2. 设置环境变量 ZONEINFO
|
||||
```
|
||||
export ZONEINFO=/path/to/tzdata.zip
|
||||
```
|
||||
然后启动aliyunpan服务即可
|
||||
|
||||
# 交流反馈
|
||||
|
||||
提交issue: [issues页面](https://github.com/tickstep/aliyunpan/issues)
|
||||
联系邮箱: tickstep@outlook.com
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity version="0.1.2.0" processorArchitecture="*" name="com.tickstep.aliyunpan" type="win32"/>
|
||||
<assemblyIdentity version="0.2.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="*"/>
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Executable
+127
@@ -0,0 +1,127 @@
|
||||
// ==========================================================================================
|
||||
// aliyunpan JS插件回调处理函数
|
||||
// 支持 JavaScript ECMAScript 5.1 语言规范
|
||||
//
|
||||
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
|
||||
// ==========================================================================================
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:下载文件前的回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "albumDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户文件网盘ID
|
||||
// albumDriveId - 用户相册网盘ID
|
||||
//
|
||||
// params - 文件下载前参数
|
||||
// {
|
||||
// "driveId": "19519221",
|
||||
// "driveFileName": "token.bat",
|
||||
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
|
||||
// "driveFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "driveFileSize": 125330,
|
||||
// "driveFileType": "file",
|
||||
// "driveFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "localFilePath": "aliyunpan\\Downloads\\token.bat"
|
||||
// }
|
||||
// driveId - 网盘ID
|
||||
// driveFileName - 网盘文件名
|
||||
// driveFilePath - 网盘文件绝对完整路径
|
||||
// driveFileSize - 网盘文件大小,单位B
|
||||
// driveFileSha1 - 网盘文件SHA1
|
||||
// driveFileType - 网盘文件类型,file-文件,folder-文件夹
|
||||
// driveFileUpdatedAt - 网盘文件修改时间
|
||||
// localFilePath - 下载文件到本地保存的路径,这个是相对路径,相对指定下载的目标文件夹
|
||||
//
|
||||
// 返回值说明
|
||||
// {
|
||||
// "downloadApproved": "yes",
|
||||
// "localFilePath": "newfolder\\token.bat"
|
||||
// }
|
||||
// downloadApproved - 该文件是否下载,yes-确认下载,no-取消下载
|
||||
// localFilePath - 文件保存的本地路径,这个是相对路径,如果为空""代表保持原本的目标路径。
|
||||
// 这个改动要小心,会导致重名文件下载只会下载一个
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function downloadFilePrepareCallback(context, params) {
|
||||
console.log(params)
|
||||
|
||||
var result = {
|
||||
"downloadApproved": "yes",
|
||||
"localFilePath": ""
|
||||
};
|
||||
|
||||
// 所有的.dmg.exe文件,下载保存的文件后缀名去掉.exe,网盘文件不改动
|
||||
if (params["driveFileName"].lastIndexOf(".dmg.exe") > 0) {
|
||||
result["localFilePath"] = params["localFilePath"].substr(0, (params["localFilePath"].length - ".exe".length));
|
||||
}
|
||||
|
||||
// 禁止.txt文件下载
|
||||
if (params["driveFileName"].lastIndexOf(".txt") > 0) {
|
||||
result["downloadApproved"] = "no";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:下载文件结束的回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "albumDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户文件网盘ID
|
||||
// albumDriveId - 用户相册网盘ID
|
||||
//
|
||||
// params - 文件下载结束参数
|
||||
// {
|
||||
// "driveId": "19519221",
|
||||
// "driveFileName": "token.bat",
|
||||
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
|
||||
// "driveFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "driveFileSize": 125330,
|
||||
// "driveFileType": "file",
|
||||
// "driveFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "downloadResult": "success",
|
||||
// "localFilePath": "aliyunpan\\Downloads\\token.bat"
|
||||
// }
|
||||
// driveId - 网盘ID
|
||||
// driveFileName - 网盘文件名
|
||||
// driveFilePath - 网盘文件绝对完整路径
|
||||
// driveFileSize - 网盘文件大小,单位B
|
||||
// driveFileSha1 - 网盘文件SHA1
|
||||
// driveFileType - 网盘文件类型,file-文件,folder-文件夹
|
||||
// driveFileUpdatedAt - 网盘文件修改时间
|
||||
// downloadResult - 下载结果,success-成功,fail-失败
|
||||
// localFilePath - 下载文件到本地保存的路径,这个是相对路径,相对指定下载的目标文件夹
|
||||
//
|
||||
// 返回值说明
|
||||
// (没有返回值)
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function downloadFileFinishCallback(context, params) {
|
||||
console.log(params)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// ==========================================================================================
|
||||
// aliyunpan JS插件回调处理函数
|
||||
// 支持 JavaScript ECMAScript 5.1 语言规范
|
||||
//
|
||||
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
|
||||
// ==========================================================================================
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:同步备份-扫描本地文件前的回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "albumDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户文件网盘ID
|
||||
// albumDriveId - 用户相册网盘ID
|
||||
//
|
||||
// params - 扫描本地文件前参数
|
||||
// {
|
||||
// "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
|
||||
// "localFileName": "token.bat",
|
||||
// "localFileSize": 125330,
|
||||
// "localFileType": "file",
|
||||
// "localFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "driveId": "19519221"
|
||||
// }
|
||||
// localFilePath - 本地文件绝对完整路径
|
||||
// localFileName - 本地文件名
|
||||
// localFileSize - 本地文件大小,单位B
|
||||
// localFileType - 本地文件类型,file-文件,folder-文件夹
|
||||
// localFileUpdatedAt - 文件修改时间
|
||||
// driveId - 备份的目标网盘ID
|
||||
//
|
||||
// 返回值说明
|
||||
// {
|
||||
// "syncScanLocalApproved": "yes"
|
||||
// }
|
||||
// syncScanLocalApproved - 该文件是否确认扫描,yes-允许扫描,no-禁止扫描。
|
||||
// 禁止扫描的文件不会执行后续的动作,例如上传,下载。
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function syncScanLocalFilePrepareCallback(context, params) {
|
||||
console.log(params);
|
||||
var result = {
|
||||
"syncScanLocalApproved": "yes"
|
||||
};
|
||||
|
||||
// 禁止.开头文件上传
|
||||
if (params["localFileName"].indexOf(".") == 0) {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止~$开头文件上传(office暂存临时文件)
|
||||
if (params["localFileName"].indexOf("~$") == 0) {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止.txt文件上传(正则表达式方式)
|
||||
if (params["localFileName"].search(/.txt$/i) >= 0) {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止password.key文件上传
|
||||
if (params["localFileName"] == "password.key") {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止@eadir文件上传
|
||||
if (params["localFileName"] == "@eadir") {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:同步备份-扫描云盘文件前的回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "albumDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户文件网盘ID
|
||||
// albumDriveId - 用户相册网盘ID
|
||||
//
|
||||
// params - 扫描云盘文件前参数
|
||||
// {
|
||||
// "driveId": "19519221",
|
||||
// "driveFileName": "token.bat",
|
||||
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
|
||||
// "driveFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "driveFileSize": 125330,
|
||||
// "driveFileType": "file",
|
||||
// "driveFileUpdatedAt": "2022-04-14 07:05:12"
|
||||
// }
|
||||
// driveId - 网盘ID
|
||||
// driveFileName - 网盘文件名
|
||||
// driveFilePath - 网盘文件绝对完整路径
|
||||
// driveFileSize - 网盘文件大小,单位B
|
||||
// driveFileSha1 - 网盘文件SHA1
|
||||
// driveFileType - 网盘文件类型,file-文件,folder-文件夹
|
||||
// driveFileUpdatedAt - 网盘文件修改时间
|
||||
//
|
||||
// 返回值说明
|
||||
// {
|
||||
// "syncScanPanApproved": "yes"
|
||||
// }
|
||||
// syncScanPanApproved - 该文件是否确认扫描,yes-允许扫描,no-禁止扫描。
|
||||
// 禁止扫描的文件不会执行后续的动作,例如上传,下载。
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function syncScanPanFilePrepareCallback(context, params) {
|
||||
console.log(params);
|
||||
|
||||
var result = {
|
||||
"syncScanPanApproved": "yes"
|
||||
};
|
||||
|
||||
// 禁止.开头文件下载
|
||||
if (params["driveFileName"].indexOf(".") == 0) {
|
||||
result["syncScanPanApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止~$开头文件下载(office暂存临时文件)
|
||||
if (params["driveFileName"].indexOf("~$") == 0) {
|
||||
result["syncScanPanApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止.txt文件下载(正则表达式方式)
|
||||
// if (params["driveFileName"].search(/.txt$/i) >= 0) {
|
||||
// result["syncScanPanApproved"] = "no";
|
||||
// }
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// ==========================================================================================
|
||||
// aliyunpan JS插件回调处理函数
|
||||
// 支持 JavaScript ECMAScript 5.1 语言规范
|
||||
//
|
||||
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
|
||||
// ==========================================================================================
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:上传文件前的回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "albumDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户文件网盘ID
|
||||
// albumDriveId - 用户相册网盘ID
|
||||
//
|
||||
// params - 文件上传前参数
|
||||
// {
|
||||
// "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
|
||||
// "localFileName": "token.bat",
|
||||
// "localFileSize": 125330,
|
||||
// "localFileType": "file",
|
||||
// "localFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "driveId": "19519221",
|
||||
// "driveFilePath": "aliyunpan/Downloads/token.bat"
|
||||
// }
|
||||
// localFilePath - 本地文件绝对完整路径
|
||||
// localFileName - 本地文件名
|
||||
// localFileSize - 本地文件大小,单位B
|
||||
// localFileType - 本地文件类型,file-文件,folder-文件夹
|
||||
// localFileUpdatedAt - 文件修改时间
|
||||
// driveId - 准备上传的目标网盘ID
|
||||
// driveFilePath - 准备上传的目标网盘保存的路径,这个是相对路径,相对指定上传的目标文件夹
|
||||
//
|
||||
// 返回值说明
|
||||
// {
|
||||
// "uploadApproved": "yes",
|
||||
// "driveFilePath": "newfolder/token.bat"
|
||||
// }
|
||||
// uploadApproved - 该文件是否确认上传,yes-允许上传,no-禁止上传
|
||||
// driveFilePath - 文件保存的网盘路径,这个是相对路径,如果为空""代表保持原本的目标路径。
|
||||
// 这个改动要小心,会导致重名文件只会上传一个
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function uploadFilePrepareCallback(context, params) {
|
||||
var result = {
|
||||
"uploadApproved": "yes",
|
||||
"driveFilePath": ""
|
||||
};
|
||||
|
||||
// 所有的.dmg文件,网盘保存的文件增加后缀名为.exe,本地文件不改动
|
||||
if (params["localFilePath"].lastIndexOf(".dmg") > 0) {
|
||||
result["driveFilePath"] = params["driveFilePath"] + ".exe";
|
||||
}
|
||||
|
||||
// 禁止.txt文件上传
|
||||
if (params["localFilePath"].lastIndexOf(".txt") > 0) {
|
||||
result["uploadApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止password.key文件上传
|
||||
if (params["localFileName"] == "password.key") {
|
||||
result["uploadApproved"] = "no";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:上传文件结束的回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "albumDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户文件网盘ID
|
||||
// albumDriveId - 用户相册网盘ID
|
||||
//
|
||||
// params - 文件上传结束参数
|
||||
// {
|
||||
// "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
|
||||
// "localFileName": "token.bat",
|
||||
// "localFileSize": 125330,
|
||||
// "localFileType": "file",
|
||||
// "localFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "localFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "uploadResult": "success",
|
||||
// "driveId": "19519221",
|
||||
// "driveFilePath": "/tmp/test/aliyunpan/Downloads/token.bat"
|
||||
// }
|
||||
// localFilePath - 本地文件绝对完整路径
|
||||
// localFileName - 本地文件名
|
||||
// localFileSize - 本地文件大小,单位B
|
||||
// localFileType - 本地文件类型,file-文件,folder-文件夹
|
||||
// localFileUpdatedAt - 文件修改时间
|
||||
// localFileSha1 - 本地文件的SHA1。这个值不一定会有
|
||||
// uploadResult - 上传结果,success-成功,fail-失败
|
||||
// driveId - 目标网盘ID
|
||||
// driveFilePath - 文件网盘保存的绝对路径
|
||||
//
|
||||
// 返回值说明
|
||||
// (没有返回值)
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function uploadFileFinishCallback(context, params) {
|
||||
console.log(params)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
@echo off
|
||||
|
||||
REM 配置环境变量
|
||||
REM set ALIYUNPAN_CONFIG_DIR=d:\path\to\your\aliyunpan\config
|
||||
|
||||
REM 指定refresh token用于登录
|
||||
aliyunpan login -RefreshToken=9078907....adg9087
|
||||
|
||||
REM 上传下载链接类型:1-默认 2-阿里ECS环境
|
||||
aliyunpan config set -transfer_url_type 1
|
||||
|
||||
REM 指定配置参数并进行启动
|
||||
REM 支持的模式:upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)
|
||||
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/备份盘/我的文档" -mode "upload"
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 请更改成你自己电脑上aliyunpan执行文件所在的目录
|
||||
#cd /path/to/aliyunpan/folder
|
||||
|
||||
# 配置环境变量
|
||||
#export ALIYUNPAN_CONFIG_DIR=/path/to/your/aliyunpan/config
|
||||
|
||||
chmod +x ./aliyunpan
|
||||
|
||||
# 指定refresh token用于登录
|
||||
./aliyunpan login -RefreshToken=9078907....adg9087
|
||||
|
||||
# 上传下载链接类型:1-默认 2-阿里ECS环境
|
||||
./aliyunpan config set -transfer_url_type 1
|
||||
|
||||
# 指定配置参数并进行启动
|
||||
# 支持的模式:upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)
|
||||
./aliyunpan sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload"
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
@echo off
|
||||
|
||||
REM 配置环境变量
|
||||
REM set ALIYUNPAN_CONFIG_DIR=d:\path\to\your\aliyunpan\config
|
||||
|
||||
REM 指定refresh token用于登录
|
||||
aliyunpan login -RefreshToken=9078907....adg9087
|
||||
|
||||
Executable → Regular
+5
@@ -1,6 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 请更改成你自己电脑上aliyunpan执行文件所在的目录
|
||||
#cd /path/to/aliyunpan/folder
|
||||
|
||||
# 配置环境变量
|
||||
#export ALIYUNPAN_CONFIG_DIR=/path/to/your/aliyunpan/config
|
||||
|
||||
chmod +x ./aliyunpan
|
||||
|
||||
# 指定refresh token用于登录
|
||||
@@ -0,0 +1,18 @@
|
||||
# linux service 配置文件模版
|
||||
# centos系统默认存放路径为:/lib/systemd/system/aliyunpansync.service
|
||||
# 启动服务:systemctl start aliyunpansync
|
||||
# 停止服务:systemctl stop aliyunpansync
|
||||
|
||||
[Unit]
|
||||
Description=aliyunpansync
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
ExecStart=/path/to/start.sh
|
||||
ExecStop=/path/to/stop.sh
|
||||
PrivateTmp=true
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 更改路径为你电脑sync.sh文件所在文件夹路径
|
||||
cd /path/to/your/sync.sh/folder
|
||||
nohup ./sync.sh >/dev/null 2>&1 &
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
PROCESS=`ps -ef|grep aliyunpan|grep -v grep|grep -v PPID|awk '{ print $2}'`
|
||||
for i in $PROCESS
|
||||
do
|
||||
# kill进程
|
||||
kill -9 $i
|
||||
break
|
||||
done
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"configVer": "1.0",
|
||||
"syncTaskList": [
|
||||
{
|
||||
"name": "设计文档备份",
|
||||
"localFolderPath": "D:\\tickstep\\Documents\\设计文档",
|
||||
"panFolderPath": "/sync_drive/我的文档",
|
||||
"mode": "upload",
|
||||
"priority": "local"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -75,8 +75,10 @@ Pack() {
|
||||
fi
|
||||
|
||||
cp README.md "$output/$1"
|
||||
cp webdav.sh "$output/$1"
|
||||
cp webdav.bat "$output/$1"
|
||||
cp docs/manual.md "$output/$1"
|
||||
cp -rf assets/scripts/* "$output/$1"
|
||||
cp -rf assets/plugin "$output/$1"
|
||||
cp -rf assets/sync_drive "$output/$1"
|
||||
|
||||
cd $output
|
||||
zip -q -r "$1.zip" "$1"
|
||||
|
||||
+25
-2
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdliner"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"sync"
|
||||
@@ -75,7 +76,29 @@ func TryLogin() *config.PanUser {
|
||||
// login
|
||||
_, webToken, err := DoLoginHelper(u.RefreshToken)
|
||||
if err != nil {
|
||||
logger.Verboseln("automatically login error")
|
||||
logger.Verboseln("automatically login use saved refresh token error ", err)
|
||||
if u.TokenId != "" {
|
||||
logger.Verboseln("try to login use tokenId")
|
||||
h := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
|
||||
r, e := h.GetRefreshToken(u.TokenId)
|
||||
if e != nil {
|
||||
logger.Verboseln("try to login use tokenId error", e)
|
||||
break
|
||||
}
|
||||
refreshToken, e := h.ParseSecureRefreshToken("", r.SecureRefreshToken)
|
||||
if e != nil {
|
||||
logger.Verboseln("try to parse refresh token error", e)
|
||||
break
|
||||
}
|
||||
_, webToken, err = DoLoginHelper(refreshToken)
|
||||
if err != nil {
|
||||
logger.Verboseln("try to use refresh token from tokenId error", e)
|
||||
break
|
||||
}
|
||||
fmt.Println("Token重新自动登录成功")
|
||||
// save new refresh token
|
||||
u.RefreshToken = refreshToken
|
||||
}
|
||||
break
|
||||
}
|
||||
// success
|
||||
@@ -89,4 +112,4 @@ func TryLogin() *config.PanUser {
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,3 +123,8 @@ func WalkDir(dirPth, suffix string) (files []string, err error) {
|
||||
func ConvertToUnixPathSeparator(p string) string {
|
||||
return strings.Replace(p, "\\", "/", -1)
|
||||
}
|
||||
|
||||
// ConvertToWindowsPathSeparator 将路径中的所有分隔符转换成windows格式
|
||||
func ConvertToWindowsPathSeparator(p string) string {
|
||||
return strings.Replace(p, "/", "\\", -1)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# alpine:3.15.0
|
||||
|
||||
# 参数
|
||||
ARG DOCKER_IMAGE_HASH
|
||||
|
||||
FROM alpine@sha256:$DOCKER_IMAGE_HASH
|
||||
LABEL author="tickstep"
|
||||
LABEL email="tickstep@outlook.com"
|
||||
LABEL version="1.0"
|
||||
LABEL description="sync & backup service for aliyun cloud drive"
|
||||
|
||||
# 时区
|
||||
ENV TZ=Asia/Shanghai
|
||||
# 手动下载tzdata安装包,注意要下载对应架构的: https://dl-cdn.alpinelinux.org/alpine/v3.15/community/
|
||||
RUN apk add -U tzdata
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
# 创建运行目录
|
||||
RUN mkdir -p /home/app
|
||||
VOLUME /home/app
|
||||
WORKDIR /home/app
|
||||
RUN cd /home/app
|
||||
|
||||
# 创建配置文件目录
|
||||
RUN mkdir -p /home/app/config
|
||||
|
||||
# 创建数据文件夹
|
||||
RUN mkdir -p /home/app/data
|
||||
RUN chmod 777 /home/app/data
|
||||
|
||||
# 复制文件
|
||||
COPY ./docker/sync/app.sh /home/app/app.sh
|
||||
RUN chmod +x /home/app/app.sh
|
||||
COPY ./docker/sync/health_check.sh /home/app/health_check.sh
|
||||
RUN chmod +x /home/app/health_check.sh
|
||||
|
||||
COPY ./out/binary_files/aliyunpan /home/app
|
||||
RUN mkdir -p /home/app/config/plugin
|
||||
COPY ./out/binary_files/plugin /home/app/config/plugin
|
||||
RUN mkdir -p /home/app/config/sync_drive
|
||||
COPY ./out/binary_files/sync_drive /home/app/config/sync_drive
|
||||
#RUN chmod +x /home/app/aliyunpan
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --start-period=5s --interval=10s --timeout=5s --retries=3 CMD /bin/sh /home/app/health_check.sh
|
||||
|
||||
# 环境变量
|
||||
ENV ALIYUNPAN_DOCKER=1
|
||||
ENV ALIYUNPAN_CONFIG_DIR=/home/app/config
|
||||
|
||||
ENV ALIYUNPAN_REFRESH_TOKEN=""
|
||||
ENV ALIYUNPAN_TRANSFER_URL_TYPE=0
|
||||
|
||||
ENV ALIYUNPAN_DOWNLOAD_PARALLEL=2
|
||||
ENV ALIYUNPAN_UPLOAD_PARALLEL=2
|
||||
ENV ALIYUNPAN_DOWNLOAD_BLOCK_SIZE=1024
|
||||
ENV ALIYUNPAN_UPLOAD_BLOCK_SIZE=10240
|
||||
ENV ALIYUNPAN_LOCAL_DIR=/home/app/data
|
||||
ENV ALIYUNPAN_PAN_DIR=/sync_drive
|
||||
ENV ALIYUNPAN_SYNC_MODE=upload
|
||||
ENV ALIYUNPAN_SYNC_PRIORITY=time
|
||||
ENV ALIYUNPAN_SYNC_LOG=false
|
||||
ENV ALIYUNPAN_LOCAL_DELAY_TIME=3
|
||||
ENV ALIYUNPAN_TASK_STEP=sync
|
||||
|
||||
# 运行
|
||||
ENTRYPOINT ./app.sh
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/bin/sh
|
||||
cd /home/app
|
||||
chmod +x ./aliyunpan
|
||||
|
||||
# sync config file
|
||||
readonly sync_drive_config_file="$ALIYUNPAN_CONFIG_DIR/sync_drive/sync_drive_config.json"
|
||||
if test -s $sync_drive_config_file
|
||||
then
|
||||
echo "using existed sync_drive_config.json file"
|
||||
else
|
||||
echo "generate sync_drive_config.json file"
|
||||
tee $sync_drive_config_file << EOF
|
||||
{
|
||||
"configVer": "1.0",
|
||||
"syncTaskList": [
|
||||
{
|
||||
"name": "阿里云盘备份",
|
||||
"id": "5b2d7c10-e927-4e72-8f9d-5abb3bb04814",
|
||||
"localFolderPath": "$ALIYUNPAN_LOCAL_DIR",
|
||||
"panFolderPath": "$ALIYUNPAN_PAN_DIR",
|
||||
"mode": "$ALIYUNPAN_SYNC_MODE",
|
||||
"priority": "$ALIYUNPAN_SYNC_PRIORITY",
|
||||
"lastSyncTime": "2022-06-12 19:28:20"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
sleep 2s
|
||||
|
||||
# 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 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}
|
||||
@@ -0,0 +1,39 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
sync:
|
||||
image: tickstep/aliyunpan-sync:v0.1.6
|
||||
container_name: aliyunpan-sync
|
||||
restart: always
|
||||
volumes:
|
||||
- ./data:/home/app/data:rw
|
||||
# (可选)可以指定JS插件sync_handler.js用于过滤文件,详见插件说明
|
||||
#- ./plugin/js/sync_handler.js:/home/app/config/plugin/js/sync_handler.js
|
||||
# (推荐)挂载sync_drive同步数据库到本地,这样即使容器销毁,同步数据库还可以用于以后使用
|
||||
#- ./sync_drive:/home/app/config/sync_drive
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
# refresh token
|
||||
- ALIYUNPAN_REFRESH_TOKEN=8bf31a...7628a8ad5a0975bf2d9
|
||||
# 上传下载链接类型:1-默认 2-阿里ECS环境
|
||||
- ALIYUNPAN_TRANSFER_URL_TYPE=1
|
||||
# 下载文件并发数
|
||||
- ALIYUNPAN_DOWNLOAD_PARALLEL=2
|
||||
# 上传文件并发数
|
||||
- ALIYUNPAN_UPLOAD_PARALLEL=2
|
||||
# 下载数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
|
||||
- ALIYUNPAN_DOWNLOAD_BLOCK_SIZE=1024
|
||||
# 上传数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
|
||||
- ALIYUNPAN_UPLOAD_BLOCK_SIZE=10240
|
||||
# 指定网盘文件夹作为备份目录,不要指定根目录
|
||||
- ALIYUNPAN_PAN_DIR=/my_sync_dir
|
||||
# 备份模式:upload(备份本地文件到云盘), download(备份云盘文件到本地), sync(双向同步备份,慎用!!!)
|
||||
- ALIYUNPAN_SYNC_MODE=upload
|
||||
# 优先级,只对双向同步备份模式有效。选项支持三种: time-时间优先,local-本地优先,pan-网盘优先
|
||||
- ALIYUNPAN_SYNC_PRIORITY=time
|
||||
# 是否显示文件备份过程日志,true-显示,false-不显示
|
||||
- ALIYUNPAN_SYNC_LOG=true
|
||||
# 本地文件修改检测延迟间隔,单位秒。如果本地文件会被频繁修改,例如录制视频文件,配置好该时间可以避免上传未录制好的文件
|
||||
- ALIYUNPAN_LOCAL_DELAY_TIME=3
|
||||
# 任务步骤, 支持两种: scan(只扫描并建立同步数据库),sync(正常启动同步任务)
|
||||
- ALIYUNPAN_TASK_STEP=sync
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
# 检查aliyunpan进程是否存在
|
||||
ps | awk '{print $4}' | grep aliyunpan
|
||||
if [ $? == 0 ]
|
||||
then
|
||||
echo $?
|
||||
exit 0
|
||||
else
|
||||
echo $?
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -22,19 +22,34 @@ WORKDIR /home/app
|
||||
RUN cd /home/app
|
||||
|
||||
# 创建配置文件目录
|
||||
RUN mkdir /home/app/config
|
||||
RUN mkdir -p /home/app/config
|
||||
|
||||
# 复制文件
|
||||
COPY ./docker/app.sh /home/app/app.sh
|
||||
COPY ./docker/webdav/app.sh /home/app/app.sh
|
||||
RUN chmod +x /home/app/app.sh
|
||||
COPY ./docker/webdav/health_check.sh /home/app/health_check.sh
|
||||
RUN chmod +x /home/app/health_check.sh
|
||||
|
||||
COPY ./out/binary_files/aliyunpan /home/app
|
||||
RUN mkdir -p /home/app/config/plugin
|
||||
COPY ./out/binary_files/plugin /home/app/config/plugin
|
||||
RUN mkdir -p /home/app/config/sync_drive
|
||||
COPY ./out/binary_files/sync_drive /home/app/config/sync_drive
|
||||
#RUN chmod +x /home/app/aliyunpan
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --start-period=5s --interval=10s --timeout=5s --retries=3 CMD /bin/sh /home/app/health_check.sh
|
||||
|
||||
# 端口
|
||||
EXPOSE 23077
|
||||
|
||||
# 环境变量
|
||||
ENV ALIYUNPAN_DOCKER=1
|
||||
ENV ALIYUNPAN_CONFIG_DIR=/home/app/config
|
||||
ENV ALIYUNPAN_AUTH_USER=admin
|
||||
ENV ALIYUNPAN_AUTH_PASSWORD=admin
|
||||
ENV ALIYUNPAN_WEBDAV_MODE=rw
|
||||
ENV ALIYUNPAN_PAN_DRIVE=File
|
||||
ENV ALIYUNPAN_PAN_DIR=/
|
||||
ENV ALIYUNPAN_REFRESH_TOKEN=""
|
||||
ENV ALIYUNPAN_TRANSFER_URL_TYPE=0
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/bin/sh
|
||||
cd /home/app
|
||||
chmod +x ./aliyunpan
|
||||
|
||||
@@ -12,4 +13,4 @@ else
|
||||
fi
|
||||
|
||||
./aliyunpan config set -transfer_url_type ${ALIYUNPAN_TRANSFER_URL_TYPE}
|
||||
./aliyunpan webdav start -ip "0.0.0.0" -port 23077 -webdav_user "${ALIYUNPAN_AUTH_USER}" -webdav_password "${ALIYUNPAN_AUTH_PASSWORD}" -pan_dir_path "${ALIYUNPAN_PAN_DIR}" -bs ${ALIYUNPAN_BLOCK_SIZE}
|
||||
./aliyunpan webdav start -ip "0.0.0.0" -port 23077 -webdav_user "${ALIYUNPAN_AUTH_USER}" -webdav_password "${ALIYUNPAN_AUTH_PASSWORD}" -webdav_mode "${ALIYUNPAN_WEBDAV_MODE}" -pan_drive "${ALIYUNPAN_PAN_DRIVE}" -pan_dir_path "${ALIYUNPAN_PAN_DIR}" -bs ${ALIYUNPAN_BLOCK_SIZE}
|
||||
@@ -2,7 +2,7 @@ version: '3'
|
||||
|
||||
services:
|
||||
webdav:
|
||||
image: tickstep/aliyunpan-webdav:tag
|
||||
image: tickstep/aliyunpan-webdav:v0.1.6
|
||||
container_name: aliyunpan-webdav
|
||||
restart: always
|
||||
ports:
|
||||
@@ -10,11 +10,15 @@ services:
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
# refresh token
|
||||
- ALIYUNPAN_REFRESH_TOKEN=b9123...13e66a1
|
||||
- ALIYUNPAN_REFRESH_TOKEN=4dd40e8b165...42b8833e4a507b6
|
||||
# webdav 登录用户名
|
||||
- ALIYUNPAN_AUTH_USER=admin
|
||||
# webdav 登录密码
|
||||
- ALIYUNPAN_AUTH_PASSWORD=admin
|
||||
# webdav 网盘模式,支持:rw-读写,ro-只读
|
||||
- ALIYUNPAN_WEBDAV_MODE=rw
|
||||
# 指定网盘类型为文件,可选: File-文件 Album-相册
|
||||
- ALIYUNPAN_PAN_DRIVE=File
|
||||
# 指定网盘文件夹作为webdav服务根目录
|
||||
- ALIYUNPAN_PAN_DIR=/
|
||||
# 上传下载链接类型:1-默认 2-阿里ECS环境
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 检查webdav的服务端口是否存在
|
||||
netstat -luntp | awk '{print $4}' | grep 23077
|
||||
if [ $? == 0 ]
|
||||
then
|
||||
echo $?
|
||||
exit 0
|
||||
else
|
||||
echo $?
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
-162
@@ -1,162 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# how to use
|
||||
# for macOS & linux, run this command in shell
|
||||
# ./docker_build.sh v0.0.1
|
||||
|
||||
name="aliyunpan"
|
||||
version=$1
|
||||
docker_image_name=tickstep/aliyunpan-webdav
|
||||
|
||||
if [ "$1" = "" ]; then
|
||||
version=v1.0.0
|
||||
fi
|
||||
|
||||
# build amd64 docker
|
||||
echo "building amd64 docker image"
|
||||
ARCH=amd64
|
||||
ZIP_FILE_NAME=aliyunpan-$version-linux-$ARCH
|
||||
# use alpine:3.15.0 as base image
|
||||
# browse the url to view more information: https://hub.docker.com/layers/alpine/library/alpine/3.15.0/images/sha256-c74f1b1166784193ea6c8f9440263b9be6cae07dfe35e32a5df7a31358ac2060?context=explore
|
||||
DOCKER_IMAGE_HASH=e7d88de73db3d3fd9b2d63aa7f447a10fd0220b7cbf39803c803f2af9ba256b3
|
||||
unzip -d ./out ./out/$ZIP_FILE_NAME.zip
|
||||
mv ./out/$ZIP_FILE_NAME ./out/binary_files
|
||||
|
||||
docker build \
|
||||
-f ./docker/Dockerfile \
|
||||
-t $docker_image_name:$version-$ARCH \
|
||||
--build-arg DOCKER_IMAGE_HASH=$DOCKER_IMAGE_HASH \
|
||||
--no-cache .
|
||||
|
||||
rm -rf out/binary_files
|
||||
|
||||
# build i386 docker
|
||||
echo "building i386 docker image"
|
||||
ARCH=386
|
||||
ZIP_FILE_NAME=aliyunpan-$version-linux-$ARCH
|
||||
DOCKER_IMAGE_HASH=2689e157117d2da668ad4699549e55eba1ceb79cb7862368b30919f0488213f4
|
||||
unzip -d ./out ./out/$ZIP_FILE_NAME.zip
|
||||
mv ./out/$ZIP_FILE_NAME ./out/binary_files
|
||||
|
||||
docker build \
|
||||
-f ./docker/Dockerfile \
|
||||
-t $docker_image_name:$version-$ARCH \
|
||||
--build-arg DOCKER_IMAGE_HASH=$DOCKER_IMAGE_HASH \
|
||||
--no-cache .
|
||||
|
||||
rm -rf out/binary_files
|
||||
|
||||
# build arm64 docker
|
||||
echo "building arm64 docker image"
|
||||
ARCH=arm64
|
||||
ZIP_FILE_NAME=aliyunpan-$version-linux-$ARCH
|
||||
DOCKER_IMAGE_HASH=c74f1b1166784193ea6c8f9440263b9be6cae07dfe35e32a5df7a31358ac2060
|
||||
unzip -d ./out ./out/$ZIP_FILE_NAME.zip
|
||||
mv ./out/$ZIP_FILE_NAME ./out/binary_files
|
||||
|
||||
docker build \
|
||||
-f ./docker/Dockerfile \
|
||||
-t $docker_image_name:$version-$ARCH \
|
||||
--build-arg DOCKER_IMAGE_HASH=$DOCKER_IMAGE_HASH \
|
||||
--no-cache .
|
||||
|
||||
rm -rf out/binary_files
|
||||
|
||||
# build armv7 docker
|
||||
echo "building armv7 docker image"
|
||||
ARCH=armv7
|
||||
ZIP_FILE_NAME=aliyunpan-$version-linux-$ARCH
|
||||
DOCKER_IMAGE_HASH=8483ecd016885d8dba70426fda133c30466f661bb041490d525658f1aac73822
|
||||
unzip -d ./out ./out/$ZIP_FILE_NAME.zip
|
||||
mv ./out/$ZIP_FILE_NAME ./out/binary_files
|
||||
|
||||
docker build \
|
||||
-f ./docker/Dockerfile \
|
||||
-t $docker_image_name:$version-$ARCH \
|
||||
--build-arg DOCKER_IMAGE_HASH=$DOCKER_IMAGE_HASH \
|
||||
--no-cache .
|
||||
|
||||
rm -rf out/binary_files
|
||||
|
||||
# build armv5 docker
|
||||
echo "building armv5 docker image"
|
||||
ARCH=armv5
|
||||
ZIP_FILE_NAME=aliyunpan-$version-linux-$ARCH
|
||||
DOCKER_IMAGE_HASH=e047bc2af17934d38c5a7fa9f46d443f1de3a7675546402592ef805cfa929f9d
|
||||
unzip -d ./out ./out/$ZIP_FILE_NAME.zip
|
||||
mv ./out/$ZIP_FILE_NAME ./out/binary_files
|
||||
|
||||
docker build \
|
||||
-f ./docker/Dockerfile \
|
||||
-t $docker_image_name:$version-$ARCH \
|
||||
--build-arg DOCKER_IMAGE_HASH=$DOCKER_IMAGE_HASH \
|
||||
--no-cache .
|
||||
|
||||
rm -rf out/binary_files
|
||||
|
||||
echo "push docker images"
|
||||
docker push $docker_image_name:$version-amd64
|
||||
docker push $docker_image_name:$version-386
|
||||
docker push $docker_image_name:$version-arm64
|
||||
docker push $docker_image_name:$version-armv7
|
||||
docker push $docker_image_name:$version-armv5
|
||||
|
||||
echo "create docker manifest"
|
||||
docker manifest create $docker_image_name:$version \
|
||||
$docker_image_name:$version-amd64 \
|
||||
$docker_image_name:$version-386 \
|
||||
$docker_image_name:$version-arm64 \
|
||||
$docker_image_name:$version-armv7 \
|
||||
$docker_image_name:$version-armv5 \
|
||||
--amend
|
||||
|
||||
|
||||
echo "annotate manifest for amd64 image"
|
||||
docker manifest annotate \
|
||||
--arch=amd64 \
|
||||
--os=linux \
|
||||
$docker_image_name:$version \
|
||||
$docker_image_name:$version-amd64
|
||||
|
||||
echo "annotate manifest for 386 image"
|
||||
docker manifest annotate \
|
||||
--arch=386 \
|
||||
--os=linux \
|
||||
$docker_image_name:$version \
|
||||
$docker_image_name:$version-386
|
||||
|
||||
echo "annotate manifest for arm64 image"
|
||||
docker manifest annotate \
|
||||
--arch=arm64 \
|
||||
--os=linux \
|
||||
--variant=v8 \
|
||||
$docker_image_name:$version \
|
||||
$docker_image_name:$version-arm64
|
||||
|
||||
echo "annotate manifest for armv7 image"
|
||||
docker manifest annotate \
|
||||
--arch=arm \
|
||||
--os=linux \
|
||||
--variant=v7 \
|
||||
$docker_image_name:$version \
|
||||
$docker_image_name:$version-armv7
|
||||
|
||||
echo "annotate manifest for armv5 image"
|
||||
docker manifest annotate \
|
||||
--arch=arm \
|
||||
--os=linux \
|
||||
--variant=v6 \
|
||||
$docker_image_name:$version \
|
||||
$docker_image_name:$version-armv5
|
||||
|
||||
echo "push manifest to docker hub"
|
||||
docker manifest push $docker_image_name:$version
|
||||
|
||||
echo "clear local docker image"
|
||||
docker rmi $docker_image_name:$version-amd64
|
||||
docker rmi $docker_image_name:$version-386
|
||||
docker rmi $docker_image_name:$version-arm64
|
||||
docker rmi $docker_image_name:$version-armv7
|
||||
docker rmi $docker_image_name:$version-armv5
|
||||
|
||||
echo "ALL DONE"
|
||||
+1290
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,17 @@ go 1.16
|
||||
|
||||
require (
|
||||
github.com/GeertJohan/go.incremental v1.0.0
|
||||
github.com/json-iterator/go v1.1.10
|
||||
github.com/boltdb/bolt v1.3.1 // indirect
|
||||
github.com/deckarep/golang-set v1.8.0
|
||||
github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb
|
||||
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce
|
||||
github.com/peterh/liner v1.2.1
|
||||
github.com/tickstep/aliyunpan-api v0.0.4
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/tickstep/aliyunpan-api v0.1.0
|
||||
github.com/tickstep/bolt v1.3.3
|
||||
github.com/tickstep/library-go v0.0.5
|
||||
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f
|
||||
|
||||
@@ -1,138 +1,118 @@
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
|
||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
|
||||
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
|
||||
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4=
|
||||
github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo=
|
||||
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
|
||||
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E=
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
|
||||
github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1 h1:9/4Hyp+A98nqssD7qceXwoLeAn75MZr25IN76dxlfbg=
|
||||
github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1/go.mod h1:TQJQ+ZNyFVvUtUEtCZxBhfWiH7RJqR3EivNmvD6Waik=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d h1:W1n4DvpzZGOISgp7wWNtraLcHtnmnTwBlJidqtMIuwQ=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro=
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
|
||||
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb h1:x0yCvYsspui5SAxSRvLd2zFg7PfFijzKdCo7QAtN92I=
|
||||
github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb/go.mod h1:ym0w0flrmBtGvApLDgFLa0sfGJkWxDQqnm0/0ok5w3Y=
|
||||
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
|
||||
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce h1:RLmZmfx/K62HKpbwPqtW3tg+V2GgugN/XNNx+uiMH/Y=
|
||||
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
|
||||
github.com/peterh/liner v1.1.1-0.20190305032635-6f820f8f90ce h1:Lz+a/i+oS4A7tb6J6IyH4ZFiWgqvNv2yslv0Qn79wok=
|
||||
github.com/peterh/liner v1.1.1-0.20190305032635-6f820f8f90ce/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
|
||||
github.com/peterh/liner v1.2.1 h1:O4BlKaq/LWu6VRWmol4ByWfzx6MfXc5Op5HETyIy5yg=
|
||||
github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk=
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd h1:ug7PpSOB5RBPK1Kg6qskGBoP3Vnj/aNYFTznWvlkGo0=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tickstep/aliyunpan-api v0.0.2 h1:aXChXeb1vmpINqV2qf60Bi3SGKgZPdmV7D8Eymb29Yw=
|
||||
github.com/tickstep/aliyunpan-api v0.0.2/go.mod h1:EdciagdVeAHfqvzStVEVp1UVtySaq1DFd5RSucDfKWY=
|
||||
github.com/tickstep/aliyunpan-api v0.0.3 h1:DBxRIjRUuebIjAgAPtS7rYZ4tafzOEF6J2949V2FeDY=
|
||||
github.com/tickstep/aliyunpan-api v0.0.3/go.mod h1:EdciagdVeAHfqvzStVEVp1UVtySaq1DFd5RSucDfKWY=
|
||||
github.com/tickstep/aliyunpan-api v0.0.4 h1:+ct8x6F6vpkezi3EMLb8zDBoWdQBA+BFuzWiYWf8q1s=
|
||||
github.com/tickstep/aliyunpan-api v0.0.4/go.mod h1:EdciagdVeAHfqvzStVEVp1UVtySaq1DFd5RSucDfKWY=
|
||||
github.com/tickstep/aliyunpan-api v0.1.0 h1:wTSCoBhQ1fBVJ9MGEDvrzdFR+uF8b6Ji2pDvY+/DL0g=
|
||||
github.com/tickstep/aliyunpan-api v0.1.0/go.mod h1:ZFtMpbmVPM6+zWRTQ7QHw6xzwN/WTm3zaxQjFZ54UAA=
|
||||
github.com/tickstep/bolt v1.3.3 h1:3abb88T5JJocnBlYTJE3EqKvwWV6+D/JZD2Fsdt1QHI=
|
||||
github.com/tickstep/bolt v1.3.3/go.mod h1:Y24RwCywbOsKAyHiVcJ4K2QQfED+q67x7PEOH9OG7Q4=
|
||||
github.com/tickstep/cloudpan189-api v0.0.3 h1:L8woe6XSUjQOsa7XRC7zH8VZj8SNKo/1qQb1k2ttLkU=
|
||||
github.com/tickstep/cloudpan189-api v0.0.3/go.mod h1:1H1r6h5fOu8sEhPY6h09YTuSt4OKl5uFJchFscBFRwo=
|
||||
github.com/tickstep/cloudpan189-api v0.0.4 h1:qsEhqvL2L5P6EHP+vfLhVqyjJbcHUkE9JEfUiyu1z0g=
|
||||
github.com/tickstep/cloudpan189-api v0.0.4/go.mod h1:1H1r6h5fOu8sEhPY6h09YTuSt4OKl5uFJchFscBFRwo=
|
||||
github.com/tickstep/cloudpan189-api v0.0.5 h1:R46M4op2aABKJgR2S8c/KUe/fIQYbUpQGxlJ9SUnsBo=
|
||||
github.com/tickstep/cloudpan189-api v0.0.5/go.mod h1:HYu2wtfBDRldAnohj9UmNpEZipQDMm2wOYszNuLzZ9s=
|
||||
github.com/tickstep/cloudpan189-api v0.0.6 h1:b+ctfvWfOVyq/Zot3rCrJxqf85b+toDSNLeGon4OWQo=
|
||||
github.com/tickstep/cloudpan189-api v0.0.6/go.mod h1:qhNVXxF1UGOApXy9uG+UuKHReFI1GpwHn0pzL4tqSd8=
|
||||
github.com/tickstep/library-go v0.0.1 h1:UbXeGE6ZxnxA6KTjMofEhG3h2aHJ4UGacfh7U2B4zgw=
|
||||
github.com/tickstep/library-go v0.0.1/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
|
||||
github.com/tickstep/library-go v0.0.2 h1:0JCxT2ZzRMrydUyqou3d9FI44ULrlxnkFcyioaGUbrE=
|
||||
github.com/tickstep/library-go v0.0.2/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
|
||||
github.com/tickstep/library-go v0.0.3 h1:j6C2qIYMC33Re/qa3cYS49DSMalgjtLR4KqGi6d1qBI=
|
||||
github.com/tickstep/library-go v0.0.3/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
|
||||
github.com/tickstep/library-go v0.0.4 h1:wNR48yDlaikxdJokL/NYX0J4ul8NN+TmBfXjxO2kfNw=
|
||||
github.com/tickstep/library-go v0.0.4/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
|
||||
github.com/tickstep/library-go v0.0.5 h1:MBb1tsvs4Wi67zy0E9eobVWLgsfPRLsqKAEdSEi3LBE=
|
||||
github.com/tickstep/library-go v0.0.5/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
|
||||
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f h1:xKDKjIsL76VUyHcA0G4Qe1cIAUB/nrq6Pt8D411bd1g=
|
||||
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f/go.mod h1:qXyCeJubPqsgeiLd3kvHOGHHSrQcNdjZ2ScXIcVZK/I=
|
||||
github.com/xujiajun/gorouter v1.2.0/go.mod h1:yJrIta+bTNpBM/2UT8hLOaEAFckO+m/qmR3luMIQygM=
|
||||
github.com/xujiajun/mmap-go v1.0.1 h1:7Se7ss1fLPPRW+ePgqGpCkfGIZzJV6JPq9Wq9iv/WHc=
|
||||
github.com/xujiajun/mmap-go v1.0.1/go.mod h1:CNN6Sw4SL69Sui00p0zEzcZKbt+5HtEnYUsc6BKKRMg=
|
||||
github.com/xujiajun/nutsdb v0.5.1-0.20200830145825-432dd3d0c873 h1:pgTLmYRrfy9lGWgXrEqaIhn4WSJb9xA5Q+Uir2Dxeag=
|
||||
github.com/xujiajun/nutsdb v0.5.1-0.20200830145825-432dd3d0c873/go.mod h1:Q8FXi2zeQRluPpUl/CKQ6J7u/9gcI02J6cZp3owFLyA=
|
||||
github.com/xujiajun/utils v0.0.0-20190123093513-8bf096c4f53b h1:jKG9OiL4T4xQN3IUrhUpc1tG+HfDXppkgVcrAiiaI/0=
|
||||
github.com/xujiajun/utils v0.0.0-20190123093513-8bf096c4f53b/go.mod h1:AZd87GYJlUzl82Yab2kTjx1EyXSQCAfZDhpTo1SQC4k=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -0,0 +1,505 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
AlbumFileCategoryOption string
|
||||
)
|
||||
|
||||
var (
|
||||
ImageOnlyOption AlbumFileCategoryOption = "image"
|
||||
VideoOnlyOption AlbumFileCategoryOption = "video"
|
||||
ImageVideoOnlyOption AlbumFileCategoryOption = "image_video"
|
||||
AllFileOption AlbumFileCategoryOption = "none"
|
||||
)
|
||||
|
||||
func CmdAlbum() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "album",
|
||||
Aliases: []string{"abm"},
|
||||
Usage: "相簿(Beta)",
|
||||
UsageText: cmder.App().Name + " album",
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
},
|
||||
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "展示相簿列表",
|
||||
UsageText: cmder.App().Name + " album list",
|
||||
Description: `
|
||||
示例:
|
||||
|
||||
展示相簿列表
|
||||
aliyunpan album ls
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
RunAlbumList()
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "new",
|
||||
Aliases: []string{""},
|
||||
Usage: "创建相簿",
|
||||
UsageText: cmder.App().Name + " album new",
|
||||
Description: `
|
||||
示例:
|
||||
|
||||
新建相簿,名称为:我的相簿2022
|
||||
aliyunpan album new "我的相簿2022"
|
||||
|
||||
新建相簿,名称为:我的相簿2022,描述为:存放2022所有文件
|
||||
aliyunpan album new "我的相簿2022" "存放2022所有文件"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
RunAlbumCreate(c.Args().Get(0), c.Args().Get(1))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "rm",
|
||||
Aliases: []string{""},
|
||||
Usage: "删除相簿",
|
||||
UsageText: cmder.App().Name + " album rm",
|
||||
Description: `
|
||||
删除相簿,同名的相簿只会删除第一个符合条件的
|
||||
示例:
|
||||
|
||||
删除名称为"我的相簿2022"的相簿
|
||||
aliyunpan album rm "我的相簿2022"
|
||||
|
||||
删除名称为"我的相簿2022-1" 和 "我的相簿2022-2"的相簿
|
||||
aliyunpan album rm "我的相簿2022-1" "我的相簿2022-2"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
RunAlbumDelete(c.Args())
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "rename",
|
||||
Aliases: []string{""},
|
||||
Usage: "重命名相簿",
|
||||
UsageText: cmder.App().Name + " album rename",
|
||||
Description: `
|
||||
重命名相簿,同名的相簿只会修改第一个符合条件的
|
||||
示例:
|
||||
|
||||
重命名相簿"我的相簿2022"为新的名称"我的相簿2022-new"
|
||||
aliyunpan album rename "我的相簿2022" "我的相簿2022-new"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
RunAlbumRename(c.Args().Get(0), c.Args().Get(1))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "list-file",
|
||||
Aliases: []string{"lf"},
|
||||
Usage: "展示相簿中的文件",
|
||||
UsageText: cmder.App().Name + " album list-file",
|
||||
Description: `
|
||||
展示相簿中文件,同名的相簿只会展示第一个符合条件的
|
||||
示例:
|
||||
|
||||
展示相簿中文件"我的相簿2022"
|
||||
aliyunpan album list-file "我的相簿2022"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
RunAlbumListFile(c.Args().Get(0))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "rm-file",
|
||||
Aliases: []string{"rf"},
|
||||
Usage: "移除相簿中的文件",
|
||||
UsageText: cmder.App().Name + " album rm-file",
|
||||
Description: `
|
||||
移除相簿中的文件,同名的相簿只会移除第一个符合条件的
|
||||
示例:
|
||||
|
||||
移除相簿 "我的相簿2022" 中的文件 1.png 2.png
|
||||
aliyunpan album rm-file 我的相簿2022 1.png 2.png
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
if len(subArgs) < 2 {
|
||||
fmt.Println("请指定移除的文件")
|
||||
return nil
|
||||
}
|
||||
RunAlbumRmFile(subArgs[0], subArgs[1:])
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "add-file",
|
||||
Aliases: []string{"af"},
|
||||
Usage: "增加(文件/相册)网盘文件到相簿中",
|
||||
UsageText: cmder.App().Name + " album add-file",
|
||||
Description: `
|
||||
增加文件到相簿中
|
||||
示例:
|
||||
|
||||
增加当前目录下的 1.png 2.png 文件到相簿 "我的相簿2022" 中
|
||||
aliyunpan album add-file 我的相簿2022 1.png 2.png
|
||||
|
||||
增加当前目录下的 myFolder 文件夹下所有文件到相簿 "我的相簿2022" 中
|
||||
aliyunpan album add-file 我的相簿2022 myFolder
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
if len(subArgs) < 2 {
|
||||
fmt.Println("请指定增加的文件")
|
||||
return nil
|
||||
}
|
||||
RunAlbumAddFile(subArgs[0], subArgs[1:], ImageVideoOnlyOption)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunAlbumList() {
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().AlbumListGetAll(&aliyunpan.AlbumListParam{})
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "ALBUM_ID", "名称", "文件数量", "创建日期", "修改日期"})
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT})
|
||||
for k, record := range records {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), record.AlbumId, record.Name, strconv.Itoa(record.FileCount),
|
||||
record.CreatedAtStr(), record.UpdatedAtStr()})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
|
||||
func RunAlbumCreate(name, description string) {
|
||||
if name == "" {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
_, err := activeUser.PanClient().AlbumCreate(&aliyunpan.AlbumCreateParam{
|
||||
Name: name,
|
||||
Description: description,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("创建相簿失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("创建相簿成功: %s\n", name)
|
||||
}
|
||||
|
||||
func RunAlbumDelete(nameList []string) {
|
||||
if len(nameList) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().AlbumListGetAll(&aliyunpan.AlbumListParam{})
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
for i, name := range nameList {
|
||||
if name == record.Name {
|
||||
nameList = append(nameList[:i], nameList[i+1:]...)
|
||||
_, err := activeUser.PanClient().AlbumDelete(&aliyunpan.AlbumDeleteParam{
|
||||
AlbumId: record.AlbumId,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("删除相簿失败: %s\n", name)
|
||||
return
|
||||
} else {
|
||||
fmt.Printf("删除相簿成功: %s\n", name)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getAlbumFromName(activeUser *config.PanUser, name string) *aliyunpan.AlbumEntity {
|
||||
records, err := activeUser.PanClient().AlbumListGetAll(&aliyunpan.AlbumListParam{})
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
if name == record.Name {
|
||||
return record
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RunAlbumRename(name, newName string) {
|
||||
if len(name) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
if len(newName) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
record := getAlbumFromName(activeUser, name)
|
||||
if record == nil {
|
||||
return
|
||||
}
|
||||
_, err := activeUser.PanClient().AlbumEdit(&aliyunpan.AlbumEditParam{
|
||||
AlbumId: record.AlbumId,
|
||||
Description: record.Description,
|
||||
Name: newName,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("重命名相簿失败: %s\n", name)
|
||||
return
|
||||
} else {
|
||||
fmt.Printf("重命名相簿成功: %s -> %s\n", name, newName)
|
||||
}
|
||||
}
|
||||
|
||||
func RunAlbumListFile(name string) {
|
||||
if len(name) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
record := getAlbumFromName(activeUser, name)
|
||||
if record == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fileList, er := activeUser.PanClient().AlbumListFileGetAll(&aliyunpan.AlbumListFileParam{
|
||||
AlbumId: record.AlbumId,
|
||||
})
|
||||
if er != nil {
|
||||
fmt.Printf("获取相簿文件列表失败:%s\n", er)
|
||||
return
|
||||
}
|
||||
renderTable(opLs, false, "", fileList)
|
||||
}
|
||||
|
||||
func RunAlbumRmFile(name string, nameList []string) {
|
||||
if len(name) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
if len(nameList) == 0 {
|
||||
fmt.Printf("指定文件不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
album := getAlbumFromName(activeUser, name)
|
||||
if album == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fileList, er := activeUser.PanClient().AlbumListFileGetAll(&aliyunpan.AlbumListFileParam{
|
||||
AlbumId: album.AlbumId,
|
||||
})
|
||||
if er != nil {
|
||||
fmt.Printf("获取相簿文件列表失败:%s\n", er)
|
||||
return
|
||||
}
|
||||
param := &aliyunpan.AlbumDeleteFileParam{
|
||||
AlbumId: album.AlbumId,
|
||||
DriveFileList: []aliyunpan.FileBatchActionParam{},
|
||||
}
|
||||
for _, file := range fileList {
|
||||
if len(nameList) == 0 {
|
||||
break
|
||||
}
|
||||
for i, name := range nameList {
|
||||
if name == file.FileName {
|
||||
nameList = append(nameList[:i], nameList[i+1:]...)
|
||||
param.AddFileItem(file.DriveId, file.FileId)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1-500 范围
|
||||
if len(param.DriveFileList) == 0 {
|
||||
fmt.Printf("没有符合的文件\n")
|
||||
return
|
||||
}
|
||||
// delete file
|
||||
_, e := activeUser.PanClient().AlbumDeleteFile(param)
|
||||
if e != nil {
|
||||
fmt.Printf("删除相簿文件失败:%s\n", e)
|
||||
return
|
||||
}
|
||||
fmt.Printf("删除相簿文件成功:%s\n", name)
|
||||
}
|
||||
|
||||
// RunAlbumAddFile 增加网盘文件到相簿
|
||||
func RunAlbumAddFile(albumName string, filePathList []string, filterOption AlbumFileCategoryOption) {
|
||||
activeUser := GetActiveUser()
|
||||
|
||||
if albumName == "" {
|
||||
fmt.Printf("必须指定相簿名称\n")
|
||||
return
|
||||
}
|
||||
album := getAlbumFromName(activeUser, albumName)
|
||||
if album == nil {
|
||||
fmt.Printf("相簿不存在\n")
|
||||
return
|
||||
}
|
||||
|
||||
paths, err := matchPathByShellPattern(activeUser.ActiveDriveId, filePathList...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if len(paths) == 0 {
|
||||
fmt.Printf("没有有效的文件\n")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("正在获取增加的文件信息,该操作可能会非常耗费时间,请耐心等待...\n")
|
||||
param := &aliyunpan.AlbumAddFileParam{
|
||||
AlbumId: album.AlbumId,
|
||||
DriveFileList: []aliyunpan.FileBatchActionParam{},
|
||||
}
|
||||
for k := range paths {
|
||||
filePath := paths[k]
|
||||
fileInfo, apierr := activeUser.PanClient().FileInfoByPath(activeUser.ActiveDriveId, filePath)
|
||||
if apierr != nil {
|
||||
fmt.Printf("获取文件信息失败: %s\n", filePath)
|
||||
continue
|
||||
}
|
||||
if fileInfo.IsFile() {
|
||||
// file
|
||||
if isFileMatchCondition(fileInfo, filterOption) {
|
||||
param.AddFileItem(fileInfo.DriveId, fileInfo.FileId)
|
||||
}
|
||||
} else {
|
||||
// folder
|
||||
activeUser.PanClient().FilesDirectoriesRecurseList(activeUser.ActiveDriveId, fileInfo.Path, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
|
||||
if apiError != nil {
|
||||
logger.Verbosef("%s\n", apiError)
|
||||
return true
|
||||
}
|
||||
if !fd.IsFolder() {
|
||||
if isFileMatchCondition(fd, filterOption) {
|
||||
param.AddFileItem(fd.DriveId, fd.FileId)
|
||||
}
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
return true
|
||||
})
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
if len(param.DriveFileList) == 0 {
|
||||
fmt.Printf("没有符合的文件\n")
|
||||
return
|
||||
}
|
||||
// add file
|
||||
_, e := activeUser.PanClient().AlbumAddFile(param)
|
||||
if e != nil {
|
||||
fmt.Printf("增加相簿文件失败:%s\n", e)
|
||||
return
|
||||
}
|
||||
fmt.Printf("增加相簿文件成功:%s\n", albumName)
|
||||
}
|
||||
|
||||
func isFileMatchCondition(fileInfo *aliyunpan.FileEntity, filterOption AlbumFileCategoryOption) bool {
|
||||
if fileInfo == nil {
|
||||
return false
|
||||
}
|
||||
if filterOption == ImageOnlyOption {
|
||||
return fileInfo.Category == "image"
|
||||
} else if filterOption == VideoOnlyOption {
|
||||
return fileInfo.Category == "video"
|
||||
} else if filterOption == ImageVideoOnlyOption {
|
||||
return fileInfo.Category == "image" || fileInfo.Category == "video"
|
||||
} else if filterOption == AllFileOption {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
// Copyright (c) 2020 tickstep
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panupload"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func CmdBackup() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "backup",
|
||||
Description: `备份指定 <文件/目录> 到云盘 <目标目录> 中
|
||||
|
||||
和上传的功能一样,只是备份多进行了如下操作
|
||||
|
||||
1. 增加了数据库,记录已经上传的文件信息。
|
||||
目前只记录 文件位置、大小、修改时间、MD5 。
|
||||
2. 上传前先根据数据库记录判断是否需要重新上传。
|
||||
3. 强制同名覆盖。
|
||||
|
||||
注:只备份(上传)新的文件(同名覆盖),不处理删除操作。
|
||||
|
||||
示例:
|
||||
1. 将本地的 C:\Users\Administrator\Video 整个目录备份到网盘 /视频 目录
|
||||
注意区别反斜杠 "\" 和 斜杠 "/" !!!
|
||||
aliyunpan-go backup C:/Users/Administrator/Video /视频
|
||||
|
||||
2. 将本地的 C:\Users\Administrator\Video 整个目录备份到网盘 /视频 目录,但是排除所有的.jpg文件
|
||||
aliyunpan-go backup -exn "\.jpg$" C:/Users/Administrator/Video /视频
|
||||
|
||||
3. 将本地的 C:\Users\Administrator\Video 整个目录备份到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
|
||||
aliyunpan-go backup -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视频
|
||||
|
||||
4. 将本地的 C:\Users\Administrator\Video 整个目录备份到网盘 /视频 目录,但是排除所有的 @eadir 文件夹
|
||||
aliyunpan-go backup -exn "^@eadir$" C:/Users/Administrator/Video /视频
|
||||
|
||||
参考:
|
||||
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式。在正则表达式中,^表示匹配开头,$表示匹配结尾。
|
||||
1)排除@eadir文件或者文件夹:-exn "^@eadir$"
|
||||
2)排除.jpg文件:-exn "\.jpg$"
|
||||
3)排除.号开头的文件:-exn "^\."
|
||||
4)排除~号开头的文件:-exn "^~"
|
||||
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
|
||||
`,
|
||||
Usage: "备份文件或目录",
|
||||
UsageText: "backup <文件/目录路径1> <文件/目录2> <文件/目录3> ... <目标目录>",
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Action: Backup,
|
||||
Flags: append(UploadFlags, cli.BoolFlag{
|
||||
Name: "delete",
|
||||
Usage: "通过本地数据库记录同步删除网盘文件",
|
||||
}, cli.BoolFlag{
|
||||
Name: "sync",
|
||||
Usage: "本地同步到网盘(会同步删除网盘文件)",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func OpenSyncDb(path string) (panupload.SyncDb, error) {
|
||||
return panupload.OpenSyncDb(path, BackupMetaBucketName)
|
||||
}
|
||||
|
||||
// 删除那些本地不存在而网盘存在的网盘文件 默认使用本地数据库判断,如果 flagSync 为 true 则遍历网盘文件列表进行判断(速度较慢)。
|
||||
func DelRemoteFileFromDB(driveId string, localDir string, savePath string, flagSync bool) {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
var db panupload.SyncDb
|
||||
var err error
|
||||
|
||||
dbpath := filepath.Join(localDir, BackupMetaDirName)
|
||||
db, err = OpenSyncDb(dbpath + string(os.PathSeparator) + "db")
|
||||
if err != nil {
|
||||
fmt.Println("同步数据库打开失败!", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
savePath = path.Join(savePath, filepath.Base(localDir))
|
||||
|
||||
//判断本地文件是否存在,如果存在返回 true 否则删除数据库相关记录和网盘上的文件。
|
||||
isLocalFileExist := func(ent *panupload.UploadedFileMeta) (isExists bool) {
|
||||
testPath := strings.TrimPrefix(ent.Path, savePath)
|
||||
testPath = filepath.Join(localDir, testPath)
|
||||
logger.Verboseln("同步删除检测:", testPath, ent.Path)
|
||||
|
||||
//为防止误删,只有当 err 是文件不存在的时候才进行删除处理。
|
||||
if fi, err := os.Stat(testPath); err == nil || !os.IsNotExist(err) {
|
||||
//使用sync功能时没有传时间参数进来,为方便对比回写数据库需补上时间。
|
||||
if fi != nil {
|
||||
ent.ModTime = fi.ModTime().Unix()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var err *apierror.ApiError
|
||||
|
||||
// 尝试从本地数据库查找
|
||||
if ent.ParentId == "" {
|
||||
if test := db.Get(path.Dir(ent.Path)); test != nil && test.IsFolder && test.FileId != "" {
|
||||
ent.ParentId = test.FileId
|
||||
}
|
||||
}
|
||||
|
||||
// 从网盘查找
|
||||
if ent.FileId == "" || ent.ParentId == "" {
|
||||
efi, err := activeUser.PanClient().FileInfoById(driveId, ent.FileId)
|
||||
//网盘上不存在这个文件或目录,只需要清理数据库
|
||||
if err != nil && err.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
db.DelWithPrefix(ent.Path)
|
||||
logger.Verboseln("删除数据库记录", ent.Path)
|
||||
return
|
||||
}
|
||||
if efi != nil {
|
||||
ent.FileId = efi.FileId
|
||||
ent.ParentId = efi.ParentFileId
|
||||
}
|
||||
}
|
||||
|
||||
if ent.FileId == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 本地文件不存在
|
||||
// 删除网盘对应文件
|
||||
fileDeleteResult, err := activeUser.PanClient().FileDelete([]*aliyunpan.FileBatchActionParam{{DriveId:driveId, FileId:ent.FileId}})
|
||||
if err != nil || len(fileDeleteResult) == 0 {
|
||||
fmt.Println("删除网盘文件或目录失败", ent.Path, err)
|
||||
} else {
|
||||
db.DelWithPrefix(ent.Path)
|
||||
logger.Verboseln("删除网盘文件和数据库记录", ent.Path)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 根据数据库记录删除不存在的文件
|
||||
if !flagSync {
|
||||
for ent, err := db.First(savePath); err == nil; ent, err = db.Next(savePath) {
|
||||
isLocalFileExist(ent)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
parent := db.Get(savePath)
|
||||
if parent.FileId == "" {
|
||||
efi, err := activeUser.PanClient().FileInfoByPath(driveId, savePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
parent.FileId = efi.FileId
|
||||
}
|
||||
|
||||
var syncFunc func(curPath, parentID string)
|
||||
|
||||
syncFunc = func(curPath, parentID string) {
|
||||
param := &aliyunpan.FileListParam{
|
||||
DriveId: driveId,
|
||||
ParentFileId: parentID,
|
||||
}
|
||||
fileResult, err := activeUser.PanClient().FileListGetAll(param)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if fileResult == nil || len(fileResult) == 0 {
|
||||
return
|
||||
}
|
||||
for _, fileEntity := range fileResult {
|
||||
ufm := &panupload.UploadedFileMeta{
|
||||
FileId: fileEntity.FileId,
|
||||
ParentId: fileEntity.ParentFileId,
|
||||
Size: fileEntity.FileSize,
|
||||
IsFolder: fileEntity.IsFolder(),
|
||||
Path: path.Join(curPath, fileEntity.FileName),
|
||||
SHA1: strings.ToLower(fileEntity.ContentHash),
|
||||
}
|
||||
|
||||
if !isLocalFileExist(ufm) {
|
||||
continue
|
||||
}
|
||||
|
||||
//如果这是一个目录就直接更新数据库,否则判断原始记录的Hash信息,如果一致才更新。
|
||||
if ufm.IsFolder {
|
||||
db.Put(ufm.Path, ufm)
|
||||
syncFunc(ufm.Path, ufm.FileId)
|
||||
} else if test := db.Get(ufm.Path); test.SHA1 == ufm.SHA1 {
|
||||
db.Put(ufm.Path, ufm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//开启自动清理功能
|
||||
db.AutoClean(parent.Path, true)
|
||||
db.Put(parent.Path, parent)
|
||||
|
||||
syncFunc(savePath, parent.FileId)
|
||||
}
|
||||
|
||||
func checkPath(localdir string) (string, error) {
|
||||
fullPath, err := filepath.Abs(localdir)
|
||||
if err != nil {
|
||||
fullPath = localdir
|
||||
}
|
||||
|
||||
if fi, err := os.Stat(fullPath); err != nil && !fi.IsDir() {
|
||||
return fullPath, os.ErrInvalid
|
||||
}
|
||||
|
||||
dbpath := filepath.Join(fullPath, BackupMetaDirName)
|
||||
//数据库目录判断
|
||||
fi, err := os.Stat(dbpath)
|
||||
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = os.Mkdir(dbpath, 0755)
|
||||
}
|
||||
if err != nil {
|
||||
return fullPath, fmt.Errorf("数据库目录[%s]创建失败,跳过处理: %s", dbpath, err)
|
||||
}
|
||||
}
|
||||
|
||||
if fi != nil && !fi.IsDir() {
|
||||
return fullPath, os.ErrPermission
|
||||
}
|
||||
|
||||
return fullPath, nil
|
||||
}
|
||||
|
||||
func Backup(c *cli.Context) error {
|
||||
if c.NArg() < 2 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
subArgs := c.Args()
|
||||
localpaths := make([]string, 0)
|
||||
flagSync := c.Bool("sync")
|
||||
flagDelete := c.Bool("delete")
|
||||
|
||||
opt := &UploadOptions{
|
||||
AllParallel: c.Int("p"),
|
||||
Parallel: 1, // 阿里云盘一个文件只支持单线程上传
|
||||
MaxRetry: c.Int("retry"),
|
||||
NoRapidUpload: c.Bool("norapid"),
|
||||
ShowProgress: !c.Bool("np"),
|
||||
IsOverwrite: true,
|
||||
DriveId: parseDriveId(c),
|
||||
ExcludeNames: c.StringSlice("exn"),
|
||||
BlockSize: int64(c.Int("bs") * 1024),
|
||||
}
|
||||
|
||||
localCount := c.NArg() - 1
|
||||
savePath := GetActiveUser().PathJoin(opt.DriveId, subArgs[localCount])
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(localCount)
|
||||
for _, p := range subArgs[:localCount] {
|
||||
go func(p string) {
|
||||
defer wg.Done()
|
||||
fullPath, err := checkPath(p)
|
||||
switch err {
|
||||
case nil:
|
||||
if flagSync || flagDelete {
|
||||
DelRemoteFileFromDB(opt.DriveId, fullPath, savePath, flagSync)
|
||||
}
|
||||
case os.ErrInvalid:
|
||||
default:
|
||||
return
|
||||
}
|
||||
localpaths = append(localpaths, fullPath)
|
||||
}(p)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if len(localpaths) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
RunUpload(localpaths, savePath, opt)
|
||||
return nil
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
@@ -45,8 +46,8 @@ type (
|
||||
MaxRetry int
|
||||
NoCheck bool
|
||||
ShowProgress bool
|
||||
DriveId string
|
||||
UseInternalUrl bool // 是否使用内置链接
|
||||
DriveId string
|
||||
UseInternalUrl bool // 是否使用内置链接
|
||||
}
|
||||
|
||||
// LocateDownloadOption 获取下载链接可选参数
|
||||
@@ -70,8 +71,8 @@ func CmdDownload() cli.Command {
|
||||
Usage: "下载文件/目录",
|
||||
UsageText: cmder.App().Name + " download <文件/目录路径1> <文件/目录2> <文件/目录3> ...",
|
||||
Description: `
|
||||
下载的文件默认保存到, 程序所在目录的 download/ 目录.
|
||||
通过 aliyunpan config set -savedir <savedir>, 自定义保存的目录.
|
||||
下载的文件默认保存到, 程序所在目录的 download/ 目录。支持软链接文件,包括Linux/macOS(ln命令)和Windows(mklink命令)创建的符号链接文件。
|
||||
通过 aliyunpan config set -savedir <savedir>, 自定义保存的目录。
|
||||
支持多个文件或目录下载.
|
||||
自动跳过下载重名的文件!
|
||||
|
||||
@@ -105,7 +106,9 @@ func CmdDownload() cli.Command {
|
||||
saveTo string
|
||||
)
|
||||
if c.Bool("save") {
|
||||
saveTo = "."
|
||||
// 使用当前工作目录
|
||||
pwd, _ := os.Getwd()
|
||||
saveTo = path.Clean(pwd)
|
||||
} else if c.String("saveto") != "" {
|
||||
saveTo = filepath.Clean(c.String("saveto"))
|
||||
}
|
||||
@@ -120,7 +123,7 @@ func CmdDownload() cli.Command {
|
||||
MaxRetry: c.Int("retry"),
|
||||
NoCheck: c.Bool("nocheck"),
|
||||
ShowProgress: !c.Bool("np"),
|
||||
DriveId: parseDriveId(c),
|
||||
DriveId: parseDriveId(c),
|
||||
}
|
||||
|
||||
RunDownload(c.Args(), do)
|
||||
@@ -149,7 +152,7 @@ func CmdDownload() cli.Command {
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "p",
|
||||
Usage: "指定同时进行下载文件的数量",
|
||||
Usage: "指定同时进行下载文件的数量(取值范围:1 ~ 20)",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "retry",
|
||||
@@ -183,6 +186,9 @@ func downloadPrintFormat(load int) string {
|
||||
// RunDownload 执行下载网盘内文件
|
||||
func RunDownload(paths []string, options *DownloadOptions) {
|
||||
activeUser := GetActiveUser()
|
||||
activeUser.PanClient().EnableCache()
|
||||
activeUser.PanClient().ClearCache()
|
||||
defer activeUser.PanClient().DisableCache()
|
||||
// pan token expired checker
|
||||
go func() {
|
||||
for {
|
||||
@@ -213,8 +219,8 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
||||
BlockSize: MaxDownloadRangeSize,
|
||||
MaxRate: config.Config.MaxDownloadRate,
|
||||
InstanceStateStorageFormat: downloader.InstanceStateStorageFormatJSON,
|
||||
ShowProgress: options.ShowProgress,
|
||||
UseInternalUrl: config.Config.TransferUrlType == 2,
|
||||
ShowProgress: options.ShowProgress,
|
||||
UseInternalUrl: config.Config.TransferUrlType == 2,
|
||||
}
|
||||
if cfg.CacheSize == 0 {
|
||||
cfg.CacheSize = int(DownloadCacheSize)
|
||||
@@ -227,6 +233,27 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
||||
options.Parallel = config.DefaultFileDownloadParallelNum
|
||||
}
|
||||
}
|
||||
if options.Parallel > config.MaxFileDownloadParallelNum {
|
||||
options.Parallel = config.MaxFileDownloadParallelNum
|
||||
}
|
||||
|
||||
// 保存文件的本地根文件夹
|
||||
originSaveRootPath := ""
|
||||
if options.SaveTo != "" {
|
||||
originSaveRootPath = options.SaveTo
|
||||
} else {
|
||||
// 使用默认的保存路径
|
||||
originSaveRootPath = GetActiveUser().GetSavePath("")
|
||||
}
|
||||
fi, err1 := os.Stat(originSaveRootPath)
|
||||
if err1 != nil && !os.IsExist(err1) {
|
||||
os.MkdirAll(originSaveRootPath, 0777) // 首先在本地创建目录
|
||||
} else {
|
||||
if !fi.IsDir() {
|
||||
fmt.Println("本地保存路径不是文件夹,请删除或者创建对应的文件夹:", originSaveRootPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
paths, err := matchPathByShellPattern(options.DriveId, paths...)
|
||||
if err != nil {
|
||||
@@ -275,7 +302,7 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
||||
// 设置储存的路径
|
||||
if options.SaveTo != "" {
|
||||
unit.OriginSaveRootPath = options.SaveTo
|
||||
unit.SavePath = filepath.Join(options.SaveTo, filepath.Base(paths[k]))
|
||||
unit.SavePath = filepath.Join(options.SaveTo, paths[k])
|
||||
} else {
|
||||
// 使用默认的保存路径
|
||||
unit.OriginSaveRootPath = GetActiveUser().GetSavePath("")
|
||||
|
||||
@@ -49,10 +49,10 @@ func CmdDrive() cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func RunSwitchDriveList(targetDriveId string) {
|
||||
func RunSwitchDriveList(targetDriveId string) {
|
||||
currentDriveId := config.Config.ActiveUser().ActiveDriveId
|
||||
var activeDriveInfo *config.DriveInfo = nil
|
||||
driveList,renderStr := getDriveOptionList()
|
||||
driveList, renderStr := getDriveOptionList()
|
||||
|
||||
if driveList == nil || len(driveList) == 0 {
|
||||
fmt.Println("切换网盘失败")
|
||||
@@ -71,15 +71,15 @@ func RunSwitchDriveList(targetDriveId string) {
|
||||
return
|
||||
}
|
||||
|
||||
if n, err := strconv.Atoi(index); err == nil && n >= 0 && n < len(driveList) {
|
||||
activeDriveInfo = driveList[n]
|
||||
if n, err1 := strconv.Atoi(index); err1 == nil && (n-1) >= 0 && (n-1) < len(driveList) {
|
||||
activeDriveInfo = driveList[n-1]
|
||||
} else {
|
||||
fmt.Printf("切换网盘失败, 请检查 # 值是否正确\n")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 直接切换
|
||||
for _,driveInfo := range driveList {
|
||||
for _, driveInfo := range driveList {
|
||||
if driveInfo.DriveId == targetDriveId {
|
||||
activeDriveInfo = driveInfo
|
||||
break
|
||||
@@ -121,7 +121,7 @@ func getDriveOptionList() (config.DriveInfoList, string) {
|
||||
tb.SetHeader([]string{"#", "drive_id", "网盘名称"})
|
||||
|
||||
for k, info := range driveList {
|
||||
tb.Append([]string{strconv.Itoa(k), info.DriveId, info.DriveName})
|
||||
tb.Append([]string{strconv.Itoa(k + 1), info.DriveId, info.DriveName})
|
||||
}
|
||||
tb.Render()
|
||||
return driveList, builder.String()
|
||||
|
||||
@@ -32,7 +32,7 @@ import (
|
||||
|
||||
type (
|
||||
dirFileListData struct {
|
||||
Dir *aliyunpan.MkdirResult
|
||||
Dir *aliyunpan.MkdirResult
|
||||
FileList aliyunpan.FileList
|
||||
}
|
||||
)
|
||||
@@ -99,7 +99,7 @@ func CmdImport() cli.Command {
|
||||
}
|
||||
|
||||
func RunImportFiles(driveId string, overwrite bool, panSavePath, localFilePath string) {
|
||||
lfi,_ := os.Stat(localFilePath)
|
||||
lfi, _ := os.Stat(localFilePath)
|
||||
if lfi != nil {
|
||||
if lfi.IsDir() {
|
||||
fmt.Println("请指定导入文件")
|
||||
@@ -125,7 +125,7 @@ func RunImportFiles(driveId string, overwrite bool, panSavePath, localFilePath s
|
||||
}
|
||||
defer importFile.Close()
|
||||
|
||||
fileData,err := ioutil.ReadAll(importFile)
|
||||
fileData, err := ioutil.ReadAll(importFile)
|
||||
if err != nil {
|
||||
fmt.Println("读取文件出错")
|
||||
return
|
||||
@@ -138,9 +138,9 @@ func RunImportFiles(driveId string, overwrite bool, panSavePath, localFilePath s
|
||||
fileText = strings.TrimSpace(fileText)
|
||||
fileLines := strings.Split(fileText, "\n")
|
||||
importFileItems := []RapidUploadItem{}
|
||||
for _,line := range fileLines {
|
||||
for _, line := range fileLines {
|
||||
line = strings.TrimSpace(line)
|
||||
if item,e := newRapidUploadItem(line); e != nil {
|
||||
if item, e := newRapidUploadItem(line); e != nil {
|
||||
fmt.Println(e)
|
||||
continue
|
||||
} else {
|
||||
@@ -159,7 +159,7 @@ func RunImportFiles(driveId string, overwrite bool, panSavePath, localFilePath s
|
||||
fmt.Println("正在导入...")
|
||||
successImportFiles := []RapidUploadItem{}
|
||||
failedImportFiles := []RapidUploadItem{}
|
||||
for _,item := range importFileItems {
|
||||
for _, item := range importFileItems {
|
||||
fmt.Printf("正在处理导入: %s\n", item.FilePath)
|
||||
result, abort := processOneImport(driveId, overwrite, dirMap, item)
|
||||
if abort {
|
||||
@@ -175,7 +175,7 @@ func RunImportFiles(driveId string, overwrite bool, panSavePath, localFilePath s
|
||||
}
|
||||
if len(failedImportFiles) > 0 {
|
||||
fmt.Println("\n以下文件导入失败")
|
||||
for _,f := range failedImportFiles {
|
||||
for _, f := range failedImportFiles {
|
||||
fmt.Printf("%s %s\n", f.FileSha1, f.FilePath)
|
||||
}
|
||||
fmt.Println("")
|
||||
@@ -185,13 +185,13 @@ func RunImportFiles(driveId string, overwrite bool, panSavePath, localFilePath s
|
||||
|
||||
func processOneImport(driveId string, isOverwrite bool, dirMap map[string]*dirFileListData, item RapidUploadItem) (result, abort bool) {
|
||||
panClient := config.Config.ActiveUser().PanClient()
|
||||
panDir,fileName := path.Split(item.FilePath)
|
||||
panDir, fileName := path.Split(item.FilePath)
|
||||
dataItem := dirMap[path.Dir(panDir)]
|
||||
if isOverwrite {
|
||||
// 标记覆盖旧同名文件
|
||||
// 检查同名文件是否存在
|
||||
var efi *aliyunpan.FileEntity = nil
|
||||
for _,fileItem := range dataItem.FileList {
|
||||
for _, fileItem := range dataItem.FileList {
|
||||
if !fileItem.IsFolder() && fileItem.FileName == fileName {
|
||||
efi = fileItem
|
||||
break
|
||||
@@ -201,8 +201,8 @@ func processOneImport(driveId string, isOverwrite bool, dirMap map[string]*dirFi
|
||||
// existed, delete it
|
||||
fdr, err := panClient.FileDelete([]*aliyunpan.FileBatchActionParam{
|
||||
{
|
||||
DriveId:driveId,
|
||||
FileId:efi.FileId,
|
||||
DriveId: driveId,
|
||||
FileId: efi.FileId,
|
||||
},
|
||||
})
|
||||
if err != nil || fdr == nil || !fdr[0].Success {
|
||||
@@ -239,7 +239,7 @@ func processOneImport(driveId string, isOverwrite bool, dirMap map[string]*dirFi
|
||||
func prepareMkdir(driveId string, importFileItems []RapidUploadItem) map[string]*dirFileListData {
|
||||
panClient := config.Config.ActiveUser().PanClient()
|
||||
resultMap := map[string]*dirFileListData{}
|
||||
for _,item := range importFileItems {
|
||||
for _, item := range importFileItems {
|
||||
var apierr *apierror.ApiError
|
||||
var rs *aliyunpan.MkdirResult
|
||||
panDir := path.Dir(item.FilePath)
|
||||
@@ -263,7 +263,7 @@ func prepareMkdir(driveId string, importFileItems []RapidUploadItem) map[string]
|
||||
param := &aliyunpan.FileListParam{}
|
||||
param.DriveId = driveId
|
||||
param.ParentFileId = rs.FileId
|
||||
allFileInfo, err1 := panClient.FileListGetAll(param)
|
||||
allFileInfo, err1 := panClient.FileListGetAll(param, 0)
|
||||
if err1 != nil {
|
||||
logger.Verboseln("获取文件信息出错")
|
||||
continue
|
||||
@@ -274,4 +274,4 @@ func prepareMkdir(driveId string, importFileItems []RapidUploadItem) map[string]
|
||||
time.Sleep(time.Duration(500) * time.Millisecond)
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
}
|
||||
|
||||
+66
-72
@@ -14,20 +14,18 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"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/library-go/requester"
|
||||
_ "github.com/tickstep/library-go/requester"
|
||||
"github.com/urfave/cli"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
||||
func CmdLogin() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "login",
|
||||
@@ -40,41 +38,24 @@ func CmdLogin() cli.Command {
|
||||
2.直接指定RefreshToken进行登录
|
||||
aliyunpan login -RefreshToken=8B12CBBCE89CA8DFC3445985B63B511B5E7EC7...
|
||||
|
||||
3.指定自行搭建的web服务,从指定的URL获取Token进行登录
|
||||
aliyunpan login --RefreshTokenUrl "http://your.host.com/aliyunpan/token/refresh"
|
||||
|
||||
web服务为GET请求,返回的响应体必须是JSON,格式要求如下所示,data内容即为token:
|
||||
{
|
||||
"code": "0",
|
||||
"msg": "ok",
|
||||
"data": "88771cd41a111521b4471a552bf633ba"
|
||||
}
|
||||
3.使用二维码方式进行登录
|
||||
aliyunpan login -QrCode
|
||||
`,
|
||||
Category: "阿里云盘账号",
|
||||
Before: cmder.ReloadConfigFunc, // 每次进行登录动作的时候需要调用刷新配置
|
||||
After: cmder.SaveConfigFunc, // 登录完成需要调用保存配置
|
||||
After: cmder.SaveConfigFunc, // 登录完成需要调用保存配置
|
||||
Action: func(c *cli.Context) error {
|
||||
// 优先从web服务获取token
|
||||
refreshTokenStr := ""
|
||||
if c.String("RefreshTokenUrl") != "" {
|
||||
refreshTokenUrl := c.String("RefreshTokenUrl")
|
||||
ts, e := getRefreshTokenFromWebServer(refreshTokenUrl)
|
||||
if e != nil {
|
||||
fmt.Println("从web服务获取Token失败")
|
||||
} else if ts != "" {
|
||||
fmt.Println("成功从web服务获取Token:" + ts)
|
||||
refreshTokenStr = ts
|
||||
}
|
||||
}
|
||||
|
||||
if refreshTokenStr == "" {
|
||||
refreshTokenStr = c.String("RefreshToken")
|
||||
}
|
||||
useQrCode := c.Bool("QrCode")
|
||||
|
||||
tokenId := ""
|
||||
webToken := aliyunpan.WebLoginToken{}
|
||||
refreshToken := ""
|
||||
var err error
|
||||
refreshToken, webToken, err = RunLogin(refreshTokenStr)
|
||||
tokenId, refreshToken, webToken, err = RunLogin(useQrCode, refreshTokenStr)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
@@ -86,20 +67,21 @@ func CmdLogin() cli.Command {
|
||||
return nil
|
||||
}
|
||||
cloudUser.RefreshToken = refreshToken
|
||||
cloudUser.TokenId = tokenId
|
||||
config.Config.SetActiveUser(cloudUser)
|
||||
fmt.Println("阿里云盘登录成功: ", cloudUser.Nickname)
|
||||
return nil
|
||||
},
|
||||
// 命令的附加options参数说明,使用 help login 命令即可查看
|
||||
// 命令的附加options参数说明,使用 help panlogin 命令即可查看
|
||||
Flags: []cli.Flag{
|
||||
// aliyunpan login -RefreshToken=8B12CBBCE89CA8DFC3445985B63B511B5E7EC7...
|
||||
// aliyunpan panlogin -RefreshToken=8B12CBBCE89CA8DFC3445985B63B511B5E7EC7...
|
||||
cli.StringFlag{
|
||||
Name: "RefreshToken",
|
||||
Usage: "使用RefreshToken Cookie来登录帐号",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "RefreshTokenUrl",
|
||||
Usage: "使用自行搭建的web服务获取RefreshToken来进行登录",
|
||||
cli.BoolFlag{
|
||||
Name: "QrCode",
|
||||
Usage: "使用二维码登录",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -123,6 +105,9 @@ func CmdLogout() cli.Command {
|
||||
confirm string
|
||||
activeUser = config.Config.ActiveUser()
|
||||
)
|
||||
if activeUser == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !c.Bool("y") {
|
||||
fmt.Printf("确认退出当前帐号: %s ? (y/n) > ", activeUser.Nickname)
|
||||
@@ -149,44 +134,53 @@ func CmdLogout() cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func RunLogin(refreshToken string) (refreshTokenStr string, webToken aliyunpan.WebLoginToken, error error) {
|
||||
return cmder.DoLoginHelper(refreshToken)
|
||||
}
|
||||
|
||||
|
||||
// getRefreshTokenFromWebServer 从自定义的web服务获取token
|
||||
func getRefreshTokenFromWebServer(url string) (string, error) {
|
||||
type tokenResult struct {
|
||||
Code string `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
if url == "" {
|
||||
return "", fmt.Errorf("url is empty")
|
||||
}
|
||||
|
||||
logger.Verboseln("do request url: " + url)
|
||||
header := map[string]string {
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"content-type": "application/json;charset=UTF-8",
|
||||
"user-agent": "aliyunpan/" + config.AppVersion,
|
||||
}
|
||||
// request
|
||||
client := requester.NewHTTPClient()
|
||||
client.SetTimeout(10 * time.Second)
|
||||
client.SetKeepAlive(false)
|
||||
body, err := client.Fetch("GET", url, nil, header)
|
||||
if err != nil {
|
||||
logger.Verboseln("get token error ", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// parse result
|
||||
r := &tokenResult{}
|
||||
if err2 := json.Unmarshal(body, r); err2 != nil {
|
||||
logger.Verboseln("parse token info result json error ", err2)
|
||||
return "", apierror.NewFailedApiError(err2.Error())
|
||||
}
|
||||
return r.Data, nil
|
||||
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)
|
||||
|
||||
// handler waiting
|
||||
line := cmdliner.NewLiner()
|
||||
var qrCodeLoginResult *panlogin.QRCodeLoginResult
|
||||
defer line.Close()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
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
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
line.State.Prompt("请在浏览器里面完成扫码登录,然后再按Enter键继续...")
|
||||
if qrCodeLoginResult == nil {
|
||||
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
|
||||
}
|
||||
|
||||
refreshTokenStr, webToken, error = cmder.DoLoginHelper(refreshToken)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -73,9 +73,34 @@ func CmdLs() cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
orderBy aliyunpan.FileOrderBy = aliyunpan.FileOrderByUpdatedAt
|
||||
orderSort aliyunpan.FileOrderDirection = aliyunpan.FileOrderDirectionDesc
|
||||
)
|
||||
|
||||
switch {
|
||||
case c.IsSet("asc"):
|
||||
orderSort = aliyunpan.FileOrderDirectionAsc
|
||||
case c.IsSet("desc"):
|
||||
orderSort = aliyunpan.FileOrderDirectionDesc
|
||||
default:
|
||||
orderSort = aliyunpan.FileOrderDirectionDesc
|
||||
}
|
||||
|
||||
switch {
|
||||
case c.IsSet("time"):
|
||||
orderBy = aliyunpan.FileOrderByUpdatedAt
|
||||
case c.IsSet("name"):
|
||||
orderBy = aliyunpan.FileOrderByName
|
||||
case c.IsSet("size"):
|
||||
orderBy = aliyunpan.FileOrderBySize
|
||||
default:
|
||||
orderBy = aliyunpan.FileOrderByUpdatedAt
|
||||
}
|
||||
|
||||
RunLs(parseDriveId(c), c.Args().Get(0), &LsOptions{
|
||||
Total: c.Bool("l") || c.Parent().Args().Get(0) == "ll",
|
||||
})
|
||||
}, orderBy, orderSort)
|
||||
|
||||
return nil
|
||||
},
|
||||
@@ -85,15 +110,36 @@ func CmdLs() cli.Command {
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "asc",
|
||||
Usage: "升序排序",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "desc",
|
||||
Usage: "降序排序",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "time",
|
||||
Usage: "根据修改时间排序",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "name",
|
||||
Usage: "根据文件名排序",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "size",
|
||||
Usage: "根据大小排序",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunLs(driveId, targetPath string, lsOptions *LsOptions) {
|
||||
func RunLs(driveId, targetPath string, lsOptions *LsOptions,
|
||||
orderBy aliyunpan.FileOrderBy, orderDirection aliyunpan.FileOrderDirection) {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
targetPath = activeUser.PathJoin(driveId, targetPath)
|
||||
if targetPath[len(targetPath) - 1] == '/' {
|
||||
targetPath = text.Substr(targetPath, 0, len(targetPath) - 1)
|
||||
if targetPath[len(targetPath)-1] == '/' {
|
||||
targetPath = text.Substr(targetPath, 0, len(targetPath)-1)
|
||||
}
|
||||
|
||||
targetPathInfo, err := activeUser.PanClient().FileInfoByPath(driveId, targetPath)
|
||||
@@ -106,8 +152,10 @@ func RunLs(driveId, targetPath string, lsOptions *LsOptions) {
|
||||
fileListParam := &aliyunpan.FileListParam{}
|
||||
fileListParam.ParentFileId = targetPathInfo.FileId
|
||||
fileListParam.DriveId = driveId
|
||||
fileListParam.OrderBy = orderBy
|
||||
fileListParam.OrderDirection = orderDirection
|
||||
if targetPathInfo.IsFolder() {
|
||||
fileResult, err := activeUser.PanClient().FileListGetAll(fileListParam)
|
||||
fileResult, err := activeUser.PanClient().FileListGetAll(fileListParam, 0)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
@@ -119,7 +167,6 @@ func RunLs(driveId, targetPath string, lsOptions *LsOptions) {
|
||||
renderTable(opLs, lsOptions.Total, targetPath, fileList)
|
||||
}
|
||||
|
||||
|
||||
func renderTable(op int, isTotal bool, path string, files aliyunpan.FileList) {
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
var (
|
||||
@@ -139,15 +186,15 @@ func renderTable(op int, isTotal bool, path string, files aliyunpan.FileList) {
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
|
||||
for k, file := range files {
|
||||
if file.IsFolder() {
|
||||
tb.Append([]string{strconv.Itoa(k), file.FileId, "-", "-", "-", file.CreatedAt, file.UpdatedAt, file.FileName + aliyunpan.PathSeparator})
|
||||
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, "-", "-", "-", file.CreatedAt, file.UpdatedAt, file.FileName + aliyunpan.PathSeparator})
|
||||
continue
|
||||
}
|
||||
|
||||
switch op {
|
||||
case opLs:
|
||||
tb.Append([]string{strconv.Itoa(k), file.FileId, converter.ConvertFileSize(file.FileSize, 2), file.ContentHash, strconv.FormatInt(file.FileSize, 10), file.CreatedAt, file.UpdatedAt, file.FileName})
|
||||
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, converter.ConvertFileSize(file.FileSize, 2), file.ContentHash, strconv.FormatInt(file.FileSize, 10), file.CreatedAt, file.UpdatedAt, file.FileName})
|
||||
case opSearch:
|
||||
tb.Append([]string{strconv.Itoa(k), file.FileId, converter.ConvertFileSize(file.FileSize, 2), file.ContentHash, strconv.FormatInt(file.FileSize, 10), file.CreatedAt, file.UpdatedAt, file.Path})
|
||||
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, converter.ConvertFileSize(file.FileSize, 2), file.ContentHash, strconv.FormatInt(file.FileSize, 10), file.CreatedAt, file.UpdatedAt, file.Path})
|
||||
}
|
||||
}
|
||||
fN, dN = files.Count()
|
||||
@@ -157,15 +204,15 @@ func renderTable(op int, isTotal bool, path string, files aliyunpan.FileList) {
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
|
||||
for k, file := range files {
|
||||
if file.IsFolder() {
|
||||
tb.Append([]string{strconv.Itoa(k), "-", file.UpdatedAt, file.FileName + aliyunpan.PathSeparator})
|
||||
tb.Append([]string{strconv.Itoa(k + 1), "-", file.UpdatedAt, file.FileName + aliyunpan.PathSeparator})
|
||||
continue
|
||||
}
|
||||
|
||||
switch op {
|
||||
case opLs:
|
||||
tb.Append([]string{strconv.Itoa(k), converter.ConvertFileSize(file.FileSize, 2), file.UpdatedAt, file.FileName})
|
||||
tb.Append([]string{strconv.Itoa(k + 1), converter.ConvertFileSize(file.FileSize, 2), file.UpdatedAt, file.FileName})
|
||||
case opSearch:
|
||||
tb.Append([]string{strconv.Itoa(k), converter.ConvertFileSize(file.FileSize, 2), file.UpdatedAt, file.Path})
|
||||
tb.Append([]string{strconv.Itoa(k + 1), converter.ConvertFileSize(file.FileSize, 2), file.UpdatedAt, file.Path})
|
||||
}
|
||||
}
|
||||
fN, dN = files.Count()
|
||||
|
||||
@@ -150,7 +150,7 @@ func RunRecycleList(driveId string) {
|
||||
fn = fn + "/"
|
||||
fs = "-"
|
||||
}
|
||||
tb.Append([]string{strconv.Itoa(k), file.FileId, fn, fs, file.CreatedAt, file.UpdatedAt})
|
||||
tb.Append([]string{strconv.Itoa(k + 1), file.FileId, fn, fs, file.CreatedAt, file.UpdatedAt})
|
||||
}
|
||||
|
||||
tb.Render()
|
||||
@@ -161,7 +161,7 @@ func RunRecycleRestore(driveId string, fidStrList ...string) {
|
||||
panClient := GetActivePanClient()
|
||||
restoreFileList := []*aliyunpan.FileBatchActionParam{}
|
||||
|
||||
for _,fid := range fidStrList {
|
||||
for _, fid := range fidStrList {
|
||||
restoreFileList = append(restoreFileList, &aliyunpan.FileBatchActionParam{
|
||||
DriveId: driveId,
|
||||
FileId: fid,
|
||||
@@ -190,7 +190,7 @@ func RunRecycleDelete(driveId string, fidStrList ...string) {
|
||||
panClient := GetActivePanClient()
|
||||
deleteFileList := []*aliyunpan.FileBatchActionParam{}
|
||||
|
||||
for _,fid := range fidStrList {
|
||||
for _, fid := range fidStrList {
|
||||
deleteFileList = append(deleteFileList, &aliyunpan.FileBatchActionParam{
|
||||
DriveId: driveId,
|
||||
FileId: fid,
|
||||
@@ -234,7 +234,7 @@ func RunRecycleClear(driveId string) {
|
||||
|
||||
// delete
|
||||
deleteFileList := []*aliyunpan.FileBatchActionParam{}
|
||||
for _,f := range fdl {
|
||||
for _, f := range fdl {
|
||||
deleteFileList = append(deleteFileList, &aliyunpan.FileBatchActionParam{
|
||||
DriveId: driveId,
|
||||
FileId: f.FileId,
|
||||
|
||||
@@ -87,8 +87,8 @@ func RunRemove(driveId string, paths ...string) {
|
||||
}
|
||||
fe.Path = absolutePath
|
||||
delFileInfos = append(delFileInfos, &aliyunpan.FileBatchActionParam{
|
||||
DriveId:driveId,
|
||||
FileId:fe.FileId,
|
||||
DriveId: driveId,
|
||||
FileId: fe.FileId,
|
||||
})
|
||||
fileId2FileEntity[fe.FileId] = fe
|
||||
cacheCleanDirs = append(cacheCleanDirs, path.Dir(fe.Path))
|
||||
@@ -98,7 +98,7 @@ func RunRemove(driveId string, paths ...string) {
|
||||
successDelFileEntity := []*aliyunpan.FileEntity{}
|
||||
fdr, err := activeUser.PanClient().FileDelete(delFileInfos)
|
||||
if fdr != nil {
|
||||
for _,item := range fdr {
|
||||
for _, item := range fdr {
|
||||
if !item.Success {
|
||||
failedRmPaths = append(failedRmPaths, fileId2FileEntity[item.FileId].Path)
|
||||
} else {
|
||||
@@ -111,7 +111,7 @@ func RunRemove(driveId string, paths ...string) {
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "文件/目录"})
|
||||
for k := range successDelFileEntity {
|
||||
tb.Append([]string{strconv.Itoa(k), successDelFileEntity[k].Path})
|
||||
tb.Append([]string{strconv.Itoa(k + 1), successDelFileEntity[k].Path})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
|
||||
+159
-55
@@ -14,6 +14,7 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
@@ -130,8 +132,7 @@ func CmdShare() cli.Command {
|
||||
RunShareList()
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "cancel",
|
||||
@@ -148,51 +149,86 @@ func CmdShare() cli.Command {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
// {
|
||||
// Name: "mc",
|
||||
// Aliases: []string{},
|
||||
// Usage: "创建秒传链接",
|
||||
// UsageText: cmder.App().Name + " share mc <文件/目录1> <文件/目录2> ...",
|
||||
// Description: `
|
||||
//创建文件秒传链接,秒传链接只能是文件,如果是文件夹则会创建文件夹包含的所有文件的秒传链接。秒传链接可以通过RapidUpload命令或者Import命令进行导入到自己的网盘。
|
||||
//示例:
|
||||
// 创建文件 1.mp4 的秒传链接
|
||||
// aliyunpan share mc 1.mp4
|
||||
//
|
||||
// 创建文件 1.mp4 的秒传链接,但链接隐藏相对路径
|
||||
// aliyunpan share mc -hp 1.mp4
|
||||
//
|
||||
// 创建文件夹 share_folder 下面所有文件的秒传链接
|
||||
// aliyunpan share mc share_folder/
|
||||
//`,
|
||||
// Action: func(c *cli.Context) error {
|
||||
// if c.NArg() < 1 {
|
||||
// cli.ShowCommandHelp(c, c.Command.Name)
|
||||
// return nil
|
||||
// }
|
||||
// if config.Config.ActiveUser() == nil {
|
||||
// fmt.Println("未登录账号")
|
||||
// return nil
|
||||
// }
|
||||
// hp := false
|
||||
// if c.IsSet("hp") {
|
||||
// hp = c.Bool("hp")
|
||||
// }
|
||||
// RunShareMc(parseDriveId(c), hp, c.Args())
|
||||
// return nil
|
||||
// },
|
||||
// Flags: []cli.Flag{
|
||||
// cli.StringFlag{
|
||||
// Name: "driveId",
|
||||
// Usage: "网盘ID",
|
||||
// Value: "",
|
||||
// },
|
||||
// cli.BoolFlag{
|
||||
// Name: "hp",
|
||||
// Usage: "hide path, 隐藏相对目录",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
{
|
||||
Name: "export",
|
||||
Usage: "导出分享记录保存到文件",
|
||||
UsageText: cmder.App().Name + " share export <csv file path>",
|
||||
Description: `
|
||||
导出分享记录,并保存到指定的文件。目前支持csv格式
|
||||
|
||||
示例:
|
||||
导出所有有效的分享并保存成文件
|
||||
aliyunpan share export "d:\myfoler\share_list.csv"
|
||||
|
||||
导出所有的分享并保存成文件
|
||||
aliyunpan share export -option 2 "d:\myfoler\share_list.csv"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() < 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
opt := c.String("option")
|
||||
if opt == "" {
|
||||
opt = "1"
|
||||
}
|
||||
filePath := c.Args()[0]
|
||||
RunShareExport(opt, filePath)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "option",
|
||||
Usage: "导出选项,1-有效分享 2-全部分享",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
// {
|
||||
// Name: "mc",
|
||||
// Aliases: []string{},
|
||||
// Usage: "创建秒传链接",
|
||||
// UsageText: cmder.App().Name + " share mc <文件/目录1> <文件/目录2> ...",
|
||||
// Description: `
|
||||
//创建文件秒传链接,秒传链接只能是文件,如果是文件夹则会创建文件夹包含的所有文件的秒传链接。秒传链接可以通过RapidUpload命令或者Import命令进行导入到自己的网盘。
|
||||
//示例:
|
||||
// 创建文件 1.mp4 的秒传链接
|
||||
// aliyunpan share mc 1.mp4
|
||||
//
|
||||
// 创建文件 1.mp4 的秒传链接,但链接隐藏相对路径
|
||||
// aliyunpan share mc -hp 1.mp4
|
||||
//
|
||||
// 创建文件夹 share_folder 下面所有文件的秒传链接
|
||||
// aliyunpan share mc share_folder/
|
||||
//`,
|
||||
// Action: func(c *cli.Context) error {
|
||||
// if c.NArg() < 1 {
|
||||
// cli.ShowCommandHelp(c, c.Command.Name)
|
||||
// return nil
|
||||
// }
|
||||
// if config.Config.ActiveUser() == nil {
|
||||
// fmt.Println("未登录账号")
|
||||
// return nil
|
||||
// }
|
||||
// hp := false
|
||||
// if c.IsSet("hp") {
|
||||
// hp = c.Bool("hp")
|
||||
// }
|
||||
// RunShareMc(parseDriveId(c), hp, c.Args())
|
||||
// return nil
|
||||
// },
|
||||
// Flags: []cli.Flag{
|
||||
// cli.StringFlag{
|
||||
// Name: "driveId",
|
||||
// Usage: "网盘ID",
|
||||
// Value: "",
|
||||
// },
|
||||
// cli.BoolFlag{
|
||||
// Name: "hp",
|
||||
// Usage: "hide path, 隐藏相对目录",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -207,7 +243,7 @@ func RunShareSet(driveId string, paths []string, expiredTime string, sharePwd st
|
||||
}
|
||||
|
||||
fidList := []string{}
|
||||
for _,f := range fileList {
|
||||
for _, f := range fileList {
|
||||
fidList = append(fidList, f.FileId)
|
||||
}
|
||||
|
||||
@@ -217,8 +253,8 @@ func RunShareSet(driveId string, paths []string, expiredTime string, sharePwd st
|
||||
}
|
||||
|
||||
r, err1 := panClient.ShareLinkCreate(aliyunpan.ShareCreateParam{
|
||||
DriveId: driveId,
|
||||
SharePwd: sharePwd,
|
||||
DriveId: driveId,
|
||||
SharePwd: sharePwd,
|
||||
Expiration: expiredTime,
|
||||
FileIdList: fidList,
|
||||
})
|
||||
@@ -251,7 +287,7 @@ func RunShareList() {
|
||||
}
|
||||
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "ShARE_ID", "分享链接", "提取码", "文件名", "FILE_ID", "过期时间", "状态"})
|
||||
tb.SetHeader([]string{"#", "ShARE_ID", "分享链接", "提取码", "文件名", "过期时间", "状态"})
|
||||
now := time.Now()
|
||||
for k, record := range records {
|
||||
et := "永久有效"
|
||||
@@ -270,9 +306,9 @@ func RunShareList() {
|
||||
}
|
||||
}
|
||||
}
|
||||
tb.Append([]string{strconv.Itoa(k), record.ShareId, record.ShareUrl, record.SharePwd,
|
||||
tb.Append([]string{strconv.Itoa(k + 1), record.ShareId, record.ShareUrl, record.SharePwd,
|
||||
record.ShareName,
|
||||
record.FileIdList[0],
|
||||
//record.FileIdList[0],
|
||||
et,
|
||||
status})
|
||||
}
|
||||
@@ -300,13 +336,81 @@ func RunShareCancel(shareIdList []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func RunShareExport(option, saveFilePath string) {
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().ShareLinkList(activeUser.UserId)
|
||||
if err != nil {
|
||||
fmt.Printf("获取分享列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
columns := [][]string{{"序号", "分享ID", "分享链接", "提取码", "文件名", "过期时间", "状态"}}
|
||||
now := time.Now()
|
||||
idx := 1
|
||||
for _, record := range records {
|
||||
et := "永久有效"
|
||||
if len(record.Expiration) > 0 {
|
||||
et = record.Expiration
|
||||
}
|
||||
status := "有效"
|
||||
if record.FirstFile == nil {
|
||||
status = "已删除"
|
||||
if option == "1" {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
cz := time.FixedZone("CST", 8*3600)
|
||||
if len(record.Expiration) > 0 {
|
||||
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", record.Expiration, cz)
|
||||
if expiredTime.Unix() < now.Unix() {
|
||||
status = "已过期"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if option == "1" {
|
||||
if status == "已过期" || status == "已删除" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
line := []string{strconv.Itoa(idx), record.ShareId, record.ShareUrl, record.SharePwd, record.ShareName, et, status}
|
||||
idx += 1
|
||||
columns = append(columns, line)
|
||||
}
|
||||
|
||||
// save to file
|
||||
if ExportCsv(saveFilePath, columns) {
|
||||
fmt.Println("分享导出成功:", saveFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
func ExportCsv(savePath string, data [][]string) bool {
|
||||
folder := filepath.Dir(savePath)
|
||||
if _, err := os.Stat(folder); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
os.MkdirAll(folder, os.ModePerm)
|
||||
}
|
||||
}
|
||||
fp, err := os.Create(savePath) // 创建文件句柄
|
||||
if err != nil {
|
||||
fmt.Println("创建文件["+savePath+"]失败,%v", err)
|
||||
return false
|
||||
}
|
||||
defer fp.Close()
|
||||
fp.WriteString("\xEF\xBB\xBF") // 写入UTF-8 BOM
|
||||
w := csv.NewWriter(fp) //创建一个新的写入文件流
|
||||
w.WriteAll(data)
|
||||
w.Flush()
|
||||
return true
|
||||
}
|
||||
|
||||
// 创建秒传链接
|
||||
func RunShareMc(driveId string, hideRelativePath bool, panPaths []string) {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
panClient := activeUser.PanClient()
|
||||
|
||||
totalCount := 0
|
||||
for _,panPath := range panPaths {
|
||||
for _, panPath := range panPaths {
|
||||
panPath = activeUser.PathJoin(driveId, panPath)
|
||||
panClient.FilesDirectoriesRecurseList(driveId, panPath, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
|
||||
if apiError != nil {
|
||||
@@ -331,4 +435,4 @@ func RunShareMc(driveId string, hideRelativePath bool, panPaths []string) {
|
||||
})
|
||||
}
|
||||
fmt.Printf("\n秒传链接总数量: %d\n", totalCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
// 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/syncdrive"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CmdSync() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "sync",
|
||||
Usage: "同步备份功能(Beta)",
|
||||
UsageText: cmder.App().Name + " sync",
|
||||
Description: `
|
||||
备份功能。支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份三种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
指定本地目录和对应的一个网盘目录,以备份文件。网盘目录必须和本地目录独占使用,不要用作其他用途,不然备份可能会有问题。
|
||||
|
||||
备份功能支持以下三种模式:
|
||||
1. upload
|
||||
备份本地文件,即上传本地文件到网盘,始终保持本地文件有一个完整的备份在网盘
|
||||
2. download
|
||||
备份云盘文件,即下载网盘文件到本地,始终保持网盘的文件有一个完整的备份在本地
|
||||
3. sync(慎用!!!双向备份过程会删除文件)
|
||||
双向备份,保持网盘文件和本地文件严格一致
|
||||
|
||||
请输入以下命令查看如何配置和启动:
|
||||
aliyunpan sync start -h
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
},
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "start",
|
||||
Usage: "启动sync同步备份任务",
|
||||
UsageText: cmder.App().Name + " sync start [arguments...]",
|
||||
Description: `
|
||||
备份本地文件到文件网盘,或者备份文件网盘的文件到本地。支持命令行配置启动或者使用备份配置文件启动同步备份任务。
|
||||
|
||||
配置文件保存在:(配置目录)/sync_drive/sync_drive_config.json,样例如下:
|
||||
{
|
||||
"configVer": "1.0",
|
||||
"syncTaskList": [
|
||||
{
|
||||
"name": "设计文档备份",
|
||||
"localFolderPath": "D:/tickstep/Documents/设计文档",
|
||||
"panFolderPath": "/sync_drive/我的文档",
|
||||
"mode": "upload",
|
||||
"priority": "time"
|
||||
}
|
||||
]
|
||||
}
|
||||
相关字段说明如下:
|
||||
name - 任务名称
|
||||
localFolderPath - 本地目录
|
||||
panFolderPath - 网盘目录
|
||||
mode - 模式,支持三种: upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)
|
||||
priority - 优先级,只对双向同步备份模式有效。选项支持三种: time-时间优先,local-本地优先,pan-网盘优先
|
||||
|
||||
例子:
|
||||
1. 查看帮助
|
||||
aliyunpan sync start -h
|
||||
|
||||
2. 使用命令行配置启动同步备份服务,将本地目录 D:\tickstep\Documents\设计文档 中的文件备份上传到云盘目录 /sync_drive/我的文档
|
||||
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "upload"
|
||||
|
||||
3. 使用命令行配置启动同步备份服务,将云盘目录 /sync_drive/我的文档 中的文件备份下载到本地目录 D:\tickstep\Documents\设计文档
|
||||
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "download"
|
||||
|
||||
4. 使用命令行配置启动同步备份服务,将云盘目录 /sync_drive/我的文档 和本地目录 D:\tickstep\Documents\设计文档 的文件进行双向同步
|
||||
同时配置同步优先选项为本地文件优先,并显示同步过程的日志
|
||||
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "sync" -pri "local" -log true
|
||||
|
||||
5. 使用命令行配置启动同步备份服务,将本地目录 D:\tickstep\Documents\设计文档 中的文件备份到云盘目录 /sync_drive/我的文档
|
||||
同时配置下载并发为2,上传并发为1,下载分片大小为256KB,上传分片大小为1MB
|
||||
aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/sync_drive/我的文档" -mode "upload" -dp 2 -up 1 -dbs 256 -ubs 1024
|
||||
|
||||
6. 使用配置文件启动同步备份服务,使用配置文件可以支持同时启动多个备份任务。配置文件必须存在,否则启动失败。
|
||||
aliyunpan sync start
|
||||
|
||||
7. 使用配置文件启动同步备份服务,并配置下载并发为2,上传并发为1,下载分片大小为256KB,上传分片大小为1MB
|
||||
aliyunpan sync start -dp 2 -up 1 -dbs 256 -ubs 1024
|
||||
|
||||
8. 当你本地同步目录文件非常多,或者云盘同步目录文件非常多,为了后期更快更精准同步文件,可以先进行文件扫描并构建同步数据库,然后再正常启动同步任务。如下所示:
|
||||
aliyunpan sync start -step scan
|
||||
aliyunpan sync start
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
activeUser := GetActiveUser()
|
||||
|
||||
if c.String("log") == "true" {
|
||||
syncdrive.LogPrompt = true
|
||||
} else {
|
||||
syncdrive.LogPrompt = false
|
||||
}
|
||||
|
||||
dp := c.Int("dp")
|
||||
if dp == 0 {
|
||||
dp = config.Config.MaxDownloadParallel
|
||||
}
|
||||
if dp == 0 {
|
||||
dp = 2
|
||||
}
|
||||
|
||||
up := c.Int("up")
|
||||
if up == 0 {
|
||||
up = config.Config.MaxUploadParallel
|
||||
}
|
||||
if up == 0 {
|
||||
up = 2
|
||||
}
|
||||
|
||||
downloadBlockSize := int64(c.Int("dbs") * 1024)
|
||||
if downloadBlockSize == 0 {
|
||||
downloadBlockSize = int64(config.Config.CacheSize)
|
||||
}
|
||||
if downloadBlockSize == 0 {
|
||||
downloadBlockSize = int64(256 * 1024)
|
||||
}
|
||||
|
||||
uploadBlockSize := int64(c.Int("ubs") * 1024)
|
||||
if uploadBlockSize == 0 {
|
||||
uploadBlockSize = aliyunpan.DefaultChunkSize
|
||||
}
|
||||
|
||||
opt := c.String("pri")
|
||||
var syncOpt syncdrive.SyncPriorityOption = syncdrive.SyncPriorityTimestampFirst
|
||||
if opt == "local" {
|
||||
syncOpt = syncdrive.SyncPriorityLocalFirst
|
||||
} else if opt == "pan" {
|
||||
syncOpt = syncdrive.SyncPriorityPanFirst
|
||||
} else {
|
||||
syncOpt = syncdrive.SyncPriorityTimestampFirst
|
||||
}
|
||||
|
||||
// 任务类型
|
||||
step := syncdrive.StepSyncFile
|
||||
stepVar := c.String("step")
|
||||
if stepVar == "scan" {
|
||||
step = syncdrive.StepScanFile
|
||||
}
|
||||
|
||||
var task *syncdrive.SyncTask
|
||||
localDir := c.String("ldir")
|
||||
panDir := c.String("pdir")
|
||||
mode := c.String("mode")
|
||||
if localDir != "" && panDir != "" {
|
||||
// make path absolute
|
||||
if !utils.IsLocalAbsPath(localDir) {
|
||||
pwd, _ := os.Getwd()
|
||||
localDir = path.Join(pwd, path.Clean(localDir))
|
||||
}
|
||||
panDir = activeUser.PathJoin(activeUser.ActiveDriveId, panDir)
|
||||
if !utils.IsLocalAbsPath(localDir) {
|
||||
fmt.Println("本地目录请指定绝对路径")
|
||||
return nil
|
||||
}
|
||||
if !utils.IsPanAbsPath(panDir) {
|
||||
fmt.Println("网盘目录请指定绝对路径")
|
||||
return nil
|
||||
}
|
||||
//if b, e := utils.PathExists(localDir); e == nil {
|
||||
// if !b {
|
||||
// fmt.Println("本地文件夹不存在:", localDir)
|
||||
// return nil
|
||||
// }
|
||||
//} else {
|
||||
// fmt.Println("本地文件夹不存在:", localDir)
|
||||
// return nil
|
||||
//}
|
||||
task = &syncdrive.SyncTask{}
|
||||
task.LocalFolderPath = path.Clean(strings.ReplaceAll(localDir, "\\", "/"))
|
||||
task.PanFolderPath = panDir
|
||||
task.Mode = syncdrive.UploadOnly
|
||||
if mode == string(syncdrive.UploadOnly) {
|
||||
task.Mode = syncdrive.UploadOnly
|
||||
} else if mode == string(syncdrive.DownloadOnly) {
|
||||
task.Mode = syncdrive.DownloadOnly
|
||||
} else if mode == string(syncdrive.SyncTwoWay) {
|
||||
task.Mode = syncdrive.SyncTwoWay
|
||||
} else {
|
||||
task.Mode = syncdrive.UploadOnly
|
||||
}
|
||||
task.Name = path.Base(task.LocalFolderPath)
|
||||
task.Id = utils.Md5Str(task.LocalFolderPath)
|
||||
task.Priority = syncOpt
|
||||
}
|
||||
|
||||
RunSync(task, dp, up, downloadBlockSize, uploadBlockSize, syncOpt, c.Int("ldt"), step)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "ldir",
|
||||
Usage: "local dir, 本地文件夹完整路径",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "pdir",
|
||||
Usage: "pan dir, 云盘文件夹完整路径",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "mode",
|
||||
Usage: "备份模式, 支持三种: upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)",
|
||||
Value: "upload",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "pri",
|
||||
Usage: "优先级priority,只对双向同步备份模式有效。当网盘和本地存在同名文件,优先使用哪个,选项支持三种: time-时间优先,local-本地优先,pan-网盘优先",
|
||||
Value: "time",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "dp",
|
||||
Usage: "download parallel, 下载并发数量,即可以同时并发下载多少个文件。0代表跟从配置文件设置(取值范围:1 ~ 10)",
|
||||
Value: 0,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "up",
|
||||
Usage: "upload parallel, 上传并发数量,即可以同时并发上传多少个文件。0代表跟从配置文件设置(取值范围:1 ~ 10)",
|
||||
Value: 0,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "dbs",
|
||||
Usage: "download block size,下载分片大小,单位KB。推荐值:1024 ~ 10240",
|
||||
Value: 1024,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "ubs",
|
||||
Usage: "upload block size,上传分片大小,单位KB。推荐值:1024 ~ 10240",
|
||||
Value: 10240,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "log",
|
||||
Usage: "是否显示文件备份过程日志,true-显示,false-不显示",
|
||||
Value: "false",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "ldt",
|
||||
Usage: "local delay time,本地文件修改检测延迟间隔,单位秒。如果本地文件会被频繁修改,例如录制视频文件,配置好该时间可以避免上传未录制好的文件",
|
||||
Value: 3,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "step",
|
||||
Usage: "task step 任务步骤, 支持两种: scan(只扫描并建立同步数据库),sync(正常启动同步任务)",
|
||||
Value: "sync",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunSync(defaultTask *syncdrive.SyncTask, fileDownloadParallel, fileUploadParallel int, downloadBlockSize, uploadBlockSize int64,
|
||||
flag syncdrive.SyncPriorityOption, localDelayTime int, taskStep syncdrive.TaskStep) {
|
||||
useInternalUrl := config.Config.TransferUrlType == 2
|
||||
maxDownloadRate := config.Config.MaxDownloadRate
|
||||
maxUploadRate := config.Config.MaxUploadRate
|
||||
activeUser := GetActiveUser()
|
||||
panClient := activeUser.PanClient()
|
||||
panClient.DisableCache()
|
||||
|
||||
// pan token expired checker
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
if RefreshTokenInNeed(activeUser) {
|
||||
logger.Verboseln("update access token for sync task")
|
||||
panClient.UpdateToken(activeUser.WebToken)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
syncFolderRootPath := config.GetSyncDriveDir()
|
||||
if b, e := utils.PathExists(syncFolderRootPath); e == nil {
|
||||
if !b {
|
||||
os.MkdirAll(syncFolderRootPath, 0755)
|
||||
}
|
||||
}
|
||||
|
||||
var tasks []*syncdrive.SyncTask
|
||||
if defaultTask != nil {
|
||||
tasks = []*syncdrive.SyncTask{}
|
||||
tasks = append(tasks, defaultTask)
|
||||
}
|
||||
|
||||
fmt.Println("启动同步备份进程")
|
||||
typeUrlStr := "默认链接"
|
||||
if useInternalUrl {
|
||||
typeUrlStr = "阿里ECS内部链接"
|
||||
}
|
||||
option := syncdrive.SyncOption{
|
||||
FileDownloadParallel: fileDownloadParallel,
|
||||
FileUploadParallel: fileUploadParallel,
|
||||
FileDownloadBlockSize: downloadBlockSize,
|
||||
FileUploadBlockSize: uploadBlockSize,
|
||||
UseInternalUrl: useInternalUrl,
|
||||
MaxDownloadRate: maxDownloadRate,
|
||||
MaxUploadRate: maxUploadRate,
|
||||
SyncPriority: flag,
|
||||
LocalFileModifiedCheckIntervalSec: localDelayTime,
|
||||
}
|
||||
syncMgr := syncdrive.NewSyncTaskManager(activeUser, activeUser.DriveList.GetFileDriveId(), panClient, syncFolderRootPath, option)
|
||||
syncConfigFile := syncMgr.ConfigFilePath()
|
||||
if tasks != nil {
|
||||
syncConfigFile = "(使用命令行配置)"
|
||||
}
|
||||
fmt.Printf("备份配置文件:%s\n链接类型:%s\n下载并发:%d\n上传并发:%d\n下载分片大小:%s\n上传分片大小:%s\n",
|
||||
syncConfigFile, typeUrlStr, fileDownloadParallel, fileUploadParallel, converter.ConvertFileSize(downloadBlockSize, 2),
|
||||
converter.ConvertFileSize(uploadBlockSize, 2))
|
||||
if _, e := syncMgr.Start(tasks, taskStep); e != nil {
|
||||
fmt.Println("启动任务失败:", e)
|
||||
return
|
||||
}
|
||||
|
||||
if taskStep != syncdrive.StepScanFile {
|
||||
_, ok := os.LookupEnv("ALIYUNPAN_DOCKER")
|
||||
if ok {
|
||||
// in docker container
|
||||
// 使用休眠以节省CPU资源
|
||||
fmt.Println("本命令不会退出,程序正在以Docker的方式运行。如需退出请借助Docker提供的方式。")
|
||||
for {
|
||||
time.Sleep(60 * time.Second)
|
||||
}
|
||||
} else {
|
||||
if config.IsAppInCliMode {
|
||||
// in cmd mode
|
||||
c := ""
|
||||
fmt.Println("本命令不会退出,如需要结束同步备份进程请输入y,然后按Enter键进行停止。")
|
||||
for strings.ToLower(c) != "y" {
|
||||
fmt.Scan(&c)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("本命令不会退出,程序正在以非交互的方式运行。如需退出请借助运行环境提供的方式。")
|
||||
logger.Verboseln("App not in CLI mode, not need to listen to input stream")
|
||||
for {
|
||||
time.Sleep(60 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("正在停止同步备份任务,请稍等...")
|
||||
}
|
||||
|
||||
// stop task
|
||||
syncMgr.Stop(taskStep)
|
||||
|
||||
if taskStep == syncdrive.StepScanFile {
|
||||
fmt.Println("\n已完成文件扫描和同步数据库的构建,可以启动任务同步了")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
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"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CmdTree() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "tree",
|
||||
Usage: "列出目录的树形图",
|
||||
UsageText: cmder.App().Name + " tree <目录>",
|
||||
Description: `
|
||||
列出指定目录内的文件和目录, 并以树形图的方式呈现
|
||||
|
||||
示例:
|
||||
|
||||
列出 当前工作目录 内的文件和目录的树形图
|
||||
aliyunpan tree
|
||||
|
||||
列出 /我的资源 内的文件和目录的树形图
|
||||
aliyunpan tree /我的资源
|
||||
|
||||
列出 /我的资源 内的文件和目录的树形图,并且显示文件对应的完整绝对路径
|
||||
aliyunpan tree -fp /我的资源
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
RunTree(parseDriveId(c), c.Args().Get(0), c.Bool("fp"))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "fp",
|
||||
Usage: "full path, 树形图是否显示文件完整路径",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
indentPrefix = "│ "
|
||||
pathPrefix = "├──"
|
||||
lastFilePrefix = "└──"
|
||||
)
|
||||
|
||||
type (
|
||||
treeStatistic struct {
|
||||
CountOfDir int64
|
||||
CountOfFile int64
|
||||
}
|
||||
)
|
||||
|
||||
func getTree(driveId, pathStr string, depth int, statistic *treeStatistic, showFullPath bool) {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
pathStr = activeUser.PathJoin(driveId, pathStr)
|
||||
pathStr = path.Clean(pathStr)
|
||||
|
||||
targetPathInfo, err1 := activeUser.PanClient().FileInfoByPath(driveId, pathStr)
|
||||
if err1 != nil {
|
||||
fmt.Println(err1)
|
||||
return
|
||||
}
|
||||
fileList := aliyunpan.FileList{}
|
||||
fileListParam := &aliyunpan.FileListParam{}
|
||||
fileListParam.ParentFileId = targetPathInfo.FileId
|
||||
fileListParam.DriveId = driveId
|
||||
fileListParam.OrderBy = aliyunpan.FileOrderByName
|
||||
fileListParam.OrderDirection = aliyunpan.FileOrderDirectionAsc
|
||||
if targetPathInfo.IsFolder() {
|
||||
fileResult, err := activeUser.PanClient().FileListGetAll(fileListParam, 0)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fileList = append(fileList, fileResult...)
|
||||
} else {
|
||||
fileList = append(fileList, targetPathInfo)
|
||||
}
|
||||
|
||||
var (
|
||||
prefix = pathPrefix
|
||||
fN = len(fileList)
|
||||
indentPrefixStr = strings.Repeat(indentPrefix, depth)
|
||||
)
|
||||
for i, file := range fileList {
|
||||
if file.IsFolder() {
|
||||
statistic.CountOfDir += 1
|
||||
if showFullPath {
|
||||
fmt.Printf("%v%v %v/ -> %s\n", indentPrefixStr, pathPrefix, file.FileName, pathStr+"/"+file.FileName)
|
||||
} else {
|
||||
fmt.Printf("%v%v %v/\n", indentPrefixStr, pathPrefix, file.FileName)
|
||||
}
|
||||
getTree(driveId, pathStr+"/"+file.Path, depth+1, statistic, showFullPath)
|
||||
continue
|
||||
}
|
||||
statistic.CountOfFile += 1
|
||||
|
||||
if i+1 == fN {
|
||||
prefix = lastFilePrefix
|
||||
}
|
||||
|
||||
if showFullPath {
|
||||
fmt.Printf("%v%v %v -> %s\n", indentPrefixStr, prefix, file.FileName, pathStr+"/"+file.FileName)
|
||||
} else {
|
||||
fmt.Printf("%v%v %v\n", indentPrefixStr, prefix, file.FileName)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// RunTree 列出树形图
|
||||
func RunTree(driveId, pathStr string, showFullPath bool) {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
activeUser.PanClient().ClearCache()
|
||||
activeUser.PanClient().EnableCache()
|
||||
defer activeUser.PanClient().DisableCache()
|
||||
pathStr = activeUser.PathJoin(driveId, pathStr)
|
||||
statistic := &treeStatistic{
|
||||
CountOfDir: 0,
|
||||
CountOfFile: 0,
|
||||
}
|
||||
fmt.Printf("%s\n", pathStr)
|
||||
getTree(driveId, pathStr, 0, statistic, showFullPath)
|
||||
fmt.Printf("\n%d 个文件夹, %d 个文件\n", statistic.CountOfDir, statistic.CountOfFile)
|
||||
}
|
||||
+115
-149
@@ -17,9 +17,9 @@ import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -54,23 +54,23 @@ const (
|
||||
type (
|
||||
// UploadOptions 上传可选项
|
||||
UploadOptions struct {
|
||||
AllParallel int // 所有文件并发上传数量,即可以同时并发上传多少个文件
|
||||
Parallel int // 单个文件并发上传数量
|
||||
MaxRetry int
|
||||
NoRapidUpload bool
|
||||
ShowProgress bool
|
||||
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
|
||||
DriveId string
|
||||
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行上传,支持正则表达式
|
||||
BlockSize int64 // 分片大小
|
||||
UseInternalUrl bool // 是否使用内置链接
|
||||
AllParallel int // 所有文件并发上传数量,即可以同时并发上传多少个文件
|
||||
Parallel int // 单个文件并发上传数量
|
||||
MaxRetry int
|
||||
NoRapidUpload bool
|
||||
ShowProgress bool
|
||||
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
|
||||
DriveId string
|
||||
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行上传,支持正则表达式
|
||||
BlockSize int64 // 分片大小
|
||||
UseInternalUrl bool // 是否使用内置链接
|
||||
}
|
||||
)
|
||||
|
||||
var UploadFlags = []cli.Flag{
|
||||
cli.IntFlag{
|
||||
Name: "p",
|
||||
Usage: "本次操作文件上传并发数量,即可以同时并发上传多少个文件。0代表跟从配置文件设置",
|
||||
Usage: "本次操作文件上传并发数量,即可以同时并发上传多少个文件。0代表跟从配置文件设置(取值范围:1 ~ 20)",
|
||||
Value: 0,
|
||||
},
|
||||
cli.IntFlag{
|
||||
@@ -114,7 +114,7 @@ func CmdUpload() cli.Command {
|
||||
Usage: "上传文件/目录",
|
||||
UsageText: cmder.App().Name + " upload <本地文件/目录的路径1> <文件/目录2> <文件/目录3> ... <目标目录>",
|
||||
Description: `
|
||||
上传指定的文件夹或者文件,上传的文件将会保存到 <目标目录>.
|
||||
上传指定的文件夹或者文件,上传的文件将会保存到 <目标目录>。支持软链接文件,包括Linux/macOS(ln命令)和Windows(mklink命令)创建的符号链接文件。
|
||||
|
||||
示例:
|
||||
1. 将本地的 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录
|
||||
@@ -161,14 +161,14 @@ func CmdUpload() cli.Command {
|
||||
subArgs := c.Args()
|
||||
RunUpload(subArgs[:c.NArg()-1], subArgs[c.NArg()-1], &UploadOptions{
|
||||
AllParallel: c.Int("p"), // 多文件上传的时候,允许同时并行上传的文件数量
|
||||
Parallel: 1, // 一个文件同时多少个线程并发上传的数量。阿里云盘只支持单线程按顺序进行文件part数据上传,所以只能是1
|
||||
Parallel: 1, // 一个文件同时多少个线程并发上传的数量。阿里云盘只支持单线程按顺序进行文件part数据上传,所以只能是1
|
||||
MaxRetry: c.Int("retry"),
|
||||
NoRapidUpload: c.Bool("norapid"),
|
||||
ShowProgress: !c.Bool("np"),
|
||||
IsOverwrite: c.Bool("ow"),
|
||||
DriveId: parseDriveId(c),
|
||||
ExcludeNames: c.StringSlice("exn"),
|
||||
BlockSize: int64(c.Int("bs") * 1024),
|
||||
DriveId: parseDriveId(c),
|
||||
ExcludeNames: c.StringSlice("exn"),
|
||||
BlockSize: int64(c.Int("bs") * 1024),
|
||||
})
|
||||
return nil
|
||||
},
|
||||
@@ -232,6 +232,9 @@ func CmdRapidUpload() cli.Command {
|
||||
// RunUpload 执行文件上传
|
||||
func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
activeUser := GetActiveUser()
|
||||
activeUser.PanClient().EnableCache()
|
||||
activeUser.PanClient().ClearCache()
|
||||
defer activeUser.PanClient().DisableCache()
|
||||
// pan token expired checker
|
||||
go func() {
|
||||
for {
|
||||
@@ -253,6 +256,10 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
opt.AllParallel = config.DefaultFileUploadParallelNum
|
||||
}
|
||||
}
|
||||
if opt.AllParallel > config.MaxFileUploadParallelNum {
|
||||
opt.AllParallel = config.MaxFileUploadParallelNum
|
||||
}
|
||||
|
||||
if opt.Parallel <= 0 {
|
||||
opt.Parallel = 1
|
||||
}
|
||||
@@ -292,6 +299,8 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
statistic = &panupload.UploadStatistic{}
|
||||
|
||||
folderCreateMutex = &sync.Mutex{}
|
||||
|
||||
pluginManger = plugins.NewPluginManager(config.GetPluginDir())
|
||||
)
|
||||
executor.SetParallel(opt.AllParallel)
|
||||
statistic.StartTimer() // 开始计时
|
||||
@@ -299,10 +308,12 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
// 全局速度统计
|
||||
globalSpeedsStat := &speeds.Speeds{}
|
||||
|
||||
// 获取当前插件
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
|
||||
// 遍历指定的文件并创建上传任务
|
||||
for _, curPath := range localPaths {
|
||||
var walkFunc filepath.WalkFunc
|
||||
var db panupload.SyncDb
|
||||
var walkFunc localfile.MyWalkFunc
|
||||
curPath = filepath.Clean(curPath)
|
||||
localPathDir := filepath.Dir(curPath)
|
||||
|
||||
@@ -317,109 +328,103 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
localPathDir = ""
|
||||
}
|
||||
|
||||
if fi, err := os.Stat(curPath); err == nil && fi.IsDir() {
|
||||
//使用绝对路径避免异常
|
||||
dbpath, err := filepath.Abs(curPath)
|
||||
walkFunc = func(file localfile.SymlinkFile, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
dbpath = curPath
|
||||
// skip this error file and continue recurse
|
||||
logger.Verboseln("upload process file: ", file, " error: ", err)
|
||||
return nil
|
||||
}
|
||||
dbpath += string(os.PathSeparator) + BackupMetaDirName
|
||||
if di, err := os.Stat(dbpath); err == nil && di.IsDir() {
|
||||
db, err = panupload.OpenSyncDb(dbpath+string(os.PathSeparator) + "db", BackupMetaBucketName)
|
||||
if db != nil {
|
||||
defer func(syncDb panupload.SyncDb) {
|
||||
db.Close()
|
||||
}(db)
|
||||
} else {
|
||||
fmt.Println(curPath, "同步数据库打开失败,跳过该目录的备份", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walkFunc = func(file string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
if os.PathSeparator == '\\' {
|
||||
file.LogicPath = cmdutil.ConvertToWindowsPathSeparator(file.LogicPath)
|
||||
file.RealPath = cmdutil.ConvertToWindowsPathSeparator(file.RealPath)
|
||||
}
|
||||
|
||||
// 是否排除上传
|
||||
if isExcludeFile(file, opt) {
|
||||
fmt.Printf("排除文件: %s\n", file)
|
||||
if isExcludeFile(file.LogicPath, opt) {
|
||||
fmt.Printf("排除文件: %s\n", file.LogicPath)
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
if fi.Mode()&os.ModeSymlink != 0 { // 读取 symbol link
|
||||
err = WalkAllFile(file+string(os.PathSeparator), walkFunc)
|
||||
return err
|
||||
}
|
||||
|
||||
subSavePath := strings.TrimPrefix(file, localPathDir)
|
||||
subSavePath := strings.TrimPrefix(file.LogicPath, localPathDir)
|
||||
|
||||
// 针对 windows 的目录处理
|
||||
if os.PathSeparator == '\\' {
|
||||
subSavePath = cmdutil.ConvertToUnixPathSeparator(subSavePath)
|
||||
}
|
||||
|
||||
subSavePath = path.Clean(savePath + aliyunpan.PathSeparator + subSavePath)
|
||||
var ufm *panupload.UploadedFileMeta
|
||||
|
||||
if db != nil {
|
||||
if ufm = db.Get(subSavePath); ufm.Size == fi.Size() && ufm.ModTime == fi.ModTime().Unix() {
|
||||
logger.Verbosef("文件未修改跳过:%s\n", file)
|
||||
return nil
|
||||
}
|
||||
// 插件回调
|
||||
ft := "file"
|
||||
if fi.IsDir() {
|
||||
ft = "folder"
|
||||
}
|
||||
|
||||
if fi.IsDir() { // 备份目录处理
|
||||
if strings.HasPrefix(fi.Name(), BackupMetaDirName) {
|
||||
pluginParam := &plugins.UploadFilePrepareParams{
|
||||
LocalFilePath: file.LogicPath,
|
||||
LocalFileName: fi.Name(),
|
||||
LocalFileSize: fi.Size(),
|
||||
LocalFileType: ft,
|
||||
LocalFileUpdatedAt: fi.ModTime().Format("2006-01-02 15:04:05"),
|
||||
DriveId: activeUser.ActiveDriveId,
|
||||
DriveFilePath: strings.TrimPrefix(strings.TrimPrefix(subSavePath, savePath), "/"),
|
||||
}
|
||||
if uploadFilePrepareResult, er := plugin.UploadFilePrepareCallback(plugins.GetContext(activeUser), pluginParam); er == nil && uploadFilePrepareResult != nil {
|
||||
if strings.Compare("yes", uploadFilePrepareResult.UploadApproved) != 0 {
|
||||
// skip upload this file
|
||||
fmt.Printf("插件禁止该文件上传: %s\n", file.LogicPath)
|
||||
return filepath.SkipDir
|
||||
}
|
||||
//不存在同步数据库时跳过
|
||||
if db == nil || ufm.FileId != "" {
|
||||
return nil
|
||||
if uploadFilePrepareResult.DriveFilePath != "" {
|
||||
targetSavePanRelativePath := strings.TrimPrefix(uploadFilePrepareResult.DriveFilePath, "/")
|
||||
subSavePath = path.Clean(savePath + aliyunpan.PathSeparator + targetSavePanRelativePath)
|
||||
fmt.Printf("插件修改文件网盘保存路径为: %s\n", subSavePath)
|
||||
}
|
||||
panClient := activeUser.PanClient()
|
||||
fmt.Println(subSavePath, "云盘文件夹预创建")
|
||||
//首先尝试直接创建文件夹
|
||||
if ufm = db.Get(path.Dir(subSavePath)); ufm.IsFolder == true && ufm.FileId != "" {
|
||||
rs, err := panClient.Mkdir(opt.DriveId, ufm.FileId, fi.Name())
|
||||
if err == nil && rs != nil && rs.FileId != "" {
|
||||
db.Put(subSavePath, &panupload.UploadedFileMeta{FileId: rs.FileId, IsFolder: true, ModTime: fi.ModTime().Unix(), ParentId: rs.ParentFileId})
|
||||
return nil
|
||||
}
|
||||
}
|
||||
rs, err := panClient.MkdirRecursive(opt.DriveId, "", "", 0, strings.Split(path.Clean(subSavePath), "/"))
|
||||
if err == nil && rs != nil && rs.FileId != "" {
|
||||
db.Put(subSavePath, &panupload.UploadedFileMeta{FileId: rs.FileId, IsFolder: true, ModTime: fi.ModTime().Unix(), ParentId: rs.ParentFileId})
|
||||
return nil
|
||||
}
|
||||
fmt.Println(subSavePath, "创建云盘文件夹失败", err)
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
taskinfo := executor.Append(&panupload.UploadTaskUnit{
|
||||
LocalFileChecksum: localfile.NewLocalFileEntity(file),
|
||||
SavePath: subSavePath,
|
||||
DriveId: opt.DriveId,
|
||||
PanClient: activeUser.PanClient(),
|
||||
UploadingDatabase: uploadDatabase,
|
||||
FolderCreateMutex: folderCreateMutex,
|
||||
Parallel: opt.Parallel,
|
||||
NoRapidUpload: opt.NoRapidUpload,
|
||||
BlockSize: opt.BlockSize,
|
||||
UploadStatistic: statistic,
|
||||
ShowProgress: opt.ShowProgress,
|
||||
IsOverwrite: opt.IsOverwrite,
|
||||
FolderSyncDb: db,
|
||||
UseInternalUrl: opt.UseInternalUrl,
|
||||
GlobalSpeedsStat: globalSpeedsStat,
|
||||
}, opt.MaxRetry)
|
||||
|
||||
fmt.Printf("[%s] 加入上传队列: %s\n", taskinfo.Id(), file)
|
||||
// 创建对应的文件上传任务
|
||||
// 上传里面的文件会创建对应的缺失文件夹
|
||||
if !fi.IsDir() {
|
||||
taskinfo := executor.Append(&panupload.UploadTaskUnit{
|
||||
LocalFileChecksum: localfile.NewLocalSymlinkFileEntity(file),
|
||||
SavePath: subSavePath,
|
||||
DriveId: opt.DriveId,
|
||||
PanClient: activeUser.PanClient(),
|
||||
UploadingDatabase: uploadDatabase,
|
||||
FolderCreateMutex: folderCreateMutex,
|
||||
Parallel: opt.Parallel,
|
||||
NoRapidUpload: opt.NoRapidUpload,
|
||||
BlockSize: opt.BlockSize,
|
||||
UploadStatistic: statistic,
|
||||
ShowProgress: opt.ShowProgress,
|
||||
IsOverwrite: opt.IsOverwrite,
|
||||
UseInternalUrl: opt.UseInternalUrl,
|
||||
GlobalSpeedsStat: globalSpeedsStat,
|
||||
}, opt.MaxRetry)
|
||||
fmt.Printf("[%s] 加入上传队列: %s\n", taskinfo.Id(), file.LogicPath)
|
||||
} else {
|
||||
// 创建文件夹
|
||||
// 这样空文件夹也可以正确上传
|
||||
saveFilePath := subSavePath
|
||||
if saveFilePath != "/" {
|
||||
fmt.Printf("正在检测和创建云盘文件夹: %s\n", saveFilePath)
|
||||
_, apierr1 := activeUser.PanClient().FileInfoByPath(opt.DriveId, saveFilePath)
|
||||
time.Sleep(1 * time.Second)
|
||||
if apierr1 != nil && apierr1.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
logger.Verbosef("%s 创建云盘文件夹: %s\n", utils.NowTimeStr(), saveFilePath)
|
||||
rs, apierr := activeUser.PanClient().Mkdir(opt.DriveId, "root", saveFilePath)
|
||||
if apierr != nil || rs.FileId == "" {
|
||||
fmt.Printf("创建云盘文件夹失败: %s\n", saveFilePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := WalkAllFile(curPath, walkFunc); err != nil {
|
||||
fmt.Printf("警告: 遍历错误: %s\n", err)
|
||||
|
||||
file := localfile.NewSymlinkFile(curPath)
|
||||
if err = localfile.WalkAllFile(file, walkFunc); err != nil {
|
||||
if err != filepath.SkipDir {
|
||||
fmt.Printf("警告: 遍历错误: %s\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,7 +446,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
for e := failed.Shift(); e != nil; e = failed.Shift() {
|
||||
item := e.(*taskframework.TaskInfoItem)
|
||||
tb.Append([]string{item.Info.Id(), item.Unit.(*panupload.UploadTaskUnit).LocalFileChecksum.Path})
|
||||
tb.Append([]string{item.Info.Id(), item.Unit.(*panupload.UploadTaskUnit).LocalFileChecksum.Path.LogicPath})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
@@ -451,14 +456,13 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
|
||||
// 是否是排除上传的文件
|
||||
func isExcludeFile(filePath string, opt *UploadOptions) bool {
|
||||
if opt == nil || len(opt.ExcludeNames) == 0{
|
||||
if opt == nil || len(opt.ExcludeNames) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _,pattern := range opt.ExcludeNames {
|
||||
fileName := path.Base(filePath)
|
||||
|
||||
m,_ := regexp.MatchString(pattern, fileName)
|
||||
for _, pattern := range opt.ExcludeNames {
|
||||
fileName := path.Base(strings.ReplaceAll(filePath, "\\", "/"))
|
||||
m, _ := regexp.MatchString(pattern, fileName)
|
||||
if m {
|
||||
return true
|
||||
}
|
||||
@@ -466,44 +470,6 @@ func isExcludeFile(filePath string, opt *UploadOptions) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func WalkAllFile(dirPath string, walkFn filepath.WalkFunc) error {
|
||||
info, err := os.Lstat(dirPath)
|
||||
if err != nil {
|
||||
err = walkFn(dirPath, nil, err)
|
||||
} else {
|
||||
err = walkAllFile(dirPath, info, walkFn)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func walkAllFile(dirPath string, info os.FileInfo, walkFn filepath.WalkFunc) error {
|
||||
if !info.IsDir() {
|
||||
return walkFn(dirPath, info, nil)
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return walkFn(dirPath, nil, err)
|
||||
}
|
||||
for _, fi := range files {
|
||||
subFilePath := dirPath + "/" + fi.Name()
|
||||
err := walkFn(subFilePath, fi, err)
|
||||
if err != nil && err != filepath.SkipDir {
|
||||
return err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
if err == filepath.SkipDir {
|
||||
continue
|
||||
}
|
||||
err := walkAllFile(subFilePath, fi, walkFn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunRapidUpload 秒传
|
||||
func RunRapidUpload(driveId string, isOverwrite bool, fileMetaList []string, savePanPath string) {
|
||||
activeUser := GetActiveUser()
|
||||
@@ -516,8 +482,8 @@ func RunRapidUpload(driveId string, isOverwrite bool, fileMetaList []string, sav
|
||||
|
||||
items := []*RapidUploadItem{}
|
||||
// parse file meta strings
|
||||
for _,fileMeta := range fileMetaList {
|
||||
item,e := newRapidUploadItem(fileMeta)
|
||||
for _, fileMeta := range fileMetaList {
|
||||
item, e := newRapidUploadItem(fileMeta)
|
||||
if e != nil {
|
||||
fmt.Println(e)
|
||||
continue
|
||||
@@ -535,7 +501,7 @@ func RunRapidUpload(driveId string, isOverwrite bool, fileMetaList []string, sav
|
||||
}
|
||||
|
||||
// upload one by one
|
||||
for _,item := range items {
|
||||
for _, item := range items {
|
||||
fmt.Println("准备秒传:", item.FilePath)
|
||||
if ee := doRapidUpload(driveId, isOverwrite, item); ee != nil {
|
||||
fmt.Println(ee)
|
||||
@@ -576,7 +542,7 @@ func doRapidUpload(driveId string, isOverwrite bool, item *RapidUploadItem) erro
|
||||
}
|
||||
if efi != nil && efi.FileId != "" {
|
||||
// existed, delete it
|
||||
fileDeleteResult, err1 := panClient.FileDelete([]*aliyunpan.FileBatchActionParam{{DriveId:efi.DriveId, FileId:efi.FileId}})
|
||||
fileDeleteResult, err1 := panClient.FileDelete([]*aliyunpan.FileBatchActionParam{{DriveId: efi.DriveId, FileId: efi.FileId}})
|
||||
if err1 != nil || len(fileDeleteResult) == 0 {
|
||||
return fmt.Errorf("无法删除文件,请稍后重试")
|
||||
}
|
||||
@@ -600,11 +566,11 @@ func doRapidUpload(driveId string, isOverwrite bool, item *RapidUploadItem) erro
|
||||
if apierr != nil {
|
||||
return fmt.Errorf("创建秒传任务失败:" + apierr.Error())
|
||||
}
|
||||
|
||||
|
||||
if uploadOpEntity.RapidUpload {
|
||||
logger.Verboseln("秒传成功, 保存到网盘路径: ", path.Join(panDir, uploadOpEntity.FileName))
|
||||
} else {
|
||||
return fmt.Errorf("失败,文件未曾上传,无法秒传")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,8 +85,8 @@ func CmdSu() cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
if n, err := strconv.Atoi(index); err == nil && n >= 0 && n < numLogins {
|
||||
uid = config.Config.UserList[n].UserId
|
||||
if n, err1 := strconv.Atoi(index); err1 == nil && (n-1) >= 0 && (n-1) < numLogins {
|
||||
uid = config.Config.UserList[n-1].UserId
|
||||
} else {
|
||||
fmt.Printf("切换用户失败, 请检查 # 值是否正确\n")
|
||||
return nil
|
||||
|
||||
+30
-13
@@ -29,14 +29,6 @@ var (
|
||||
panCommandVerbose = logger.New("PANCOMMAND", config.EnvVerbose)
|
||||
)
|
||||
|
||||
const(
|
||||
// 备份数据库桶分区标志
|
||||
BackupMetaBucketName = "adrive"
|
||||
|
||||
// 备份数据文件夹目录名称,隐藏目录
|
||||
BackupMetaDirName = ".adrive"
|
||||
)
|
||||
|
||||
// GetFileInfoByPaths 获取指定文件路径的文件详情信息
|
||||
func GetAppFileInfoByPaths(driveId string, paths ...string) (fileInfoList []*aliyunpan.FileEntity, failedPaths []string, error error) {
|
||||
if len(paths) <= 0 {
|
||||
@@ -89,7 +81,7 @@ func RandomStr(count int) string {
|
||||
STR_SET := "abcdefjhijklmnopqrstuvwxyz1234567890"
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
str := strings.Builder{}
|
||||
for i := 0; i < count; i++ {
|
||||
for i := 0; i < count; i++ {
|
||||
str.WriteByte(byte(STR_SET[rand.Intn(len(STR_SET))]))
|
||||
}
|
||||
return str.String()
|
||||
@@ -100,7 +92,7 @@ func GetAllPathFolderByPath(pathStr string) []string {
|
||||
dirs := []string{}
|
||||
p := "/"
|
||||
dirs = append(dirs, p)
|
||||
for _,s := range dirNames {
|
||||
for _, s := range dirNames {
|
||||
p = path.Join(p, s)
|
||||
dirs = append(dirs, p)
|
||||
}
|
||||
@@ -114,7 +106,7 @@ func EscapeStr(s string) string {
|
||||
|
||||
// UnescapeStr 反转义字符串
|
||||
func UnescapeStr(s string) string {
|
||||
r,_ := url.PathUnescape(s)
|
||||
r, _ := url.PathUnescape(s)
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -130,7 +122,7 @@ func RefreshTokenInNeed(activeUser *config.PanUser) bool {
|
||||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||||
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", activeUser.WebToken.ExpireTime, cz)
|
||||
now := time.Now()
|
||||
if (expiredTime.Unix() - now.Unix()) <= (10 * 60) { // 10min
|
||||
if (expiredTime.Unix() - now.Unix()) <= (20 * 60) { // 20min
|
||||
// need update refresh token
|
||||
logger.Verboseln("access token expired, get new from refresh token")
|
||||
if wt, er := aliyunpan.GetAccessTokenFromRefreshToken(activeUser.RefreshToken); er == nil {
|
||||
@@ -143,4 +135,29 @@ func RefreshTokenInNeed(activeUser *config.PanUser) bool {
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ReloadRefreshTokenInNeed 从配置文件加载最新token
|
||||
func ReloadRefreshTokenInNeed(activeUser *config.PanUser) bool {
|
||||
if activeUser == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// refresh expired token
|
||||
if activeUser.PanClient() != nil {
|
||||
if len(activeUser.WebToken.RefreshToken) > 0 {
|
||||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||||
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", activeUser.WebToken.ExpireTime, cz)
|
||||
now := time.Now()
|
||||
if (expiredTime.Unix() - now.Unix()) <= (10 * 60) { // 10min
|
||||
// reload refresh token from config file
|
||||
u := config.Config.ActiveUser()
|
||||
activeUser.WebToken = u.WebToken
|
||||
activeUser.PanClient().UpdateToken(u.WebToken)
|
||||
logger.Verboseln("reload access token from config file")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
+68
-14
@@ -18,7 +18,10 @@ import (
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/webdav"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CmdWebdav() cli.Command {
|
||||
@@ -59,8 +62,8 @@ aliyunpan webdav start -h
|
||||
2. 使用默认配置启动webdav服务
|
||||
aliyunpan webdav start
|
||||
|
||||
3. 启动webdav服务,并配置IP为127.0.0.1,端口为23077,登录用户名为admin,登录密码为admin123,网盘目录 /webdav_folder 作为服务的根目录
|
||||
aliyunpan webdav start -ip "127.0.0.1" -port 23077 -webdav_user "admin" -webdav_password "admin123" -pan_dir_path "/webdav_folder"
|
||||
3. 启动webdav服务,并配置IP为127.0.0.1,端口为23077,登录用户名为admin,登录密码为admin123,模式为读写,文件网盘目录 /webdav_folder 作为服务的根目录
|
||||
aliyunpan webdav start -ip "127.0.0.1" -port 23077 -webdav_user "admin" -webdav_password "admin123" -webdav_mode "rw" -pan_drive "File" -pan_dir_path "/webdav_folder"
|
||||
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
@@ -68,27 +71,38 @@ aliyunpan webdav start -h
|
||||
fmt.Println("未登录账号,请先登录")
|
||||
return nil
|
||||
}
|
||||
activeUser := GetActiveUser()
|
||||
go func() {
|
||||
for {
|
||||
// token刷新
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
//time.Sleep(time.Duration(5) * time.Second)
|
||||
if ReloadRefreshTokenInNeed(activeUser) {
|
||||
logger.Verboseln("reload new access token for webdav")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
webdavServ := &webdav.WebdavConfig{
|
||||
PanDriveId: "",
|
||||
PanUserId: "",
|
||||
PanUser: nil,
|
||||
PanDriveId: "",
|
||||
PanUserId: "",
|
||||
PanUser: nil,
|
||||
UploadChunkSize: c.Int("bs") * 1024,
|
||||
TransferUrlType: config.Config.TransferUrlType,
|
||||
Address: "0.0.0.0",
|
||||
Port: 23077,
|
||||
Prefix: "/",
|
||||
Users: []webdav.WebdavUser{{
|
||||
Address: "0.0.0.0",
|
||||
Port: 23077,
|
||||
Prefix: "/",
|
||||
Users: []webdav.WebdavUser{{
|
||||
Username: "admin",
|
||||
Password: "admin",
|
||||
Scope: "/",
|
||||
Mode: "rw",
|
||||
}},
|
||||
}
|
||||
|
||||
// pan user
|
||||
panUserId := config.Config.ActiveUID
|
||||
activeUser := GetActiveUser()
|
||||
panUserId := activeUser.UserId
|
||||
webdavServ.PanUserId = panUserId
|
||||
webdavServ.PanDriveId = activeUser.DriveList.GetFileDriveId()
|
||||
webdavServ.PanUser = activeUser
|
||||
|
||||
// address
|
||||
@@ -105,6 +119,20 @@ aliyunpan webdav start -h
|
||||
}
|
||||
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") {
|
||||
@@ -124,15 +152,31 @@ aliyunpan webdav start -h
|
||||
}
|
||||
webdavServ.Users[0].Password = webdavPassword
|
||||
|
||||
webdavMode := "rw"
|
||||
if c.IsSet("webdav_mode") {
|
||||
webdavMode = c.String("webdav_mode")
|
||||
}
|
||||
webdavServ.Users[0].Mode = webdavMode
|
||||
if webdavServ.Users[0].Mode != "rw" && webdavServ.Users[0].Mode != "ro" {
|
||||
webdavServ.Users[0].Mode = "rw"
|
||||
}
|
||||
|
||||
err := config.Config.Save()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
modeStr := "读写"
|
||||
if webdavServ.Users[0].Mode == "rw" {
|
||||
modeStr = "读写"
|
||||
} else if webdavServ.Users[0].Mode == "ro" {
|
||||
modeStr = "只读"
|
||||
}
|
||||
|
||||
fmt.Println("----------------------------------------")
|
||||
fmt.Printf("webdav网盘信息:\n链接:http://localhost:%d\n用户名:%s\n密码:%s\n网盘服务目录:%s\n",
|
||||
webdavServ.Port, webdavServ.Users[0].Username, webdavServ.Users[0].Password, webdavServ.Users[0].Scope)
|
||||
fmt.Printf("webdav网盘信息:\n链接:http://localhost:%d\n用户名:%s\n密码:%s\n网盘模式:%s\n网盘服务类型:%s\n网盘服务目录:%s\n",
|
||||
webdavServ.Port, webdavServ.Users[0].Username, webdavServ.Users[0].Password, modeStr, panDriveNameStr, webdavServ.Users[0].Scope)
|
||||
fmt.Println("----------------------------------------")
|
||||
fmt.Println("webdav在线网盘服务运行中...")
|
||||
webdavServ.StartServer()
|
||||
@@ -156,6 +200,16 @@ aliyunpan webdav start -h
|
||||
Name: "webdav_password",
|
||||
Usage: "Webdav登录密码,默认为:admin",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "webdav_mode",
|
||||
Usage: "Webdav模式,包括:rw-读写,ro-只读,默认为:rw",
|
||||
Value: "rw",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "pan_drive",
|
||||
Usage: "Webdav绑定的网盘类型。File-文件 Album-相册。默认为文件网盘",
|
||||
Value: "File",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "pan_dir_path",
|
||||
Usage: "Webdav绑定的网盘文件夹路径,默认为:/",
|
||||
|
||||
@@ -35,10 +35,10 @@ func (pu *PanUser) CacheFilesDirectoriesList(pathStr string) (fdl aliyunpan.File
|
||||
return nil
|
||||
}
|
||||
fileListParam := &aliyunpan.FileListParam{
|
||||
DriveId: pu.ActiveDriveId,
|
||||
DriveId: pu.ActiveDriveId,
|
||||
ParentFileId: fi.FileId,
|
||||
}
|
||||
fdl, apiError = pu.panClient.FileListGetAll(fileListParam)
|
||||
fdl, apiError = pu.panClient.FileListGetAll(fileListParam, 100)
|
||||
if apiError != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdutil"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdutil/jsonhelper"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/library/homedir"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
@@ -39,11 +40,24 @@ const (
|
||||
// ConfigVersion 配置文件版本
|
||||
ConfigVersion string = "1.0"
|
||||
|
||||
// DefaultUploadParallelNum 默认的文件上传并发数量
|
||||
// DefaultFileUploadParallelNum 默认的文件上传并发数量
|
||||
DefaultFileUploadParallelNum = 10
|
||||
|
||||
// MaxFileUploadParallelNum 最大文件上传并发数量。过大会被阿里云盘风控,导致无法上传
|
||||
MaxFileUploadParallelNum = 20
|
||||
|
||||
// DefaultFileDownloadParallelNum 默认的文件下载并发数量
|
||||
DefaultFileDownloadParallelNum = 5
|
||||
|
||||
// MaxFileDownloadParallelNum 最大文件下载并发数量。过大会被阿里云盘风控,导致无法下载
|
||||
MaxFileDownloadParallelNum = 20
|
||||
|
||||
// DefaultTokenServiceWebHost 默认的token服务
|
||||
DefaultTokenServiceWebHost = "https://api.tickstep.com"
|
||||
//DefaultTokenServiceWebHost = "http://localhost:8977"
|
||||
|
||||
// DefaultVideoFileExtensions 默认的视频文件后缀
|
||||
DefaultVideoFileExtensions = "mp4,flv,mkv,mov,rm,rmvb,wmv,wma,mv,asf,asx,mpg,mpeg,mpe,3gp,m4v,avi,vob"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -54,6 +68,9 @@ var (
|
||||
Config = NewConfig(configFilePath)
|
||||
|
||||
AppVersion string
|
||||
|
||||
// IsAppInCliMode 是否在交互模式
|
||||
IsAppInCliMode = false
|
||||
)
|
||||
|
||||
type UpdateCheckInfo struct {
|
||||
@@ -75,7 +92,7 @@ type PanConfig struct {
|
||||
|
||||
MaxDownloadRate int64 `json:"maxDownloadRate"` // 限制最大下载速度,单位 B/s, 即字节/每秒
|
||||
MaxUploadRate int64 `json:"maxUploadRate"` // 限制最大上传速度,单位 B/s, 即字节/每秒
|
||||
TransferUrlType int `json:"transferUrlType"` // 上传/下载URL类别,1-默认,2-阿里云ECS
|
||||
TransferUrlType int `json:"transferUrlType"` // 上传/下载URL类别,1-默认,2-阿里云ECS
|
||||
|
||||
SaveDir string `json:"saveDir"` // 下载储存路径
|
||||
|
||||
@@ -83,6 +100,8 @@ type PanConfig struct {
|
||||
LocalAddrs string `json:"localAddrs"` // 本地网卡地址
|
||||
UpdateCheckInfo UpdateCheckInfo `json:"updateCheckInfo"`
|
||||
|
||||
VideoFileExtensions string `json:"videoFileExtensions"`
|
||||
|
||||
configFilePath string
|
||||
configFile *os.File
|
||||
fileMu sync.Mutex
|
||||
@@ -185,8 +204,8 @@ func (c *PanConfig) lazyOpenConfigFile() (err error) {
|
||||
}
|
||||
|
||||
c.fileMu.Lock()
|
||||
os.MkdirAll(filepath.Dir(c.configFilePath), 0700)
|
||||
c.configFile, err = os.OpenFile(c.configFilePath, os.O_CREATE|os.O_RDWR, 0600)
|
||||
os.MkdirAll(filepath.Dir(c.configFilePath), 0755)
|
||||
c.configFile, err = os.OpenFile(c.configFilePath, os.O_CREATE|os.O_RDWR, 0755)
|
||||
c.fileMu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
@@ -252,6 +271,7 @@ func (c *PanConfig) initDefaultConfig() {
|
||||
}
|
||||
}
|
||||
c.ConfigVer = ConfigVersion
|
||||
c.VideoFileExtensions = DefaultVideoFileExtensions
|
||||
}
|
||||
|
||||
// GetConfigDir 获取配置路径
|
||||
@@ -282,7 +302,7 @@ func GetConfigDir() string {
|
||||
|
||||
// 3. ~/.aliyunpan/
|
||||
if runtime.GOOS == "linux" || runtime.GOOS == "windows" {
|
||||
cd,er := homedir.Expand("~/.aliyunpan")
|
||||
cd, er := homedir.Expand("~/.aliyunpan")
|
||||
if er == nil {
|
||||
if IsFolderExist(cd) {
|
||||
logger.Verboseln("use config dir: ", cd)
|
||||
@@ -298,6 +318,32 @@ func GetConfigDir() string {
|
||||
return configDir
|
||||
}
|
||||
|
||||
// GetPluginDir 获取插件文件夹路径
|
||||
func GetPluginDir() string {
|
||||
return strings.TrimSuffix(GetConfigDir(), "/") + "/plugin"
|
||||
}
|
||||
|
||||
// GetSyncDriveDir 获取同步备份的文件夹路径
|
||||
func GetSyncDriveDir() string {
|
||||
return strings.TrimSuffix(GetConfigDir(), "/") + "/sync_drive"
|
||||
}
|
||||
|
||||
// GetLogDir 获取日志文件目录路径
|
||||
func GetLogDir() string {
|
||||
return strings.TrimSuffix(GetConfigDir(), "/") + "/logs"
|
||||
}
|
||||
|
||||
// GetLogFilePath 获取日志文件路径
|
||||
func GetLogFilePath() string {
|
||||
dirPath := GetLogDir()
|
||||
if b, e := utils.PathExists(dirPath); e == nil {
|
||||
if !b {
|
||||
os.MkdirAll(dirPath, 0755)
|
||||
}
|
||||
}
|
||||
return dirPath + "/" + "aliyunpan_verbose.log"
|
||||
}
|
||||
|
||||
func (c *PanConfig) ActiveUser() *PanUser {
|
||||
if c.activeUser == nil {
|
||||
if c.UserList == nil {
|
||||
@@ -357,6 +403,7 @@ func (c *PanConfig) SetActiveUser(user *PanUser) *PanUser {
|
||||
u.Nickname = user.Nickname
|
||||
u.WebToken = user.WebToken
|
||||
u.RefreshToken = user.RefreshToken
|
||||
u.TokenId = user.TokenId
|
||||
needToInsert = false
|
||||
break
|
||||
}
|
||||
@@ -417,4 +464,19 @@ func (c *PanConfig) HTTPClient(ua string) *requester.HTTPClient {
|
||||
client.SetUserAgent(ua)
|
||||
}
|
||||
return client
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PanConfig) GetVideoExtensionList() []string {
|
||||
if c.VideoFileExtensions == "" {
|
||||
return []string{}
|
||||
}
|
||||
exts := strings.Split(c.VideoFileExtensions, ",")
|
||||
r := []string{}
|
||||
for _, ex := range exts {
|
||||
ex = strings.TrimSpace(ex)
|
||||
if ex != "" {
|
||||
r = append(r, strings.ToLower(ex))
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -76,8 +76,8 @@ func (c *PanConfig) PrintTable() {
|
||||
[]string{"cache_size", converter.ConvertFileSize(int64(c.CacheSize), 2), "1KB ~ 256KB", "下载缓存, 如果硬盘占用高或下载速度慢, 请尝试调大此值"},
|
||||
[]string{"max_download_parallel", strconv.Itoa(c.MaxDownloadParallel), "1 ~ 20", "最大下载并发量,即同时下载文件最大数量"},
|
||||
[]string{"max_upload_parallel", strconv.Itoa(c.MaxUploadParallel), "1 ~ 20", "最大上传并发量,即同时上传文件最大数量"},
|
||||
[]string{"max_download_rate", showMaxRate(c.MaxDownloadRate), "", "限制最大下载速度, 0代表不限制"},
|
||||
[]string{"max_upload_rate", showMaxRate(c.MaxUploadRate), "", "限制最大上传速度, 0代表不限制"},
|
||||
[]string{"max_download_rate", showMaxRate(c.MaxDownloadRate), "", "限制单个文件最大下载速度, 0代表不限制"},
|
||||
[]string{"max_upload_rate", showMaxRate(c.MaxUploadRate), "", "限制单个文件最大上传速度, 0代表不限制"},
|
||||
[]string{"transfer_url_type", strconv.Itoa(c.TransferUrlType), "1-默认,2-阿里云ECS", "上传下载URL类别。除非在阿里云ECS(暂只支持经典网络)服务器中使用,不然请设置1"},
|
||||
[]string{"savedir", c.SaveDir, "", "下载文件的储存目录"},
|
||||
[]string{"proxy", c.Proxy, "", "设置代理, 支持 http/socks5 代理,例如:http://127.0.0.1:8888"},
|
||||
|
||||
+28
-18
@@ -26,12 +26,12 @@ import (
|
||||
type DriveInfo struct {
|
||||
DriveId string `json:"driveId"`
|
||||
DriveName string `json:"driveName"`
|
||||
DriveTag string `json:"driveTag"`
|
||||
DriveTag string `json:"driveTag"`
|
||||
}
|
||||
type DriveInfoList []*DriveInfo
|
||||
|
||||
func (d DriveInfoList) GetFileDriveId() string {
|
||||
for _,drive := range d {
|
||||
for _, drive := range d {
|
||||
if drive.DriveTag == "File" {
|
||||
return drive.DriveId
|
||||
}
|
||||
@@ -39,24 +39,34 @@ func (d DriveInfoList) GetFileDriveId() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d DriveInfoList) GetAlbumDriveId() string {
|
||||
for _, drive := range d {
|
||||
if drive.DriveTag == "Album" {
|
||||
return drive.DriveId
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type PanUser struct {
|
||||
UserId string `json:"userId"`
|
||||
Nickname string `json:"nickname"`
|
||||
Nickname string `json:"nickname"`
|
||||
AccountName string `json:"accountName"`
|
||||
|
||||
Workdir string `json:"workdir"`
|
||||
Workdir string `json:"workdir"`
|
||||
WorkdirFileEntity aliyunpan.FileEntity `json:"workdirFileEntity"`
|
||||
|
||||
AlbumWorkdir string `json:"albumWorkdir"`
|
||||
AlbumWorkdir string `json:"albumWorkdir"`
|
||||
AlbumWorkdirFileEntity aliyunpan.FileEntity `json:"albumWorkdirFileEntity"`
|
||||
|
||||
ActiveDriveId string `json:"activeDriveId"`
|
||||
DriveList DriveInfoList `json:"driveList"`
|
||||
ActiveDriveId string `json:"activeDriveId"`
|
||||
DriveList DriveInfoList `json:"driveList"`
|
||||
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
WebToken aliyunpan.WebLoginToken `json:"webToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
WebToken aliyunpan.WebLoginToken `json:"webToken"`
|
||||
TokenId string `json:"tokenId"`
|
||||
|
||||
panClient *aliyunpan.PanClient
|
||||
panClient *aliyunpan.PanClient
|
||||
cacheOpMap cachemap.CacheOpMap
|
||||
}
|
||||
|
||||
@@ -72,9 +82,9 @@ func SetupUserByCookie(webToken *aliyunpan.WebLoginToken) (user *PanUser, err *a
|
||||
doLoginAct:
|
||||
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{})
|
||||
u := &PanUser{
|
||||
WebToken: *webToken,
|
||||
panClient: panClient,
|
||||
Workdir: "/",
|
||||
WebToken: *webToken,
|
||||
panClient: panClient,
|
||||
Workdir: "/",
|
||||
WorkdirFileEntity: *aliyunpan.NewFileEntityForRootDir(),
|
||||
}
|
||||
|
||||
@@ -83,7 +93,7 @@ doLoginAct:
|
||||
if err != nil {
|
||||
if err.Code == apierror.ApiCodeTokenExpiredCode && tryRefreshWebToken {
|
||||
tryRefreshWebToken = false
|
||||
webCookie,_ := aliyunpan.GetAccessTokenFromRefreshToken(webToken.RefreshToken)
|
||||
webCookie, _ := aliyunpan.GetAccessTokenFromRefreshToken(webToken.RefreshToken)
|
||||
if webCookie != nil {
|
||||
webToken = webCookie
|
||||
goto doLoginAct
|
||||
@@ -171,7 +181,7 @@ func (pu *PanUser) GetSavePath(filePanPath string) string {
|
||||
}
|
||||
|
||||
func (pu *PanUser) GetDriveByTag(tag string) *DriveInfo {
|
||||
for _,item := range pu.DriveList {
|
||||
for _, item := range pu.DriveList {
|
||||
if item.DriveTag == tag {
|
||||
return item
|
||||
}
|
||||
@@ -180,7 +190,7 @@ func (pu *PanUser) GetDriveByTag(tag string) *DriveInfo {
|
||||
}
|
||||
|
||||
func (pu *PanUser) GetDriveById(id string) *DriveInfo {
|
||||
for _,item := range pu.DriveList {
|
||||
for _, item := range pu.DriveList {
|
||||
if item.DriveId == id {
|
||||
return item
|
||||
}
|
||||
@@ -189,7 +199,7 @@ func (pu *PanUser) GetDriveById(id string) *DriveInfo {
|
||||
}
|
||||
|
||||
func (pu *PanUser) GetActiveDriveInfo() *DriveInfo {
|
||||
for _,item := range pu.DriveList {
|
||||
for _, item := range pu.DriveList {
|
||||
if item.DriveId == pu.ActiveDriveId {
|
||||
return item
|
||||
}
|
||||
@@ -213,4 +223,4 @@ func (di *DriveInfo) IsFileDrive() bool {
|
||||
|
||||
func (di *DriveInfo) IsAlbumDrive() bool {
|
||||
return di.DriveTag == "Album"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func (pl *PanUserList) String() string {
|
||||
tb.SetHeader([]string{"#", "uid", "用户名", "昵称"})
|
||||
|
||||
for k, userInfo := range *pl {
|
||||
tb.Append([]string{strconv.Itoa(k), userInfo.UserId, userInfo.AccountName, userInfo.Nickname})
|
||||
tb.Append([]string{strconv.Itoa(k + 1), userInfo.UserId, userInfo.AccountName, userInfo.Nickname})
|
||||
}
|
||||
|
||||
tb.Render()
|
||||
@@ -95,7 +95,7 @@ func DecryptString(text string) string {
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
d, _ := hex.DecodeString(text)
|
||||
d, _ := hex.DecodeString(text)
|
||||
|
||||
// use the machine unique id as the key
|
||||
// but in some OS, this key will be changed if you reinstall the OS
|
||||
@@ -120,4 +120,4 @@ func IsFolderExist(pathStr string) bool {
|
||||
return false
|
||||
}
|
||||
return fi.IsDir()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +50,10 @@ type (
|
||||
onDownloadStatusEvent DownloadStatusFunc //状态处理事件
|
||||
|
||||
monitorCancelFunc context.CancelFunc
|
||||
globalSpeedsStat *speeds.Speeds // 全局速度统计
|
||||
globalSpeedsStat *speeds.Speeds // 全局速度统计
|
||||
|
||||
fileInfo *aliyunpan.FileEntity // 下载的文件信息
|
||||
driveId string
|
||||
fileInfo *aliyunpan.FileEntity // 下载的文件信息
|
||||
driveId string
|
||||
loadBalancerCompareFunc LoadBalancerCompareFunc // 负载均衡检测函数
|
||||
durlCheckFunc DURLCheckFunc // 下载url检测函数
|
||||
statusCodeBodyCheckFunc StatusCodeBodyCheckFunc
|
||||
@@ -76,9 +76,9 @@ type (
|
||||
//NewDownloader 初始化Downloader
|
||||
func NewDownloader(writer io.WriterAt, config *Config, p *aliyunpan.PanClient, globalSpeedsStat *speeds.Speeds) (der *Downloader) {
|
||||
der = &Downloader{
|
||||
config: config,
|
||||
writer: writer,
|
||||
panClient: p,
|
||||
config: config,
|
||||
writer: writer,
|
||||
panClient: p,
|
||||
globalSpeedsStat: globalSpeedsStat,
|
||||
}
|
||||
return
|
||||
@@ -216,7 +216,7 @@ func (der *Downloader) checkLoadBalancers() *LoadBalancerResponseList {
|
||||
}
|
||||
|
||||
loadBalancer := &LoadBalancerResponse{
|
||||
URL: req.URL.String(),
|
||||
URL: req.URL.String(),
|
||||
}
|
||||
|
||||
loadBalancerResponses = append(loadBalancerResponses, loadBalancer)
|
||||
@@ -286,6 +286,12 @@ func (der *Downloader) checkLoadBalancers() *LoadBalancerResponseList {
|
||||
func (der *Downloader) Execute() error {
|
||||
der.lazyInit()
|
||||
|
||||
// zero file, no need to download data
|
||||
if der.fileInfo.FileSize == 0 {
|
||||
cmdutil.Trigger(der.onFinishEvent)
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
loadBalancerResponseList = der.checkLoadBalancers()
|
||||
bii *transfer.DownloadInstanceInfo
|
||||
@@ -300,7 +306,7 @@ func (der *Downloader) Execute() error {
|
||||
var (
|
||||
isInstance = bii != nil // 是否存在断点信息
|
||||
status *transfer.DownloadStatus
|
||||
single = false // 默认开启多线程下载
|
||||
single = false // 默认开启多线程下载
|
||||
)
|
||||
if !isInstance {
|
||||
bii = &transfer.DownloadInstanceInfo{}
|
||||
@@ -374,7 +380,7 @@ func (der *Downloader) Execute() error {
|
||||
var apierr *apierror.ApiError
|
||||
durl, apierr := der.panClient.GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
|
||||
DriveId: der.driveId,
|
||||
FileId: der.fileInfo.FileId,
|
||||
FileId: der.fileInfo.FileId,
|
||||
})
|
||||
time.Sleep(time.Duration(200) * time.Millisecond)
|
||||
if apierr != nil {
|
||||
@@ -382,7 +388,7 @@ func (der *Downloader) Execute() error {
|
||||
cmdutil.Trigger(der.onCancelEvent)
|
||||
return apierr
|
||||
}
|
||||
if durl == nil || durl.Url == "" {
|
||||
if durl == nil || durl.Url == "" || durl.Url == aliyunpan.IllegalDownloadUrl {
|
||||
logger.Verbosef("无法获取有效的下载链接: %+v\n", durl)
|
||||
cmdutil.Trigger(der.onCancelEvent)
|
||||
der.removeInstanceState() // 移除断点续传文件
|
||||
@@ -397,7 +403,7 @@ func (der *Downloader) Execute() error {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Verbosef("work id: %d, download url: %s\n", k, durl)
|
||||
logger.Verbosef("work id: %d, download url: %v\n", k, durl)
|
||||
client := requester.NewHTTPClient()
|
||||
client.SetKeepAlive(true)
|
||||
client.SetTimeout(10 * time.Minute)
|
||||
|
||||
@@ -63,6 +63,8 @@ const (
|
||||
StatusCodeCanceled
|
||||
//StatusCodeDownloadUrlExpired 下载链接已过期
|
||||
StatusCodeDownloadUrlExpired
|
||||
//StatusCodeIllegalDownloadFile 文件非法,不允许下载
|
||||
StatusCodeIllegalDownloadFile
|
||||
)
|
||||
|
||||
//GetStatusText 根据状态码获取状态信息
|
||||
|
||||
@@ -16,8 +16,8 @@ package uploader
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
@@ -33,13 +33,13 @@ type (
|
||||
}
|
||||
|
||||
fileBlock struct {
|
||||
readRange transfer.Range
|
||||
readed int64
|
||||
readerAt io.ReaderAt
|
||||
speedsStatRef *speeds.Speeds
|
||||
readRange transfer.Range
|
||||
readed int64
|
||||
readerAt io.ReaderAt
|
||||
speedsStatRef *speeds.Speeds
|
||||
globalSpeedsStatRef *speeds.Speeds
|
||||
rateLimit *speeds.RateLimit
|
||||
mu sync.Mutex
|
||||
rateLimit *speeds.RateLimit
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
bufioFileBlock struct {
|
||||
@@ -66,11 +66,11 @@ func SplitBlock(fileSize, blockSize int64) (blockList []*BlockState) {
|
||||
// NewBufioSplitUnit io.ReaderAt实现SplitUnit接口, 有Buffer支持
|
||||
func NewBufioSplitUnit(readerAt io.ReaderAt, readRange transfer.Range, speedsStat *speeds.Speeds, rateLimit *speeds.RateLimit, globalSpeedsStat *speeds.Speeds) SplitUnit {
|
||||
su := &fileBlock{
|
||||
readerAt: readerAt,
|
||||
readRange: readRange,
|
||||
speedsStatRef: speedsStat,
|
||||
readerAt: readerAt,
|
||||
readRange: readRange,
|
||||
speedsStatRef: speedsStat,
|
||||
globalSpeedsStatRef: globalSpeedsStat,
|
||||
rateLimit: rateLimit,
|
||||
rateLimit: rateLimit,
|
||||
}
|
||||
return &bufioFileBlock{
|
||||
fileBlock: su,
|
||||
|
||||
@@ -16,11 +16,11 @@ package uploader
|
||||
import (
|
||||
"context"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"github.com/tickstep/library-go/requester/rio"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -44,13 +44,13 @@ type (
|
||||
|
||||
instanceState *InstanceState
|
||||
|
||||
multiUpload MultiUpload // 上传体接口
|
||||
file rio.ReaderAtLen64 // 上传
|
||||
config *MultiUploaderConfig
|
||||
workers workerList
|
||||
speedsStat *speeds.Speeds
|
||||
rateLimit *speeds.RateLimit
|
||||
globalSpeedsStat *speeds.Speeds // 全局速度统计
|
||||
multiUpload MultiUpload // 上传体接口
|
||||
file rio.ReaderAtLen64 // 上传
|
||||
config *MultiUploaderConfig
|
||||
workers workerList
|
||||
speedsStat *speeds.Speeds
|
||||
rateLimit *speeds.RateLimit
|
||||
globalSpeedsStat *speeds.Speeds // 全局速度统计
|
||||
|
||||
executeTime time.Time
|
||||
finished chan struct{}
|
||||
@@ -71,12 +71,12 @@ type (
|
||||
)
|
||||
|
||||
// NewMultiUploader 初始化上传
|
||||
func NewMultiUploader(multiUpload MultiUpload, file rio.ReaderAtLen64, config *MultiUploaderConfig, uploadOpEntity *aliyunpan.CreateFileUploadResult, globalSpeedsStat *speeds.Speeds) *MultiUploader {
|
||||
func NewMultiUploader(multiUpload MultiUpload, file rio.ReaderAtLen64, config *MultiUploaderConfig, uploadOpEntity *aliyunpan.CreateFileUploadResult, globalSpeedsStat *speeds.Speeds) *MultiUploader {
|
||||
return &MultiUploader{
|
||||
multiUpload: multiUpload,
|
||||
file: file,
|
||||
config: config,
|
||||
UploadOpEntity: uploadOpEntity,
|
||||
multiUpload: multiUpload,
|
||||
file: file,
|
||||
config: config,
|
||||
UploadOpEntity: uploadOpEntity,
|
||||
globalSpeedsStat: globalSpeedsStat,
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,7 @@ func (muer *MultiUploader) check() {
|
||||
}
|
||||
|
||||
// Execute 执行上传
|
||||
func (muer *MultiUploader) Execute() {
|
||||
func (muer *MultiUploader) Execute() error {
|
||||
muer.check()
|
||||
muer.lazyInit()
|
||||
|
||||
@@ -171,6 +171,7 @@ func (muer *MultiUploader) Execute() {
|
||||
utils.TriggerOnSync(muer.onSuccessEvent)
|
||||
}
|
||||
utils.TriggerOnSync(muer.onFinishEvent)
|
||||
return err
|
||||
}
|
||||
|
||||
// InstanceState 返回断点续传信息
|
||||
@@ -178,8 +179,8 @@ func (muer *MultiUploader) InstanceState() *InstanceState {
|
||||
blockStates := make([]*BlockState, 0, len(muer.workers))
|
||||
for _, wer := range muer.workers {
|
||||
blockStates = append(blockStates, &BlockState{
|
||||
ID: wer.id,
|
||||
Range: wer.splitUnit.Range(),
|
||||
ID: wer.id,
|
||||
Range: wer.splitUnit.Range(),
|
||||
UploadDone: wer.uploadDone,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ package uploader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/tickstep/aliyunpan/internal/waitgroup"
|
||||
"github.com/oleiade/lane"
|
||||
"github.com/tickstep/aliyunpan/internal/waitgroup"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -27,7 +27,7 @@ type (
|
||||
id int
|
||||
partOffset int64
|
||||
splitUnit SplitUnit
|
||||
uploadDone bool
|
||||
uploadDone bool
|
||||
}
|
||||
|
||||
workerList []*worker
|
||||
@@ -114,11 +114,10 @@ func (muer *MultiUploader) upload() (uperr error) {
|
||||
} else if me.NeedStartOver {
|
||||
uploaderVerbose.Warnf("upload start over: %d\n", wer.id)
|
||||
// 从头开始上传
|
||||
uploadDeque = lane.NewDeque()
|
||||
for _,item := range muer.workers {
|
||||
item.uploadDone = false
|
||||
uploadDeque.Append(item)
|
||||
}
|
||||
muer.closeCanceledOnce.Do(func() { // 只关闭一次
|
||||
close(muer.canceled)
|
||||
})
|
||||
uperr = me.Err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,16 @@ import (
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/file/downloader"
|
||||
"github.com/tickstep/aliyunpan/internal/functions"
|
||||
"github.com/tickstep/aliyunpan/internal/localfile"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/tickstep/aliyunpan/internal/taskframework"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -47,7 +50,7 @@ type (
|
||||
ParentTaskExecutor *taskframework.TaskExecutor
|
||||
|
||||
DownloadStatistic *DownloadStatistic // 下载统计
|
||||
GlobalSpeedsStat *speeds.Speeds // 全局速度统计
|
||||
GlobalSpeedsStat *speeds.Speeds // 全局速度统计
|
||||
|
||||
// 可选项
|
||||
VerbosePrinter *logger.CmdVerbose
|
||||
@@ -57,10 +60,10 @@ type (
|
||||
IsOverwrite bool // 是否覆盖已存在的文件
|
||||
NoCheck bool // 不校验文件
|
||||
|
||||
FilePanPath string // 要下载的网盘文件路径
|
||||
SavePath string // 文件保存在本地的路径
|
||||
OriginSaveRootPath string // 文件保存在本地的根目录路径
|
||||
DriveId string
|
||||
FilePanPath string // 要下载的网盘文件路径
|
||||
SavePath string // 文件保存在本地的路径
|
||||
OriginSaveRootPath string // 文件保存在本地的根目录路径
|
||||
DriveId string
|
||||
|
||||
fileInfo *aliyunpan.FileEntity // 文件或目录详情
|
||||
}
|
||||
@@ -93,32 +96,64 @@ func (dtu *DownloadTaskUnit) verboseInfof(format string, a ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// download 执行下载
|
||||
// download 执行下载文件(非目录)
|
||||
func (dtu *DownloadTaskUnit) download() (err error) {
|
||||
var (
|
||||
writer downloader.Writer
|
||||
file *os.File
|
||||
)
|
||||
|
||||
dtu.Cfg.InstanceStatePath = dtu.SavePath + DownloadSuffix
|
||||
|
||||
// 创建下载的目录
|
||||
// 获取SavePath所在的目录
|
||||
dir := filepath.Dir(dtu.SavePath)
|
||||
fileInfo, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
// 目录不存在, 创建
|
||||
err = os.MkdirAll(dir, 0777)
|
||||
//fileInfo, err := os.Stat(dir)
|
||||
//if err != nil {
|
||||
// // 目录不存在, 创建
|
||||
// err = os.MkdirAll(dir, 0777)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//} else if !fileInfo.IsDir() {
|
||||
// // SavePath所在的目录不是目录
|
||||
// return fmt.Errorf("%s, path %s: not a directory", StrDownloadInitError, dir)
|
||||
//}
|
||||
// 支持本地符号链接文件,整体逻辑和上面注释代码一致
|
||||
savePathSymlinkFile := localfile.SymlinkFile{
|
||||
LogicPath: dtu.SavePath,
|
||||
RealPath: "",
|
||||
}
|
||||
originSaveRootSymlinkFile := localfile.NewSymlinkFile(dtu.OriginSaveRootPath)
|
||||
suffixPath := localfile.GetSuffixPath(dir, dtu.OriginSaveRootPath)
|
||||
saveDirPathSymlinkFile, saveDirPathFileInfo, err := localfile.RetrieveRealPathFromLogicSuffixPath(originSaveRootSymlinkFile, suffixPath)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
// 本地保存目录不存在,需要创建对应的保存目录
|
||||
realSaveDirPath := saveDirPathSymlinkFile.RealPath
|
||||
suffixPath = localfile.GetSuffixPath(filepath.Dir(dtu.SavePath), saveDirPathSymlinkFile.LogicPath) // 获取后缀不存在的路径
|
||||
if suffixPath != "" {
|
||||
realSaveDirPath = filepath.Join(realSaveDirPath, suffixPath) // 拼接
|
||||
}
|
||||
// 创建保存目录
|
||||
err = os.MkdirAll(realSaveDirPath, 0777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !fileInfo.IsDir() {
|
||||
|
||||
// 拼接完整的保存文件路径
|
||||
savePathSymlinkFile.RealPath = filepath.Join(realSaveDirPath, filepath.Base(localfile.CleanPath(dtu.SavePath)))
|
||||
} else if !saveDirPathFileInfo.IsDir() {
|
||||
// SavePath所在的目录不是目录
|
||||
return fmt.Errorf("%s, path %s: not a directory", StrDownloadInitError, dir)
|
||||
return fmt.Errorf("%s, path %s: not a directory", StrDownloadInitError, saveDirPathSymlinkFile.RealPath)
|
||||
} else {
|
||||
// 保存目录已存在,直接拼接成文件完整保存路径
|
||||
savePathSymlinkFile.RealPath = filepath.Join(saveDirPathSymlinkFile.RealPath, filepath.Base(localfile.CleanPath(dtu.SavePath)))
|
||||
}
|
||||
savePathSymlinkFile, _, _ = localfile.RetrieveRealPath(savePathSymlinkFile)
|
||||
|
||||
// 下载配置文件存储路径
|
||||
dtu.Cfg.InstanceStatePath = savePathSymlinkFile.RealPath + DownloadSuffix
|
||||
|
||||
// 打开文件
|
||||
writer, file, err = downloader.NewDownloaderWriterByFilename(dtu.SavePath, os.O_CREATE|os.O_WRONLY, 0666)
|
||||
writer, file, err = downloader.NewDownloaderWriterByFilename(savePathSymlinkFile.RealPath, os.O_CREATE|os.O_WRONLY, 0666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s, %s", StrDownloadInitError, err)
|
||||
}
|
||||
@@ -145,12 +180,12 @@ func (dtu *DownloadTaskUnit) download() (err error) {
|
||||
if dtu.IsPrintStatus {
|
||||
// 输出所有的worker状态
|
||||
var (
|
||||
tb = cmdtable.NewTable(builder)
|
||||
tb = cmdtable.NewTable(builder)
|
||||
)
|
||||
tb.SetHeader([]string{"#", "status", "range", "left", "speeds", "error"})
|
||||
workersCallback(func(key int, worker *downloader.Worker) bool {
|
||||
wrange := worker.GetRange()
|
||||
tb.Append([]string{fmt.Sprint(worker.ID()), worker.GetStatus().StatusText(), wrange.ShowDetails(), strconv.FormatInt(wrange.Len(), 10), converter.ConvertFileSize(worker.GetSpeedsPerSecond(), 2)+"/s", fmt.Sprint(worker.Err())})
|
||||
tb.Append([]string{fmt.Sprint(worker.ID()), worker.GetStatus().StatusText(), wrange.ShowDetails(), strconv.FormatInt(wrange.Len(), 10), converter.ConvertFileSize(worker.GetSpeedsPerSecond(), 2) + "/s", fmt.Sprint(worker.Err())})
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -270,7 +305,7 @@ func (dtu *DownloadTaskUnit) handleError(result *taskframework.TaskUnitRunResult
|
||||
result.NeedRetry = true
|
||||
}
|
||||
}
|
||||
time.Sleep(1*time.Second)
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
//checkFileValid 检测文件有效性
|
||||
@@ -328,9 +363,14 @@ func (dtu *DownloadTaskUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunRes
|
||||
}
|
||||
|
||||
func (dtu *DownloadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
// 执行插件
|
||||
dtu.pluginCallback("success")
|
||||
}
|
||||
|
||||
func (dtu *DownloadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
// 失败
|
||||
dtu.pluginCallback("fail")
|
||||
|
||||
// 失败
|
||||
if lastRunResult.Err == nil {
|
||||
// result中不包含Err, 忽略输出
|
||||
@@ -340,9 +380,37 @@ func (dtu *DownloadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunRe
|
||||
fmt.Printf("[%s] %s, %s\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err)
|
||||
}
|
||||
|
||||
func (dtu *DownloadTaskUnit) pluginCallback(result string) {
|
||||
if dtu.fileInfo == nil {
|
||||
return
|
||||
}
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
pluginParam := &plugins.DownloadFileFinishParams{
|
||||
DriveId: dtu.fileInfo.DriveId,
|
||||
DriveFilePath: dtu.fileInfo.Path,
|
||||
DriveFileName: dtu.fileInfo.FileName,
|
||||
DriveFileSize: dtu.fileInfo.FileSize,
|
||||
DriveFileType: "file",
|
||||
DriveFileSha1: dtu.fileInfo.ContentHash,
|
||||
DriveFileUpdatedAt: dtu.fileInfo.UpdatedAt,
|
||||
DownloadResult: result,
|
||||
LocalFilePath: dtu.SavePath,
|
||||
}
|
||||
if er := plugin.DownloadFileFinishCallback(plugins.GetContext(config.Config.ActiveUser()), pluginParam); er != nil {
|
||||
logger.Verboseln("插件DownloadFileFinishCallback调用失败: {}", er)
|
||||
} else {
|
||||
logger.Verboseln("插件DownloadFileFinishCallback调用成功")
|
||||
}
|
||||
}
|
||||
|
||||
func (dtu *DownloadTaskUnit) OnComplete(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
}
|
||||
|
||||
func (dtu *DownloadTaskUnit) OnCancel(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
|
||||
}
|
||||
|
||||
func (dtu *DownloadTaskUnit) RetryWait() time.Duration {
|
||||
return functions.RetryWait(dtu.taskInfo.Retry())
|
||||
}
|
||||
@@ -363,33 +431,87 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
dtu.handleError(result)
|
||||
return
|
||||
}
|
||||
time.Sleep(1*time.Second)
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
// 输出文件信息
|
||||
fmt.Print("\n")
|
||||
fmt.Printf("[%s] ----\n%s\n", dtu.taskInfo.Id(), dtu.fileInfo.String())
|
||||
|
||||
// 调用插件
|
||||
ft := "file"
|
||||
if dtu.fileInfo.IsFolder() {
|
||||
ft = "folder"
|
||||
}
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
localFilePath := strings.TrimPrefix(dtu.SavePath, dtu.OriginSaveRootPath)
|
||||
localFilePath = strings.TrimPrefix(strings.TrimPrefix(localFilePath, "\\"), "/")
|
||||
pluginParam := &plugins.DownloadFilePrepareParams{
|
||||
DriveId: dtu.fileInfo.DriveId,
|
||||
DriveFilePath: dtu.fileInfo.Path,
|
||||
DriveFileName: dtu.fileInfo.FileName,
|
||||
DriveFileSize: dtu.fileInfo.FileSize,
|
||||
DriveFileType: ft,
|
||||
DriveFileSha1: dtu.fileInfo.ContentHash,
|
||||
DriveFileUpdatedAt: dtu.fileInfo.UpdatedAt,
|
||||
LocalFilePath: localFilePath,
|
||||
}
|
||||
if downloadFilePrepareResult, er := plugin.DownloadFilePrepareCallback(plugins.GetContext(config.Config.ActiveUser()), pluginParam); er == nil && downloadFilePrepareResult != nil {
|
||||
if strings.Compare("yes", downloadFilePrepareResult.DownloadApproved) != 0 {
|
||||
// skip download this file
|
||||
fmt.Printf("插件取消了该文件下载: %s\n", dtu.fileInfo.Path)
|
||||
result.Succeed = false
|
||||
result.Cancel = true
|
||||
return
|
||||
}
|
||||
if downloadFilePrepareResult.LocalFilePath != "" {
|
||||
targetSaveRelativePath := strings.TrimPrefix(downloadFilePrepareResult.LocalFilePath, "/")
|
||||
targetSaveRelativePath = strings.TrimPrefix(targetSaveRelativePath, "\\")
|
||||
dtu.SavePath = path.Clean(dtu.OriginSaveRootPath + string(os.PathSeparator) + targetSaveRelativePath)
|
||||
fmt.Printf("插件修改文件下载保存路径为: %s\n", dtu.SavePath)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是一个目录, 将子文件和子目录加入队列
|
||||
if dtu.fileInfo.IsFolder() {
|
||||
_, err := os.Stat(dtu.SavePath)
|
||||
//_, err := os.Stat(dtu.SavePath)
|
||||
//if err != nil && !os.IsExist(err) {
|
||||
// os.MkdirAll(dtu.SavePath, 0777) // 首先在本地创建目录, 保证空目录也能被保存
|
||||
//}
|
||||
// 支持本地符号逻辑文件,整体逻辑等效上面的注释代码
|
||||
originSaveRootSymlinkFile := localfile.NewSymlinkFile(dtu.OriginSaveRootPath)
|
||||
suffixPath := localfile.GetSuffixPath(dtu.SavePath, dtu.OriginSaveRootPath)
|
||||
savePathSymlinkFile, _, err := localfile.RetrieveRealPathFromLogicSuffixPath(originSaveRootSymlinkFile, suffixPath)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
os.MkdirAll(dtu.SavePath, 0777) // 首先在本地创建目录, 保证空目录也能被保存
|
||||
realSavePath := savePathSymlinkFile.RealPath
|
||||
suffixPath = localfile.GetSuffixPath(dtu.SavePath, savePathSymlinkFile.LogicPath) // 获取后缀不存在的路径
|
||||
if suffixPath != "" {
|
||||
realSavePath = filepath.Join(realSavePath, suffixPath)
|
||||
}
|
||||
err1 := os.MkdirAll(realSavePath, 0777) // 首先在本地创建目录, 保证空目录也能被保存
|
||||
if err1 != nil {
|
||||
result.ResultMessage = "创建目录错误"
|
||||
result.Succeed = false
|
||||
result.Err = err1
|
||||
result.NeedRetry = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 获取该目录下的文件列表
|
||||
fileList,apierr := dtu.PanClient.FileListGetAll(&aliyunpan.FileListParam{
|
||||
fileList, apierr := dtu.PanClient.FileListGetAll(&aliyunpan.FileListParam{
|
||||
DriveId: dtu.DriveId,
|
||||
ParentFileId: dtu.fileInfo.FileId,
|
||||
})
|
||||
}, 1000)
|
||||
if apierr != nil {
|
||||
// retry one more time
|
||||
time.Sleep(3*time.Second)
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
fileList,apierr = dtu.PanClient.FileListGetAll(&aliyunpan.FileListParam{
|
||||
fileList, apierr = dtu.PanClient.FileListGetAll(&aliyunpan.FileListParam{
|
||||
DriveId: dtu.DriveId,
|
||||
ParentFileId: dtu.fileInfo.FileId,
|
||||
})
|
||||
}, 1000)
|
||||
if apierr != nil {
|
||||
logger.Verbosef("[%s] get download file list for %s error: %s\n",
|
||||
dtu.taskInfo.Id(), dtu.FilePanPath, apierr)
|
||||
@@ -408,7 +530,7 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
result.NeedRetry = true
|
||||
return
|
||||
}
|
||||
time.Sleep(1*time.Second)
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// 创建对应的任务进行下载
|
||||
for k := range fileList {
|
||||
@@ -438,7 +560,13 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
|
||||
fmt.Printf("[%s] 准备下载: %s\n", dtu.taskInfo.Id(), dtu.FilePanPath)
|
||||
|
||||
if !dtu.IsOverwrite && FileExist(dtu.SavePath) {
|
||||
//if !dtu.IsOverwrite && FileExist(dtu.SavePath) {
|
||||
// fmt.Printf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||
// result.Succeed = true // 执行成功
|
||||
// return
|
||||
//}
|
||||
// 支持符号文件,逻辑和注释代码一致
|
||||
if !dtu.IsOverwrite && SymlinkFileExist(dtu.SavePath, dtu.OriginSaveRootPath) {
|
||||
fmt.Printf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||
result.Succeed = true // 执行成功
|
||||
return
|
||||
|
||||
@@ -15,6 +15,7 @@ package pandownload
|
||||
|
||||
import (
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/internal/localfile"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -26,7 +27,8 @@ func CheckFileValid(filePath string, fileInfo *aliyunpan.FileEntity) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FileExist 检查文件是否存在,
|
||||
// FileExist 检查文件是否存在
|
||||
//
|
||||
// 只有当文件存在, 文件大小不为0或断点续传文件不存在时, 才判断为存在
|
||||
func FileExist(path string) bool {
|
||||
if info, err := os.Stat(path); err == nil {
|
||||
@@ -40,3 +42,27 @@ func FileExist(path string) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SymlinkFileExist 检查文件是否存在
|
||||
//
|
||||
// 逻辑和 FileExist 一致,增加符号链接文件的支持
|
||||
func SymlinkFileExist(fullPath, rootPath string) bool {
|
||||
originSaveRootSymlinkFile := localfile.NewSymlinkFile(rootPath)
|
||||
suffixPath := localfile.GetSuffixPath(fullPath, rootPath)
|
||||
savePathSymlinkFile, savePathFileInfo, err := localfile.RetrieveRealPathFromLogicSuffixPath(originSaveRootSymlinkFile, suffixPath)
|
||||
if err != nil || savePathFileInfo == nil {
|
||||
return false
|
||||
} else {
|
||||
if savePathFileInfo.IsDir() {
|
||||
return true
|
||||
} else {
|
||||
if savePathFileInfo.Size() == 0 {
|
||||
return false
|
||||
}
|
||||
if _, err = os.Stat(savePathSymlinkFile.RealPath + DownloadSuffix); err != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
package panlogin
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/crypto"
|
||||
"github.com/tickstep/library-go/getip"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LoginHelper struct {
|
||||
webHost string
|
||||
}
|
||||
|
||||
type LoginHttpResult struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// QRCodeUrlResult 二维码登录页面
|
||||
type QRCodeUrlResult struct {
|
||||
TokenId string `json:"tokenId"`
|
||||
TokenUrl string `json:"tokenUrl"`
|
||||
ExpiredTime int `json:"expiredTime"`
|
||||
}
|
||||
|
||||
// QRCodeLoginResult 二维码登录结果
|
||||
type QRCodeLoginResult struct {
|
||||
QrCodeStatus string `json:"qrCodeStatus"`
|
||||
SecureRefreshToken string `json:"secureRefreshToken"`
|
||||
}
|
||||
|
||||
func NewLoginHelper(webHost string) *LoginHelper {
|
||||
return &LoginHelper{
|
||||
webHost: webHost,
|
||||
}
|
||||
}
|
||||
|
||||
// GetQRCodeLoginUrl 获取登录二维码链接
|
||||
func (h *LoginHelper) GetQRCodeLoginUrl(keyStr string) (*QRCodeUrlResult, error) {
|
||||
if keyStr == "" {
|
||||
keyStr = utils.GetUniqueKeyStr()
|
||||
}
|
||||
fullUrl := strings.Builder{}
|
||||
ipAddr, err := getip.IPInfoFromTechainBaidu()
|
||||
if err != nil {
|
||||
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)
|
||||
|
||||
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,
|
||||
}
|
||||
// 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 qr code error ", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errResp := &LoginHttpResult{}
|
||||
if err1 := json.Unmarshal(body, errResp); err1 != nil {
|
||||
logger.Verboseln("parse qrcode result json error ", err1)
|
||||
return nil, err1
|
||||
}
|
||||
if errResp.Code != 0 {
|
||||
return nil, fmt.Errorf(errResp.Msg)
|
||||
}
|
||||
|
||||
// parse result
|
||||
r := &LoginHttpResult{}
|
||||
r.Data = &QRCodeUrlResult{}
|
||||
if err2 := json.Unmarshal(body, r); err2 != nil {
|
||||
logger.Verboseln("parse qrcode result json error ", err2)
|
||||
return nil, err2
|
||||
}
|
||||
|
||||
token := r.Data.(*QRCodeUrlResult)
|
||||
if len(token.TokenUrl) > 0 {
|
||||
u, err := url.Parse(token.TokenUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m, _ := url.ParseQuery(u.RawQuery)
|
||||
token.TokenId = m["tokenId"][0]
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// GetQRCodeLoginResult 获取二维码登录结果
|
||||
func (h *LoginHelper) GetQRCodeLoginResult(tokenId string) (*QRCodeLoginResult, error) {
|
||||
fullUrl := strings.Builder{}
|
||||
fmt.Fprintf(&fullUrl, "%s/auth/tickstep/aliyunpan/token/qrcode/result?tokenId=%s",
|
||||
h.webHost, tokenId)
|
||||
|
||||
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,
|
||||
}
|
||||
// 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 qr code result error ", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errResp := &LoginHttpResult{}
|
||||
if err1 := json.Unmarshal(body, errResp); err1 != nil {
|
||||
logger.Verboseln("parse qrcode result json error ", err1)
|
||||
return nil, err1
|
||||
}
|
||||
if errResp.Code != 0 {
|
||||
return nil, fmt.Errorf(errResp.Msg)
|
||||
}
|
||||
|
||||
// parse result
|
||||
r := &LoginHttpResult{}
|
||||
r.Data = &QRCodeLoginResult{}
|
||||
if err2 := json.Unmarshal(body, r); err2 != nil {
|
||||
logger.Verboseln("parse qrcode result json error ", err2)
|
||||
return nil, err2
|
||||
}
|
||||
return r.Data.(*QRCodeLoginResult), nil
|
||||
}
|
||||
|
||||
// GetRefreshToken 获取Token
|
||||
func (h *LoginHelper) GetRefreshToken(tokenId string) (*QRCodeLoginResult, error) {
|
||||
fullUrl := strings.Builder{}
|
||||
fmt.Fprintf(&fullUrl, "%s/auth/tickstep/aliyunpan/token/qrcode/retrieve?tokenId=%s",
|
||||
h.webHost, tokenId)
|
||||
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,
|
||||
}
|
||||
// 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 refresh token result error ", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errResp := &LoginHttpResult{}
|
||||
if err1 := json.Unmarshal(body, errResp); err1 != nil {
|
||||
logger.Verboseln("parse qrcode result json error ", err1)
|
||||
return nil, err1
|
||||
}
|
||||
if errResp.Code != 0 {
|
||||
return nil, fmt.Errorf(errResp.Msg)
|
||||
}
|
||||
|
||||
// parse result
|
||||
r := &LoginHttpResult{}
|
||||
r.Data = &QRCodeLoginResult{}
|
||||
if err2 := json.Unmarshal(body, r); err2 != nil {
|
||||
logger.Verboseln("parse refresh token result json error ", err2)
|
||||
return nil, err2
|
||||
}
|
||||
return r.Data.(*QRCodeLoginResult), nil
|
||||
}
|
||||
|
||||
// ParseSecureRefreshToken 解密Token
|
||||
func (h *LoginHelper) ParseSecureRefreshToken(keyStr, secureRefreshToken string) (string, error) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Verboseln("decrypt string failed, maybe the key has been changed")
|
||||
}
|
||||
}()
|
||||
|
||||
if len(keyStr) == 0 {
|
||||
keyStr = utils.GetUniqueKeyStr()
|
||||
}
|
||||
|
||||
if secureRefreshToken == "" {
|
||||
return "", nil
|
||||
}
|
||||
d, _ := hex.DecodeString(secureRefreshToken)
|
||||
|
||||
// use the machine unique id as the key
|
||||
// but in some OS, this key will be changed if you reinstall the OS
|
||||
key := []byte(keyStr)
|
||||
if len(key) > 16 {
|
||||
key = key[:16]
|
||||
}
|
||||
r, e := crypto.DecryptAES(d, key)
|
||||
if e != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
refreshToken := string(r)
|
||||
matched, _ := regexp.MatchString(`^[\-a-zA-Z0-9]+`, refreshToken)
|
||||
if !matched {
|
||||
return "", fmt.Errorf("Token解析错误")
|
||||
}
|
||||
return refreshToken, nil
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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 panlogin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/library-go/ids"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetQRCodeLoginUrl(t *testing.T) {
|
||||
h := NewLoginHelper("http://localhost:8977")
|
||||
keyStr := ids.GetUniqueId("", 32)
|
||||
fmt.Println(keyStr)
|
||||
r, e := h.GetQRCodeLoginUrl(keyStr)
|
||||
fmt.Println(e)
|
||||
fmt.Println(r)
|
||||
}
|
||||
|
||||
func TestGetQRCodeLoginResult(t *testing.T) {
|
||||
h := NewLoginHelper("http://localhost:8977")
|
||||
tokenId := "26e69f9978ba4574a1d66e58399fed4e"
|
||||
for {
|
||||
r, e := h.GetQRCodeLoginResult(tokenId)
|
||||
fmt.Println(e)
|
||||
fmt.Println(r)
|
||||
time.Sleep(1 * time.Second)
|
||||
if r.QrCodeStatus == "CONFIRMED" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRefreshToken(t *testing.T) {
|
||||
h := NewLoginHelper("http://localhost:8977")
|
||||
tokenId := "26e69f9978ba4574a1d66e58399fed4e"
|
||||
r, e := h.GetRefreshToken(tokenId)
|
||||
fmt.Println(e)
|
||||
fmt.Println(r)
|
||||
}
|
||||
|
||||
func TestParseRefreshToken(t *testing.T) {
|
||||
h := NewLoginHelper("http://localhost:8977")
|
||||
secureToken := "eff6054736d47b9c31f8839465555ebdff38c878ea0abbcd4b2336b30d33c71c7dac8824e40991654429ae521d8ef471"
|
||||
r, e := h.ParseSecureRefreshToken("", secureToken)
|
||||
fmt.Println(e)
|
||||
fmt.Println(r)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright (c) 2020 tickstep & chenall
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package panupload
|
||||
|
||||
type SyncDb interface {
|
||||
//读取记录,返回值不会是nil
|
||||
Get(key string) (ufm *UploadedFileMeta)
|
||||
//删除单条记录
|
||||
Del(key string) error
|
||||
//根据前辍删除数据库记录,比如删除一个目录时可以连同子目录一起删除
|
||||
DelWithPrefix(prefix string) error
|
||||
Put(key string, value *UploadedFileMeta) error
|
||||
Close() error
|
||||
//读取数据库指定路径前辍的第一条记录(也作为循环获取的初始化,配置Next函数使用)
|
||||
First(prefix string) (*UploadedFileMeta, error)
|
||||
//获取指定路径前辍的的下一条记录
|
||||
Next(prefix string) (*UploadedFileMeta, error)
|
||||
//是否进行自动数据库清理
|
||||
//注: 清理规则,所有以 prefix 前辍开头并且未更新的记录都将被清理,只有在必要的时候才开启这个功能。
|
||||
AutoClean(prefix string, cleanFlag bool)
|
||||
}
|
||||
|
||||
type autoCleanInfo struct {
|
||||
PreFix string
|
||||
SyncTime int64
|
||||
}
|
||||
|
||||
func OpenSyncDb(file string, bucket string) (SyncDb, error) {
|
||||
return openBoltDb(file, bucket)
|
||||
}
|
||||
|
||||
type dbTableField struct {
|
||||
Path string
|
||||
Data []byte
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
// Copyright (c) 2020 tickstep & chenall
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package panupload
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/tickstep/bolt"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"time"
|
||||
)
|
||||
|
||||
type boltDB struct {
|
||||
db *bolt.DB
|
||||
bucket string
|
||||
next map[string]*boltDBScan
|
||||
cleanInfo *autoCleanInfo
|
||||
}
|
||||
|
||||
type boltDBScan struct {
|
||||
entries []*boltKV
|
||||
off int
|
||||
size int
|
||||
}
|
||||
|
||||
type boltKV struct {
|
||||
k []byte
|
||||
v []byte
|
||||
}
|
||||
|
||||
func openBoltDb(file string, bucket string) (SyncDb, error) {
|
||||
db, err := bolt.Open(file + "_bolt.db", 0600, &bolt.Options{Timeout: 5 * time.Second})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Verboseln("open boltDB ok")
|
||||
return &boltDB{db: db, bucket: bucket, next: make(map[string]*boltDBScan)}, nil
|
||||
}
|
||||
|
||||
func (db *boltDB) Get(key string) (data *UploadedFileMeta) {
|
||||
data = &UploadedFileMeta{Path: key}
|
||||
db.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(db.bucket))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
v := b.Get([]byte(key))
|
||||
return jsoniter.Unmarshal(v, data)
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (db *boltDB) Del(key string) error {
|
||||
return db.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(db.bucket))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Delete([]byte(key))
|
||||
})
|
||||
}
|
||||
|
||||
func (db *boltDB) AutoClean(prefix string, cleanFlag bool) {
|
||||
if !cleanFlag {
|
||||
db.cleanInfo = nil
|
||||
} else if db.cleanInfo == nil {
|
||||
db.cleanInfo = &autoCleanInfo{
|
||||
PreFix: prefix,
|
||||
SyncTime: time.Now().Unix(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *boltDB) clean() (count uint) {
|
||||
for ufm, err := db.First(db.cleanInfo.PreFix); err == nil; ufm, err = db.Next(db.cleanInfo.PreFix) {
|
||||
if ufm.LastSyncTime != db.cleanInfo.SyncTime {
|
||||
db.DelWithPrefix(ufm.Path)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *boltDB) DelWithPrefix(prefix string) error {
|
||||
return db.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(db.bucket))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
c := b.Cursor()
|
||||
for k, _ := c.Seek([]byte(prefix)); k != nil && bytes.HasPrefix(k, []byte(prefix)); k, _ = c.Next() {
|
||||
b.Delete(k)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (db *boltDB) First(prefix string) (*UploadedFileMeta, error) {
|
||||
db.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(db.bucket))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
c := b.Cursor()
|
||||
db.next[prefix] = &boltDBScan{
|
||||
entries: []*boltKV{},
|
||||
off: 0,
|
||||
size: 0,
|
||||
}
|
||||
for k, v := c.Seek([]byte(prefix)); k != nil && bytes.HasPrefix(k, []byte(prefix)); k, v = c.Next() {
|
||||
//fmt.Printf("key=%s, value=%s\n", k, v)
|
||||
if len(k) > 0 {
|
||||
db.next[prefix].entries = append(db.next[prefix].entries, &boltKV{
|
||||
k: k,
|
||||
v: v,
|
||||
})
|
||||
}
|
||||
}
|
||||
db.next[prefix].off = 0
|
||||
db.next[prefix].size = len(db.next[prefix].entries)
|
||||
return nil
|
||||
})
|
||||
return db.Next(prefix)
|
||||
}
|
||||
|
||||
func (db *boltDB) Next(prefix string) (*UploadedFileMeta, error) {
|
||||
data := &UploadedFileMeta{}
|
||||
if _,ok := db.next[prefix]; ok {
|
||||
if db.next[prefix].off >= db.next[prefix].size {
|
||||
return nil, fmt.Errorf("no any more record")
|
||||
}
|
||||
kv := db.next[prefix].entries[db.next[prefix].off]
|
||||
db.next[prefix].off++
|
||||
if kv != nil {
|
||||
jsoniter.Unmarshal(kv.v, &data)
|
||||
data.Path = string(kv.k)
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no any more record")
|
||||
}
|
||||
|
||||
func (db *boltDB) Put(key string, value *UploadedFileMeta) error {
|
||||
if db.cleanInfo != nil {
|
||||
value.LastSyncTime = db.cleanInfo.SyncTime
|
||||
}
|
||||
|
||||
return db.db.Update(func(tx *bolt.Tx) error {
|
||||
data, err := jsoniter.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := tx.Bucket([]byte(db.bucket))
|
||||
if b == nil {
|
||||
b,err = tx.CreateBucket([]byte(db.bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return b.Put([]byte(key), data)
|
||||
})
|
||||
}
|
||||
|
||||
func (db *boltDB) Close() error {
|
||||
if db.cleanInfo != nil {
|
||||
db.clean()
|
||||
}
|
||||
if db.db != nil {
|
||||
return db.db.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
@@ -33,32 +34,21 @@ type (
|
||||
PanUpload struct {
|
||||
panClient *aliyunpan.PanClient
|
||||
targetPath string
|
||||
driveId string
|
||||
driveId string
|
||||
|
||||
// 网盘上传参数
|
||||
uploadOpEntity *aliyunpan.CreateFileUploadResult
|
||||
useInternalUrl bool
|
||||
}
|
||||
|
||||
UploadedFileMeta struct {
|
||||
IsFolder bool `json:"isFolder,omitempty"` // 是否目录
|
||||
Path string `json:"-"` // 本地路径,不记录到数据库
|
||||
SHA1 string `json:"sha1,omitempty"` // 文件的 SHA1
|
||||
FileId string `json:"id,omitempty"` //文件、目录ID
|
||||
ParentId string `json:"parentId,omitempty"` //父文件夹ID
|
||||
Size int64 `json:"length,omitempty"` // 文件大小
|
||||
ModTime int64 `json:"modtime,omitempty"` // 修改日期
|
||||
LastSyncTime int64 `json:"synctime,omitempty"` //最后同步时间
|
||||
}
|
||||
|
||||
EmptyReaderLen64 struct {
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
uploadUrlExpired = fmt.Errorf("UrlExpired")
|
||||
uploadPartNotSeq = fmt.Errorf("PartNotSequential")
|
||||
uploadTerminate = fmt.Errorf("UploadErrorTerminate")
|
||||
uploadUrlExpired = fmt.Errorf("UrlExpired")
|
||||
uploadPartNotSeq = fmt.Errorf("PartNotSequential")
|
||||
uploadTerminate = fmt.Errorf("UploadErrorTerminate")
|
||||
uploadPartAlreadyExist = fmt.Errorf("PartAlreadyExist")
|
||||
)
|
||||
|
||||
@@ -72,9 +62,9 @@ func (e EmptyReaderLen64) Len() int64 {
|
||||
|
||||
func NewPanUpload(panClient *aliyunpan.PanClient, targetPath, driveId string, uploadOpEntity *aliyunpan.CreateFileUploadResult, useInternalUrl bool) uploader.MultiUpload {
|
||||
return &PanUpload{
|
||||
panClient: panClient,
|
||||
targetPath: targetPath,
|
||||
driveId: driveId,
|
||||
panClient: panClient,
|
||||
targetPath: targetPath,
|
||||
driveId: driveId,
|
||||
uploadOpEntity: uploadOpEntity,
|
||||
useInternalUrl: useInternalUrl,
|
||||
}
|
||||
@@ -98,10 +88,10 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
if pu.useInternalUrl {
|
||||
uploadUrl = pu.uploadOpEntity.PartInfoList[partseq].InternalUploadURL
|
||||
}
|
||||
if isUrlExpired(uploadUrl) {
|
||||
if IsUrlExpired(uploadUrl) {
|
||||
// get renew upload url
|
||||
infoList := make([]aliyunpan.FileUploadPartInfoParam, 0)
|
||||
for _,item := range pu.uploadOpEntity.PartInfoList {
|
||||
for _, item := range pu.uploadOpEntity.PartInfoList {
|
||||
infoList = append(infoList, aliyunpan.FileUploadPartInfoParam{
|
||||
PartNumber: item.PartNumber,
|
||||
})
|
||||
@@ -115,7 +105,7 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
newUploadInfo, err := pu.panClient.GetUploadUrl(refreshUploadParam)
|
||||
if err != nil {
|
||||
return false, &uploader.MultiError{
|
||||
Err: uploadUrlExpired,
|
||||
Err: uploadUrlExpired,
|
||||
Terminated: false,
|
||||
}
|
||||
}
|
||||
@@ -127,6 +117,7 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
var resp *http.Response
|
||||
var respError error = nil
|
||||
respErr = nil
|
||||
var err error
|
||||
|
||||
// do http upload request
|
||||
if uploadClient == nil {
|
||||
@@ -134,11 +125,14 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
uploadClient.SetTimeout(0)
|
||||
uploadClient.SetKeepAlive(true)
|
||||
}
|
||||
resp, _ = uploadClient.Req(httpMethod, fullUrl, r, headers)
|
||||
resp, err = uploadClient.Req(httpMethod, fullUrl, r, headers)
|
||||
if err != nil {
|
||||
logger.Verbosef("分片上传出错: 分片%d => %s\n", partseq, err)
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
if blen, e := strconv.Atoi(resp.Header.Get("content-length")); e == nil {
|
||||
if blen > 0 {
|
||||
if blen > 0 {
|
||||
buf := make([]byte, blen)
|
||||
resp.Body.Read(buf)
|
||||
logger.Verbosef("分片上传出错: 分片%d => %s\n", partseq, string(buf))
|
||||
@@ -146,25 +140,25 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
errResp := &apierror.ErrorXmlResp{}
|
||||
if err := xml.Unmarshal(buf, errResp); err == nil {
|
||||
if errResp.Code != "" {
|
||||
if "PartNotSequential" == errResp.Code {
|
||||
if "PartNotSequential" == errResp.Code || "NoSuchUpload" == errResp.Code {
|
||||
respError = uploadPartNotSeq
|
||||
respErr = &uploader.MultiError{
|
||||
Err: uploadPartNotSeq,
|
||||
Terminated: false,
|
||||
Err: uploadPartNotSeq,
|
||||
Terminated: false,
|
||||
NeedStartOver: true,
|
||||
}
|
||||
return resp, respError
|
||||
} else if "AccessDenied" == errResp.Code && "Request has expired." == errResp.Message {
|
||||
respError = uploadUrlExpired
|
||||
respErr = &uploader.MultiError{
|
||||
Err: uploadUrlExpired,
|
||||
Err: uploadUrlExpired,
|
||||
Terminated: false,
|
||||
}
|
||||
return resp, respError
|
||||
} else if "PartAlreadyExist" == errResp.Code {
|
||||
respError = uploadPartAlreadyExist
|
||||
respErr = &uploader.MultiError{
|
||||
Err: uploadPartAlreadyExist,
|
||||
Err: uploadPartAlreadyExist,
|
||||
Terminated: false,
|
||||
}
|
||||
return resp, respError
|
||||
@@ -204,10 +198,10 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
if respErr.Err == uploadUrlExpired {
|
||||
// URL过期,获取新的URL
|
||||
guur, er := pu.panClient.GetUploadUrl(&aliyunpan.GetUploadUrlParam{
|
||||
DriveId: pu.driveId,
|
||||
FileId: pu.uploadOpEntity.FileId,
|
||||
UploadId: pu.uploadOpEntity.UploadId,
|
||||
PartInfoList: []aliyunpan.FileUploadPartInfoParam{{PartNumber:(partseq+1)}}, // 阿里云盘partNum从1开始计数,partSeq从0开始
|
||||
DriveId: pu.driveId,
|
||||
FileId: pu.uploadOpEntity.FileId,
|
||||
UploadId: pu.uploadOpEntity.UploadId,
|
||||
PartInfoList: []aliyunpan.FileUploadPartInfoParam{{PartNumber: (partseq + 1)}}, // 阿里云盘partNum从1开始计数,partSeq从0开始
|
||||
})
|
||||
if er != nil {
|
||||
return false, &uploader.MultiError{
|
||||
@@ -247,12 +241,31 @@ func (pu *PanUpload) CommitFile() (cerr error) {
|
||||
var er *apierror.ApiError
|
||||
|
||||
_, er = pu.panClient.CompleteUploadFile(&aliyunpan.CompleteUploadFileParam{
|
||||
DriveId: pu.driveId,
|
||||
FileId: pu.uploadOpEntity.FileId,
|
||||
DriveId: pu.driveId,
|
||||
FileId: pu.uploadOpEntity.FileId,
|
||||
UploadId: pu.uploadOpEntity.UploadId,
|
||||
})
|
||||
if er != nil {
|
||||
return er
|
||||
}
|
||||
|
||||
// 视频文件触发云端转码请求
|
||||
pu.triggerVideoTranscodeAction()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TriggerVideoTranscodeAction 触发视频文件转码成功
|
||||
func (pu *PanUpload) triggerVideoTranscodeAction() {
|
||||
// 视频文件触发云端转码请求
|
||||
if pu.uploadOpEntity != nil && IsVideoFile(pu.uploadOpEntity.FileName) {
|
||||
time.Sleep(3 * time.Second)
|
||||
_, er1 := pu.panClient.VideoGetPreviewPlayInfo(&aliyunpan.VideoGetPreviewPlayInfoParam{
|
||||
DriveId: pu.driveId,
|
||||
FileId: pu.uploadOpEntity.FileId,
|
||||
})
|
||||
if er1 == nil {
|
||||
logger.Verboseln("触发视频文件转码成功:" + pu.uploadOpEntity.FileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ func (ud *UploadingDatabase) UpdateUploading(meta *localfile.LocalFileMeta, stat
|
||||
if uploading.LocalFileMeta == nil {
|
||||
continue
|
||||
}
|
||||
if uploading.LocalFileMeta.EqualLengthMD5(meta) || uploading.LocalFileMeta.Path == meta.Path {
|
||||
if uploading.LocalFileMeta.EqualLengthMD5(meta) || uploading.LocalFileMeta.Path.LogicPath == meta.Path.LogicPath {
|
||||
ud.UploadingList[k].State = state
|
||||
return
|
||||
}
|
||||
@@ -161,7 +161,7 @@ func (ud *UploadingDatabase) Search(meta *localfile.LocalFileMeta) *uploader.Ins
|
||||
if uploading.LocalFileMeta.EqualLengthSHA1(meta) {
|
||||
return uploading.State
|
||||
}
|
||||
if uploading.LocalFileMeta.Path == meta.Path {
|
||||
if uploading.LocalFileMeta.Path.LogicPath == meta.Path.LogicPath {
|
||||
// 移除旧的信息
|
||||
// 目前只是比较了文件大小
|
||||
if meta.Length != uploading.LocalFileMeta.Length {
|
||||
@@ -190,7 +190,7 @@ func (ud *UploadingDatabase) clearModTimeChange() {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := os.Stat(uploading.LocalFileMeta.Path)
|
||||
info, err := os.Stat(uploading.LocalFileMeta.Path.RealPath)
|
||||
if err != nil {
|
||||
ud.deleteIndex(i)
|
||||
i--
|
||||
|
||||
@@ -15,6 +15,7 @@ package panupload
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"os"
|
||||
"path"
|
||||
@@ -48,7 +49,6 @@ type (
|
||||
SavePath string // 保存路径
|
||||
DriveId string // 网盘ID,例如:文件网盘,相册网盘
|
||||
FolderCreateMutex *sync.Mutex
|
||||
FolderSyncDb SyncDb //文件备份状态数据库
|
||||
|
||||
PanClient *aliyunpan.PanClient
|
||||
UploadingDatabase *UploadingDatabase // 数据库
|
||||
@@ -217,8 +217,12 @@ func (utu *UploadTaskUnit) upload() (result *taskframework.TaskUnitRunResult) {
|
||||
}
|
||||
return
|
||||
})
|
||||
muer.Execute()
|
||||
|
||||
er := muer.Execute()
|
||||
if er != nil {
|
||||
result.ResultMessage = StrUploadFailed
|
||||
result.NeedRetry = true
|
||||
result.Err = er
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -233,41 +237,46 @@ func (utu *UploadTaskUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunResul
|
||||
}
|
||||
|
||||
func (utu *UploadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
//文件上传成功
|
||||
if utu.FolderSyncDb == nil || lastRunResult == ResultLocalFileNotUpdated { //不需要更新数据库
|
||||
return
|
||||
}
|
||||
ufm := &UploadedFileMeta{
|
||||
IsFolder: false,
|
||||
SHA1: utu.LocalFileChecksum.SHA1,
|
||||
ModTime: utu.LocalFileChecksum.ModTime,
|
||||
Size: utu.LocalFileChecksum.Length,
|
||||
}
|
||||
|
||||
if utu.LocalFileChecksum.UploadOpEntity != nil {
|
||||
ufm.FileId = utu.LocalFileChecksum.UploadOpEntity.FileId
|
||||
ufm.ParentId = utu.LocalFileChecksum.UploadOpEntity.ParentFileId
|
||||
} else {
|
||||
efi, _ := utu.PanClient.FileInfoByPath(utu.DriveId, utu.SavePath)
|
||||
if efi != nil {
|
||||
ufm.FileId = efi.FileId
|
||||
ufm.ParentId = efi.ParentFileId
|
||||
}
|
||||
}
|
||||
utu.FolderSyncDb.Put(utu.SavePath, ufm)
|
||||
// 执行插件
|
||||
utu.pluginCallback("success")
|
||||
}
|
||||
|
||||
func (utu *UploadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
// 失败
|
||||
utu.pluginCallback("fail")
|
||||
}
|
||||
|
||||
var ResultLocalFileNotUpdated = &taskframework.TaskUnitRunResult{ResultCode: 1, Succeed: true, ResultMessage: "本地文件未更新,无需上传!"}
|
||||
var ResultUpdateLocalDatabase = &taskframework.TaskUnitRunResult{ResultCode: 2, Succeed: true, ResultMessage: "本地文件和云端文件MD5一致,无需上传!"}
|
||||
func (utu *UploadTaskUnit) pluginCallback(result string) {
|
||||
if utu.LocalFileChecksum == nil {
|
||||
return
|
||||
}
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
_, fileName := filepath.Split(utu.LocalFileChecksum.Path.LogicPath)
|
||||
pluginParam := &plugins.UploadFileFinishParams{
|
||||
LocalFilePath: utu.LocalFileChecksum.Path.LogicPath,
|
||||
LocalFileName: fileName,
|
||||
LocalFileSize: utu.LocalFileChecksum.LocalFileMeta.Length,
|
||||
LocalFileType: "file",
|
||||
LocalFileUpdatedAt: time.Unix(utu.LocalFileChecksum.LocalFileMeta.ModTime, 0).Format("2006-01-02 15:04:05"),
|
||||
LocalFileSha1: utu.LocalFileChecksum.LocalFileMeta.SHA1,
|
||||
UploadResult: result,
|
||||
DriveId: utu.DriveId,
|
||||
DriveFilePath: utu.panDir + "/" + utu.panFile,
|
||||
}
|
||||
if er := plugin.UploadFileFinishCallback(plugins.GetContext(config.Config.ActiveUser()), pluginParam); er != nil {
|
||||
logger.Verboseln("插件UploadFileFinishCallback调用失败: {}", er)
|
||||
} else {
|
||||
logger.Verboseln("插件UploadFileFinishCallback调用成功")
|
||||
}
|
||||
}
|
||||
|
||||
func (utu *UploadTaskUnit) OnComplete(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
// 任务结束,可能成功也可能失败
|
||||
}
|
||||
func (utu *UploadTaskUnit) OnCancel(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
|
||||
}
|
||||
|
||||
func (utu *UploadTaskUnit) RetryWait() time.Duration {
|
||||
return functions.RetryWait(utu.taskInfo.Retry())
|
||||
}
|
||||
@@ -283,7 +292,7 @@ func (utu *UploadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
timeStart := time.Now()
|
||||
result = &taskframework.TaskUnitRunResult{}
|
||||
|
||||
fmt.Printf("[%s] %s 准备上传: %s => %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path, utu.SavePath)
|
||||
fmt.Printf("[%s] %s 准备上传: %s => %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path.LogicPath, utu.SavePath)
|
||||
|
||||
defer func() {
|
||||
var msg string
|
||||
@@ -307,14 +316,10 @@ func (utu *UploadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
var contentHashName string
|
||||
var checkNameMode string
|
||||
var saveFilePath string
|
||||
var testFileMeta = &UploadedFileMeta{}
|
||||
var uploadOpEntity *aliyunpan.CreateFileUploadResult
|
||||
var proofCode = ""
|
||||
var localFileInfo os.FileInfo
|
||||
var localFile *os.File
|
||||
timeStart2 := time.Now()
|
||||
timeStart3 := time.Now()
|
||||
timeStart4 := time.Now()
|
||||
|
||||
switch utu.Step {
|
||||
case StepUploadPrepareUpload:
|
||||
@@ -327,50 +332,32 @@ func (utu *UploadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
|
||||
StepUploadPrepareUpload:
|
||||
// 创建上传任务
|
||||
if utu.FolderSyncDb != nil {
|
||||
//启用了备份功能,强制使用覆盖同名文件功能
|
||||
utu.IsOverwrite = true
|
||||
testFileMeta = utu.FolderSyncDb.Get(utu.SavePath)
|
||||
}
|
||||
|
||||
// 创建云盘文件夹
|
||||
timeStart2 = time.Now()
|
||||
// utu.FolderCreateMutex.Lock()
|
||||
saveFilePath = path.Dir(utu.SavePath)
|
||||
if saveFilePath != "/" {
|
||||
fmt.Printf("[%s] %s 正在检测和创建云盘文件夹: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), saveFilePath)
|
||||
//同步功能先尝试从数据库获取
|
||||
if utu.FolderSyncDb != nil {
|
||||
timeStart3 = time.Now()
|
||||
utu.FolderCreateMutex.Lock()
|
||||
if test := utu.FolderSyncDb.Get(saveFilePath); test.FileId != "" && test.IsFolder {
|
||||
rs = &aliyunpan.MkdirResult{FileId: test.FileId}
|
||||
}
|
||||
utu.FolderCreateMutex.Unlock()
|
||||
logger.Verbosef("[%s] %s 检测和创建云盘文件夹完毕[from db], 耗时 %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utils.ConvertTime(time.Now().Sub(timeStart3)))
|
||||
}
|
||||
if rs == nil {
|
||||
timeStart4 = time.Now()
|
||||
fe, apierr1 := utu.PanClient.FileInfoByPath(utu.DriveId, saveFilePath)
|
||||
time.Sleep(1 * time.Second)
|
||||
if apierr1 != nil && apierr1.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
logger.Verbosef("[%s] %s 创建云盘文件夹: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), saveFilePath)
|
||||
utu.FolderCreateMutex.Lock()
|
||||
// rs, apierr = utu.PanClient.MkdirRecursive(utu.DriveId, "", "", 0, strings.Split(path.Clean(saveFilePath), "/"))
|
||||
// 可以直接创建的,不用循环创建
|
||||
rs, apierr = utu.PanClient.Mkdir(utu.DriveId, "root", saveFilePath)
|
||||
utu.FolderCreateMutex.Unlock()
|
||||
if apierr != nil || rs.FileId == "" {
|
||||
result.Err = apierr
|
||||
result.ResultMessage = "创建云盘文件夹失败"
|
||||
return
|
||||
}
|
||||
logger.Verbosef("[%s] %s 创建云盘文件夹, 耗时 %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utils.ConvertTime(time.Now().Sub(timeStart4)))
|
||||
logger.Verbosef("[%s] %s 创建云盘文件夹成功\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
} else {
|
||||
rs = &aliyunpan.MkdirResult{}
|
||||
rs.FileId = fe.FileId
|
||||
}
|
||||
} else {
|
||||
rs = &aliyunpan.MkdirResult{}
|
||||
rs.FileId = ""
|
||||
}
|
||||
// time.Sleep(time.Duration(2) * time.Second)
|
||||
// utu.FolderCreateMutex.Unlock()
|
||||
logger.Verbosef("[%s] %s 检测和创建云盘文件夹完毕, 耗时 %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utils.ConvertTime(time.Now().Sub(timeStart2)))
|
||||
time.Sleep(time.Duration(2) * time.Second)
|
||||
|
||||
sha1Str = ""
|
||||
proofCode = ""
|
||||
@@ -378,18 +365,15 @@ StepUploadPrepareUpload:
|
||||
checkNameMode = "auto_rename"
|
||||
if !utu.NoRapidUpload {
|
||||
// 计算文件SHA1
|
||||
fmt.Printf("[%s] %s 正在计算文件SHA1: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path)
|
||||
fmt.Printf("[%s] %s 正在计算文件SHA1: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path.LogicPath)
|
||||
utu.LocalFileChecksum.Sum(localfile.CHECKSUM_SHA1)
|
||||
if testFileMeta.SHA1 == utu.LocalFileChecksum.SHA1 {
|
||||
return ResultUpdateLocalDatabase
|
||||
}
|
||||
sha1Str = utu.LocalFileChecksum.SHA1
|
||||
if utu.LocalFileChecksum.Length == 0 {
|
||||
sha1Str = aliyunpan.DefaultZeroSizeFileContentHash
|
||||
}
|
||||
|
||||
// proof code
|
||||
localFile, _ = os.Open(utu.LocalFileChecksum.Path)
|
||||
localFile, _ = os.Open(utu.LocalFileChecksum.Path.RealPath)
|
||||
localFileInfo, _ = localFile.Stat()
|
||||
proofCode = aliyunpan.CalcProofCode(utu.PanClient.GetAccessToken(), rio.NewFileReaderAtLen64(localFile), localFileInfo.Size())
|
||||
localFile.Close()
|
||||
@@ -432,7 +416,7 @@ StepUploadPrepareUpload:
|
||||
// 创建上传任务
|
||||
appCreateUploadFileParam = &aliyunpan.CreateFileUploadParam{
|
||||
DriveId: utu.DriveId,
|
||||
Name: filepath.Base(utu.LocalFileChecksum.Path),
|
||||
Name: filepath.Base(utu.SavePath),
|
||||
Size: utu.LocalFileChecksum.Length,
|
||||
ContentHash: sha1Str,
|
||||
ContentHashName: contentHashName,
|
||||
@@ -466,6 +450,15 @@ stepUploadRapidUpload:
|
||||
stepUploadUpload:
|
||||
// 正常上传流程
|
||||
uploadResult := utu.upload()
|
||||
|
||||
if uploadResult != nil && uploadResult.Err != nil {
|
||||
if uploadResult.Err == uploadPartNotSeq {
|
||||
fmt.Printf("[%s] %s 文件分片上传顺序错误,开始重新上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
// 需要重新从0开始上传
|
||||
uploadResult = nil
|
||||
utu.LocalFileChecksum.UploadOpEntity = nil
|
||||
utu.state = nil
|
||||
goto StepUploadPrepareUpload
|
||||
}
|
||||
}
|
||||
return uploadResult
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ import (
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -45,18 +47,34 @@ func getBlockSize(fileSize int64) int64 {
|
||||
return MinUploadBlockSize
|
||||
}
|
||||
|
||||
|
||||
// isUrlExpired 上传链接是否已过期。过期返回True
|
||||
func isUrlExpired(urlStr string) bool {
|
||||
// IsUrlExpired 上传链接是否已过期。过期返回True
|
||||
func IsUrlExpired(urlStr string) bool {
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
expiredTimeSecStr := u.Query().Get("x-oss-expires")
|
||||
expiredTimeSec,_ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
|
||||
expiredTimeSec, _ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
|
||||
if (time.Now().Unix() - 10) >= expiredTimeSec {
|
||||
// expired
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func IsVideoFile(fileName string) bool {
|
||||
if fileName == "" {
|
||||
return false
|
||||
}
|
||||
extName := strings.ToLower(path.Ext(fileName))
|
||||
if strings.Index(extName, ".") == 0 {
|
||||
extName = strings.TrimPrefix(extName, ".")
|
||||
}
|
||||
extList := config.Config.GetVideoExtensionList()
|
||||
for _, ext := range extList {
|
||||
if ext == extName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -43,11 +43,11 @@ func (lfm *LocalFileMeta) EqualLengthSHA1(m *LocalFileMeta) bool {
|
||||
|
||||
// CompleteAbsPath 补齐绝对路径
|
||||
func (lfm *LocalFileMeta) CompleteAbsPath() {
|
||||
if filepath.IsAbs(lfm.Path) {
|
||||
if filepath.IsAbs(lfm.Path.LogicPath) {
|
||||
return
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(lfm.Path)
|
||||
absPath, err := filepath.Abs(lfm.Path.LogicPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -55,7 +55,7 @@ func (lfm *LocalFileMeta) CompleteAbsPath() {
|
||||
if os.PathSeparator == '\\' {
|
||||
absPath = strings.ReplaceAll(absPath, "\\", "/")
|
||||
}
|
||||
lfm.Path = absPath
|
||||
lfm.Path.LogicPath = absPath
|
||||
}
|
||||
|
||||
// GetFileSum 获取文件的大小, md5, crc32
|
||||
|
||||
@@ -46,12 +46,12 @@ const (
|
||||
type (
|
||||
// LocalFileMeta 本地文件元信息
|
||||
LocalFileMeta struct {
|
||||
Path string `json:"path,omitempty"` // 本地路径
|
||||
Length int64 `json:"length,omitempty"` // 文件大小
|
||||
MD5 string `json:"md5,omitempty"` // 文件的 md5
|
||||
CRC32 uint32 `json:"crc32,omitempty"` // 文件的 crc32
|
||||
SHA1 string `json:"sha1,omitempty"` // 文件的 sha1
|
||||
ModTime int64 `json:"modtime"` // 修改日期
|
||||
Path SymlinkFile `json:"path,omitempty"` // 本地路径
|
||||
Length int64 `json:"length,omitempty"` // 文件大小
|
||||
MD5 string `json:"md5,omitempty"` // 文件的 md5
|
||||
CRC32 uint32 `json:"crc32,omitempty"` // 文件的 crc32
|
||||
SHA1 string `json:"sha1,omitempty"` // 文件的 sha1
|
||||
ModTime int64 `json:"modtime"` // 修改日期
|
||||
|
||||
// 网盘上传参数
|
||||
UploadOpEntity *aliyunpan.CreateFileUploadResult `json:"uploadOpEntity"`
|
||||
@@ -69,14 +69,18 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func NewLocalFileEntity(localPath string) *LocalFileEntity {
|
||||
return NewLocalFileEntityWithBufSize(localPath, DefaultBufSize)
|
||||
func NewLocalSymlinkFileEntity(file SymlinkFile) *LocalFileEntity {
|
||||
return NewLocalFileEntityWithBufSize(file, DefaultBufSize)
|
||||
}
|
||||
|
||||
func NewLocalFileEntityWithBufSize(localPath string, bufSize int) *LocalFileEntity {
|
||||
func NewLocalFileEntity(localPath string) *LocalFileEntity {
|
||||
return NewLocalFileEntityWithBufSize(NewSymlinkFile(localPath), DefaultBufSize)
|
||||
}
|
||||
|
||||
func NewLocalFileEntityWithBufSize(file SymlinkFile, bufSize int) *LocalFileEntity {
|
||||
return &LocalFileEntity{
|
||||
LocalFileMeta: LocalFileMeta{
|
||||
Path: localPath,
|
||||
Path: file,
|
||||
},
|
||||
bufSize: bufSize,
|
||||
}
|
||||
@@ -89,7 +93,7 @@ func (lfc *LocalFileEntity) OpenPath() error {
|
||||
}
|
||||
|
||||
var err error
|
||||
lfc.file, err = os.Open(lfc.Path)
|
||||
lfc.file, err = os.Open(lfc.Path.RealPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
package localfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SymlinkFile 软链接文件,Linux/macOS的ln,Windows的mklink命令创建的文件链接。对于非软链接文件而言,真实的路径和逻辑路径是一样的。
|
||||
type SymlinkFile struct {
|
||||
// LogicPath 逻辑路径
|
||||
LogicPath string `json:"logicPath"`
|
||||
// RealPath 真正的文件路径,即文件的本体
|
||||
RealPath string `json:"realPath"`
|
||||
}
|
||||
|
||||
func (s *SymlinkFile) String() string {
|
||||
sb := &strings.Builder{}
|
||||
fmt.Fprintf(sb, "{\"logicPath\":%s, \"realPath\": %s}", s.LogicPath, s.RealPath)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func NewSymlinkFile(filePath string) SymlinkFile {
|
||||
p := CleanPath(filePath)
|
||||
return SymlinkFile{
|
||||
LogicPath: p,
|
||||
RealPath: p,
|
||||
}
|
||||
}
|
||||
|
||||
type MyWalkFunc func(path SymlinkFile, info fs.FileInfo, err error) error
|
||||
|
||||
// CleanPath 规范化文件路径,分隔符全部转换成Unix文件分隔符"/"。同时清除后缀无用的"/"路径
|
||||
func CleanPath(p string) string {
|
||||
if p == "" || p == "/" {
|
||||
return p
|
||||
}
|
||||
|
||||
p = path.Clean(strings.ReplaceAll(p, "\\", "/"))
|
||||
if p == "." {
|
||||
p = ""
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// GetSuffixPath 获取相对路径。即fullFilePath相对rootFilePath的相对路径
|
||||
func GetSuffixPath(fullFilePath, rootFilePath string) string {
|
||||
rootFilePath = CleanPath(rootFilePath)
|
||||
fullFilePath = CleanPath(fullFilePath)
|
||||
return strings.TrimPrefix(strings.TrimPrefix(fullFilePath, rootFilePath), "/")
|
||||
}
|
||||
|
||||
// RetrieveRealPath 递归调用找到软链接文件的真实文件对应的路径信息
|
||||
func RetrieveRealPath(file SymlinkFile) (SymlinkFile, os.FileInfo, error) {
|
||||
info, err := os.Lstat(file.RealPath)
|
||||
if err == nil {
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
// 软链接文件
|
||||
if f, e := os.Readlink(file.RealPath); e == nil {
|
||||
file.RealPath = strings.ReplaceAll(f, "\\", "/")
|
||||
return RetrieveRealPath(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
return file, info, err
|
||||
}
|
||||
|
||||
// RetrieveRealPathFromLogicPath 遍历路径获取逻辑路径真正的文件路径。如果逻辑路径不完全存在,则返回已经存在的那部分路径
|
||||
//
|
||||
// logicFilePath - 目标逻辑路径
|
||||
// 由于目标逻辑路径期间有可能会经过多次符号逻辑文件,同时有部分逻辑路径可能是不存在的,所以需要按照逻辑文件起始开始进行遍历,直至完成逻辑文件路径的所有遍历,
|
||||
// 或者直到不存在的逻辑文件部分,然后返回。
|
||||
//
|
||||
// 例如逻辑文件路径:/Volumes/Downloads/dev/sync_drive_config.json。
|
||||
//
|
||||
// 如果/Volumes/Downloads存在而后面部分不存在,则最终结果返回/Volumes/Downloads对应的文件信息,同时返回error
|
||||
func RetrieveRealPathFromLogicPath(logicFilePath string) (SymlinkFile, os.FileInfo, error) {
|
||||
logicFilePath = CleanPath(logicFilePath)
|
||||
if logicFilePath == "/" {
|
||||
return RetrieveRealPath(NewSymlinkFile(logicFilePath))
|
||||
}
|
||||
pathParts := strings.Split(logicFilePath, "/")
|
||||
exitedSymlinkFile := NewSymlinkFile("")
|
||||
var exitedFileInfo os.FileInfo
|
||||
|
||||
sf := NewSymlinkFile("")
|
||||
var fi os.FileInfo
|
||||
var err error
|
||||
for _, p := range pathParts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(p, ":") {
|
||||
// windows volume label, e.g: C:/ D:/
|
||||
sf.LogicPath += p
|
||||
sf.RealPath += p
|
||||
exitedSymlinkFile = sf
|
||||
continue
|
||||
}
|
||||
sf.LogicPath += "/" + p
|
||||
sf.RealPath += "/" + p
|
||||
sf, fi, err = RetrieveRealPath(sf)
|
||||
if err != nil {
|
||||
// may be permission deny or not existed
|
||||
return exitedSymlinkFile, exitedFileInfo, err
|
||||
}
|
||||
exitedSymlinkFile = sf
|
||||
exitedFileInfo = fi
|
||||
}
|
||||
return exitedSymlinkFile, exitedFileInfo, nil
|
||||
}
|
||||
|
||||
// RetrieveRealPathFromLogicSuffixPath 通过指定根目录和后缀逻辑路径,获取文件对应的真实路径信息
|
||||
//
|
||||
// 整体功能和 RetrieveRealPathFromLogicPath 一致,将逻辑路径分成根路径和后缀路径两部分,并且根路径必须存在。
|
||||
func RetrieveRealPathFromLogicSuffixPath(rootFile SymlinkFile, logicSuffixPath string) (SymlinkFile, os.FileInfo, error) {
|
||||
if rootFile.LogicPath == "" || rootFile.RealPath == "" {
|
||||
return rootFile, nil, fmt.Errorf("root file path error")
|
||||
}
|
||||
rootFile.LogicPath = CleanPath(rootFile.LogicPath)
|
||||
rootFile.RealPath = CleanPath(rootFile.RealPath)
|
||||
rootFileInfo, e := os.Lstat(rootFile.RealPath)
|
||||
if e != nil {
|
||||
return rootFile, nil, e
|
||||
}
|
||||
|
||||
logicSuffixPath = CleanPath(logicSuffixPath)
|
||||
pathParts := strings.Split(logicSuffixPath, "/")
|
||||
exitedSymlinkFile := rootFile
|
||||
exitedFileInfo := rootFileInfo
|
||||
|
||||
sf := rootFile
|
||||
var fi os.FileInfo
|
||||
var err error
|
||||
for _, p := range pathParts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
sf.LogicPath += "/" + p
|
||||
sf.RealPath += "/" + p
|
||||
sf, fi, err = RetrieveRealPath(sf)
|
||||
if err != nil {
|
||||
// may be permission deny or not existed
|
||||
return exitedSymlinkFile, exitedFileInfo, err
|
||||
}
|
||||
exitedSymlinkFile = sf
|
||||
exitedFileInfo = fi
|
||||
}
|
||||
return exitedSymlinkFile, exitedFileInfo, nil
|
||||
}
|
||||
|
||||
// WalkAllFile 遍历本地文件,支持软链接(符号逻辑)文件(Linux & Windows & macOS)
|
||||
func WalkAllFile(file SymlinkFile, walkFn MyWalkFunc) error {
|
||||
file.LogicPath = path.Clean(strings.ReplaceAll(file.LogicPath, "\\", "/"))
|
||||
file.RealPath = path.Clean(strings.ReplaceAll(file.RealPath, "\\", "/"))
|
||||
|
||||
file, info, err := RetrieveRealPath(file)
|
||||
if err != nil {
|
||||
err = walkFn(file, nil, err)
|
||||
} else {
|
||||
err = walkAllFile(file, info, walkFn)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func walkAllFile(file SymlinkFile, info os.FileInfo, walkFn MyWalkFunc) error {
|
||||
if !info.IsDir() {
|
||||
return walkFn(file, info, nil)
|
||||
}
|
||||
|
||||
files, err1 := ioutil.ReadDir(file.RealPath)
|
||||
if err1 != nil {
|
||||
return walkFn(file, nil, err1)
|
||||
}
|
||||
for _, fi := range files {
|
||||
subFile := SymlinkFile{
|
||||
LogicPath: path.Join(path.Clean(file.LogicPath), "/", fi.Name()),
|
||||
RealPath: path.Join(path.Clean(file.RealPath), "/", fi.Name()),
|
||||
}
|
||||
subFile, fi, err1 = RetrieveRealPath(subFile)
|
||||
err := walkFn(subFile, fi, err1)
|
||||
if err != nil && err != filepath.SkipDir {
|
||||
return err
|
||||
}
|
||||
if fi == nil {
|
||||
continue
|
||||
}
|
||||
if fi.IsDir() {
|
||||
if err == filepath.SkipDir {
|
||||
continue
|
||||
}
|
||||
err = walkAllFile(subFile, fi, walkFn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package localfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMyWalkFile(t *testing.T) {
|
||||
count := 0
|
||||
walkFunc := func(file SymlinkFile, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
count += 1
|
||||
fmt.Println("file: ", utils.ObjectToJsonStr(file, false))
|
||||
//fmt.Println("file: ", file)
|
||||
return nil
|
||||
}
|
||||
|
||||
//curPath := "D:\\smb\\feny\\goprojects\\dev\\lks"
|
||||
curPath := "/Volumes/Downloads/dev/lks"
|
||||
file := NewSymlinkFile(curPath)
|
||||
if err := WalkAllFile(file, walkFunc); err != nil {
|
||||
if err != filepath.SkipDir {
|
||||
fmt.Printf("警告: 遍历错误: %s\n", err)
|
||||
}
|
||||
}
|
||||
fmt.Println("count: ", count)
|
||||
}
|
||||
|
||||
func TestRetrieveRealPath(t *testing.T) {
|
||||
curPath := "/Volumes/Downloads/dev/lks/test"
|
||||
file := NewSymlinkFile(curPath)
|
||||
sf, _, e := RetrieveRealPath(file)
|
||||
if e != nil {
|
||||
fmt.Println(e)
|
||||
}
|
||||
fmt.Println(sf)
|
||||
}
|
||||
|
||||
func TestRetrieveRealPathFromLogicPath(t *testing.T) {
|
||||
curPath := "/Volumes/Downloads/dev/lks/test/未命名文件夹cmd/sync_drive_config.json"
|
||||
sf, _, e := RetrieveRealPathFromLogicPath(curPath)
|
||||
if e != nil {
|
||||
fmt.Println(e)
|
||||
}
|
||||
fmt.Println(sf)
|
||||
}
|
||||
|
||||
func TestRetrieveRealPathFromLogicSuffixPath(t *testing.T) {
|
||||
rootPath := NewSymlinkFile("/Volumes/Downloads/dev/测试同步盘/new_lks")
|
||||
rootPath, _, _ = RetrieveRealPath(rootPath)
|
||||
suffixPath := "test/未命名文件夹cmd/sync_drive_config.json"
|
||||
sf, _, e := RetrieveRealPathFromLogicSuffixPath(rootPath, suffixPath)
|
||||
if e != nil {
|
||||
fmt.Println(e)
|
||||
}
|
||||
fmt.Println(sf)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package plugins
|
||||
|
||||
type (
|
||||
IdlePlugin struct {
|
||||
Name string
|
||||
}
|
||||
)
|
||||
|
||||
func NewIdlePlugin() *IdlePlugin {
|
||||
return &IdlePlugin{
|
||||
Name: "IdlePlugin",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *IdlePlugin) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *IdlePlugin) UploadFilePrepareCallback(context *Context, params *UploadFilePrepareParams) (*UploadFilePrepareResult, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *IdlePlugin) UploadFileFinishCallback(context *Context, params *UploadFileFinishParams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *IdlePlugin) DownloadFilePrepareCallback(context *Context, params *DownloadFilePrepareParams) (*DownloadFilePrepareResult, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *IdlePlugin) DownloadFileFinishCallback(context *Context, params *DownloadFileFinishParams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *IdlePlugin) SyncScanLocalFilePrepareCallback(context *Context, params *SyncScanLocalFilePrepareParams) (*SyncScanLocalFilePrepareResult, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *IdlePlugin) SyncScanPanFilePrepareCallback(context *Context, params *SyncScanPanFilePrepareParams) (*SyncScanPanFilePrepareResult, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *IdlePlugin) Stop() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
JsPlugin struct {
|
||||
Name string
|
||||
vm *goja.Runtime
|
||||
}
|
||||
)
|
||||
|
||||
func NewJsPlugin() *JsPlugin {
|
||||
return &JsPlugin{
|
||||
Name: "JsPlugin",
|
||||
vm: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// jsLog 支持js中的console.log方法
|
||||
func jsLog(call goja.FunctionCall) goja.Value {
|
||||
str := call.Argument(0)
|
||||
buf := &strings.Builder{}
|
||||
fmt.Fprintf(buf, "JAVASCRIPT: %+v", str.Export())
|
||||
logger.Verboseln(buf.String())
|
||||
return str
|
||||
}
|
||||
|
||||
func (js *JsPlugin) Start() error {
|
||||
js.Name = "JsPlugin"
|
||||
js.vm = goja.New()
|
||||
js.vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
|
||||
|
||||
// 内置log
|
||||
console := js.vm.NewObject()
|
||||
console.Set("log", jsLog)
|
||||
js.vm.Set("console", console) // console.log()
|
||||
|
||||
// 内置系统函数PluginUtil
|
||||
pluginObj := js.vm.NewObject()
|
||||
js.vm.Set("PluginUtil", pluginObj)
|
||||
|
||||
// PluginUtil.Http
|
||||
httpObj := js.vm.NewObject()
|
||||
pluginObj.Set("Http", httpObj)
|
||||
httpObj.Set("get", HttpGet) // PluginUtil.Http.get()
|
||||
httpObj.Set("post", HttpPost) // PluginUtil.Http.post()
|
||||
|
||||
// PluginUtil.LocalFS
|
||||
localFS := js.vm.NewObject()
|
||||
pluginObj.Set("LocalFS", localFS)
|
||||
localFS.Set("deleteFile", DeleteLocalFile) // PluginUtil.LocalFS.deleteFile()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadScript 加载脚本
|
||||
func (js *JsPlugin) LoadScript(script string) error {
|
||||
_, err := js.vm.RunString(script)
|
||||
if err != nil {
|
||||
logger.Verboseln("JS代码有问题!{}", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (js *JsPlugin) isHandlerFuncExisted(fnName string) bool {
|
||||
ret := js.vm.Get(fnName)
|
||||
if ret != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UploadFilePrepareCallback 上传文件前的回调函数
|
||||
func (js *JsPlugin) UploadFilePrepareCallback(context *Context, params *UploadFilePrepareParams) (*UploadFilePrepareResult, error) {
|
||||
var fn func(*Context, *UploadFilePrepareParams) (*UploadFilePrepareResult, error)
|
||||
if !js.isHandlerFuncExisted("uploadFilePrepareCallback") {
|
||||
return nil, nil
|
||||
}
|
||||
err := js.vm.ExportTo(js.vm.Get("uploadFilePrepareCallback"), &fn)
|
||||
if err != nil {
|
||||
logger.Verboseln("Js函数映射到 Go 函数失败!")
|
||||
return nil, nil
|
||||
}
|
||||
r, er := fn(context, params)
|
||||
if er != nil {
|
||||
logger.Verboseln(er)
|
||||
return nil, er
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// UploadFileFinishCallback 上传文件结束的回调函数
|
||||
func (js *JsPlugin) UploadFileFinishCallback(context *Context, params *UploadFileFinishParams) error {
|
||||
var fn func(*Context, *UploadFileFinishParams) error
|
||||
if !js.isHandlerFuncExisted("uploadFileFinishCallback") {
|
||||
return nil
|
||||
}
|
||||
err := js.vm.ExportTo(js.vm.Get("uploadFileFinishCallback"), &fn)
|
||||
if err != nil {
|
||||
logger.Verboseln("Js函数映射到 Go 函数失败!")
|
||||
return nil
|
||||
}
|
||||
er := fn(context, params)
|
||||
if er != nil {
|
||||
logger.Verboseln(er)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloadFilePrepareCallback 下载文件前的回调函数
|
||||
func (js *JsPlugin) DownloadFilePrepareCallback(context *Context, params *DownloadFilePrepareParams) (*DownloadFilePrepareResult, error) {
|
||||
var fn func(*Context, *DownloadFilePrepareParams) (*DownloadFilePrepareResult, error)
|
||||
if !js.isHandlerFuncExisted("downloadFilePrepareCallback") {
|
||||
return nil, nil
|
||||
}
|
||||
err := js.vm.ExportTo(js.vm.Get("downloadFilePrepareCallback"), &fn)
|
||||
if err != nil {
|
||||
logger.Verboseln("Js函数映射到 Go 函数失败!")
|
||||
return nil, nil
|
||||
}
|
||||
r, er := fn(context, params)
|
||||
if er != nil {
|
||||
logger.Verboseln(er)
|
||||
return nil, er
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// DownloadFileFinishCallback 下载文件结束的回调函数
|
||||
func (js *JsPlugin) DownloadFileFinishCallback(context *Context, params *DownloadFileFinishParams) error {
|
||||
var fn func(*Context, *DownloadFileFinishParams) error
|
||||
if !js.isHandlerFuncExisted("downloadFileFinishCallback") {
|
||||
return nil
|
||||
}
|
||||
err := js.vm.ExportTo(js.vm.Get("downloadFileFinishCallback"), &fn)
|
||||
if err != nil {
|
||||
logger.Verboseln("Js函数映射到 Go 函数失败!")
|
||||
return nil
|
||||
}
|
||||
er := fn(context, params)
|
||||
if er != nil {
|
||||
logger.Verboseln(er)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncScanLocalFilePrepareCallback 同步备份-扫描本地文件的回调函数
|
||||
func (js *JsPlugin) SyncScanLocalFilePrepareCallback(context *Context, params *SyncScanLocalFilePrepareParams) (*SyncScanLocalFilePrepareResult, error) {
|
||||
var fn func(*Context, *SyncScanLocalFilePrepareParams) (*SyncScanLocalFilePrepareResult, error)
|
||||
if !js.isHandlerFuncExisted("syncScanLocalFilePrepareCallback") {
|
||||
return nil, nil
|
||||
}
|
||||
err := js.vm.ExportTo(js.vm.Get("syncScanLocalFilePrepareCallback"), &fn)
|
||||
if err != nil {
|
||||
logger.Verboseln("Js函数映射到 Go 函数失败!")
|
||||
return nil, nil
|
||||
}
|
||||
r, er := fn(context, params)
|
||||
if er != nil {
|
||||
logger.Verboseln(er)
|
||||
return nil, er
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// SyncScanPanFilePrepareCallback 同步备份-扫描本地文件的回调函数
|
||||
func (js *JsPlugin) SyncScanPanFilePrepareCallback(context *Context, params *SyncScanPanFilePrepareParams) (*SyncScanPanFilePrepareResult, error) {
|
||||
var fn func(*Context, *SyncScanPanFilePrepareParams) (*SyncScanPanFilePrepareResult, error)
|
||||
if !js.isHandlerFuncExisted("syncScanPanFilePrepareCallback") {
|
||||
return nil, nil
|
||||
}
|
||||
err := js.vm.ExportTo(js.vm.Get("syncScanPanFilePrepareCallback"), &fn)
|
||||
if err != nil {
|
||||
logger.Verboseln("Js函数映射到 Go 函数失败!")
|
||||
return nil, nil
|
||||
}
|
||||
r, er := fn(context, params)
|
||||
if er != nil {
|
||||
logger.Verboseln(er)
|
||||
return nil, er
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (js *JsPlugin) Stop() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package plugins
|
||||
|
||||
type (
|
||||
// Context 插件回调函数上下文信息
|
||||
Context struct {
|
||||
AppName string `json:"appName"`
|
||||
Version string `json:"version"`
|
||||
UserId string `json:"userId"`
|
||||
Nickname string `json:"nickname"`
|
||||
FileDriveId string `json:"fileDriveId"`
|
||||
AlbumDriveId string `json:"albumDriveId"`
|
||||
}
|
||||
|
||||
// UploadFilePrepareParams 上传文件前的回调函数-参数
|
||||
UploadFilePrepareParams struct {
|
||||
LocalFilePath string `json:"localFilePath"`
|
||||
LocalFileName string `json:"localFileName"`
|
||||
LocalFileSize int64 `json:"localFileSize"`
|
||||
LocalFileType string `json:"localFileType"`
|
||||
LocalFileUpdatedAt string `json:"localFileUpdatedAt"`
|
||||
DriveId string `json:"driveId"`
|
||||
DriveFilePath string `json:"driveFilePath"`
|
||||
}
|
||||
|
||||
// UploadFilePrepareResult 上传文件前的回调函数-返回结果
|
||||
UploadFilePrepareResult struct {
|
||||
// UploadApproved 确认该文件是否上传。yes-上传 no-不上传
|
||||
UploadApproved string `json:"uploadApproved"`
|
||||
// DriveFilePath 保存网盘的修改后的路径。注意该路径是相对路径
|
||||
DriveFilePath string `json:"driveFilePath"`
|
||||
}
|
||||
|
||||
// UploadFileFinishParams 上传文件结束的回调函数-参数
|
||||
UploadFileFinishParams struct {
|
||||
LocalFilePath string `json:"localFilePath"`
|
||||
LocalFileName string `json:"localFileName"`
|
||||
LocalFileSize int64 `json:"localFileSize"`
|
||||
LocalFileType string `json:"localFileType"`
|
||||
LocalFileUpdatedAt string `json:"localFileUpdatedAt"`
|
||||
LocalFileSha1 string `json:"localFileSha1"`
|
||||
UploadResult string `json:"uploadResult"`
|
||||
DriveId string `json:"driveId"`
|
||||
DriveFilePath string `json:"driveFilePath"`
|
||||
}
|
||||
|
||||
// DownloadFilePrepareParams 下载文件前的回调函数-参数
|
||||
DownloadFilePrepareParams struct {
|
||||
DriveId string `json:"driveId"`
|
||||
DriveFileName string `json:"driveFileName"`
|
||||
DriveFilePath string `json:"driveFilePath"`
|
||||
DriveFileSha1 string `json:"driveFileSha1"`
|
||||
DriveFileSize int64 `json:"driveFileSize"`
|
||||
DriveFileType string `json:"driveFileType"`
|
||||
DriveFileUpdatedAt string `json:"driveFileUpdatedAt"`
|
||||
LocalFilePath string `json:"localFilePath"`
|
||||
}
|
||||
|
||||
// DownloadFilePrepareResult 上传文件前的回调函数-返回结果
|
||||
DownloadFilePrepareResult struct {
|
||||
// DownloadApproved 确认该文件是否下载。yes-下载 no-不下载
|
||||
DownloadApproved string `json:"downloadApproved"`
|
||||
// LocalFilePath 保存本地的修改后的路径。注意该路径是相对路径
|
||||
LocalFilePath string `json:"localFilePath"`
|
||||
}
|
||||
|
||||
// DownloadFileFinishParams 下载文件结束的回调函数-参数
|
||||
DownloadFileFinishParams struct {
|
||||
DriveId string `json:"driveId"`
|
||||
DriveFileName string `json:"driveFileName"`
|
||||
DriveFilePath string `json:"driveFilePath"`
|
||||
DriveFileSha1 string `json:"driveFileSha1"`
|
||||
DriveFileSize int64 `json:"driveFileSize"`
|
||||
DriveFileType string `json:"driveFileType"`
|
||||
DriveFileUpdatedAt string `json:"driveFileUpdatedAt"`
|
||||
DownloadResult string `json:"downloadResult"`
|
||||
LocalFilePath string `json:"localFilePath"`
|
||||
}
|
||||
|
||||
// SyncScanLocalFilePrepareParams 同步备份-扫描本地文件前参数
|
||||
SyncScanLocalFilePrepareParams struct {
|
||||
LocalFilePath string `json:"localFilePath"`
|
||||
LocalFileName string `json:"localFileName"`
|
||||
LocalFileSize int64 `json:"localFileSize"`
|
||||
LocalFileType string `json:"localFileType"`
|
||||
LocalFileUpdatedAt string `json:"localFileUpdatedAt"`
|
||||
DriveId string `json:"driveId"`
|
||||
}
|
||||
|
||||
// SyncScanLocalFilePrepareResult 同步备份-扫描本地文件-返回结果
|
||||
SyncScanLocalFilePrepareResult struct {
|
||||
// SyncScanLocalApproved 该文件是否确认扫描,yes-允许扫描,no-禁止扫描
|
||||
SyncScanLocalApproved string `json:"syncScanLocalApproved"`
|
||||
}
|
||||
|
||||
// SyncScanPanFilePrepareParams 同步备份-扫描云盘文件前参数
|
||||
SyncScanPanFilePrepareParams struct {
|
||||
DriveId string `json:"driveId"`
|
||||
DriveFileName string `json:"driveFileName"`
|
||||
DriveFilePath string `json:"driveFilePath"`
|
||||
DriveFileSha1 string `json:"driveFileSha1"`
|
||||
DriveFileSize int64 `json:"driveFileSize"`
|
||||
DriveFileType string `json:"driveFileType"`
|
||||
DriveFileUpdatedAt string `json:"driveFileUpdatedAt"`
|
||||
}
|
||||
|
||||
// SyncScanPanFilePrepareResult 同步备份-扫描云盘文件-返回结果
|
||||
SyncScanPanFilePrepareResult struct {
|
||||
// SyncScanPanApproved 该文件是否确认扫描,yes-允许扫描,no-禁止扫描
|
||||
SyncScanPanApproved string `json:"syncScanPanApproved"`
|
||||
}
|
||||
|
||||
// Plugin 插件接口
|
||||
Plugin interface {
|
||||
// Start 启动
|
||||
Start() error
|
||||
|
||||
// UploadFilePrepareCallback 上传文件前的回调函数
|
||||
UploadFilePrepareCallback(context *Context, params *UploadFilePrepareParams) (*UploadFilePrepareResult, error)
|
||||
|
||||
// UploadFileFinishCallback 上传文件结束的回调函数
|
||||
UploadFileFinishCallback(context *Context, params *UploadFileFinishParams) error
|
||||
|
||||
// DownloadFilePrepareCallback 下载文件前的回调函数
|
||||
DownloadFilePrepareCallback(context *Context, params *DownloadFilePrepareParams) (*DownloadFilePrepareResult, error)
|
||||
|
||||
// DownloadFileFinishCallback 下载文件结束的回调函数
|
||||
DownloadFileFinishCallback(context *Context, params *DownloadFileFinishParams) error
|
||||
|
||||
// SyncScanLocalFilePrepareCallback 同步备份-扫描本地文件的回调函数
|
||||
SyncScanLocalFilePrepareCallback(context *Context, params *SyncScanLocalFilePrepareParams) (*SyncScanLocalFilePrepareResult, error)
|
||||
|
||||
// SyncScanPanFilePrepareCallback 同步备份-扫描本地文件的回调函数
|
||||
SyncScanPanFilePrepareCallback(context *Context, params *SyncScanPanFilePrepareParams) (*SyncScanPanFilePrepareResult, error)
|
||||
|
||||
// Stop 停止
|
||||
Stop() error
|
||||
}
|
||||
)
|
||||
|
||||
var ()
|
||||
@@ -0,0 +1,97 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
PluginManager struct {
|
||||
PluginPath string
|
||||
}
|
||||
)
|
||||
|
||||
func GetContext(user *config.PanUser) *Context {
|
||||
if user == nil {
|
||||
return &Context{
|
||||
AppName: "aliyunpan",
|
||||
Version: config.AppVersion,
|
||||
UserId: "",
|
||||
Nickname: "",
|
||||
FileDriveId: "",
|
||||
AlbumDriveId: "",
|
||||
}
|
||||
}
|
||||
return &Context{
|
||||
AppName: "aliyunpan",
|
||||
Version: config.AppVersion,
|
||||
UserId: user.UserId,
|
||||
Nickname: user.Nickname,
|
||||
FileDriveId: user.DriveList.GetFileDriveId(),
|
||||
AlbumDriveId: user.DriveList.GetFileDriveId(),
|
||||
}
|
||||
}
|
||||
|
||||
func NewPluginManager(pluginDir string) *PluginManager {
|
||||
return &PluginManager{
|
||||
PluginPath: pluginDir,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginManager) SetPluginPath(pluginPath string) error {
|
||||
if fi, err := os.Stat(pluginPath); err == nil && fi.IsDir() {
|
||||
p.PluginPath = filepath.Clean(pluginPath)
|
||||
} else {
|
||||
return fmt.Errorf("path must be a folder")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PluginManager) GetPlugin() (Plugin, error) {
|
||||
// js plugins folder
|
||||
// only support js plugins right now
|
||||
jsPluginPath := path.Clean(p.PluginPath + string(os.PathSeparator) + "js")
|
||||
if fi, err := os.Stat(jsPluginPath); err == nil && fi.IsDir() {
|
||||
jsPlugin := NewJsPlugin()
|
||||
if jsPlugin.Start() != nil {
|
||||
logger.Verbosef("初始化JS脚本错误\n")
|
||||
return interface{}(NewIdlePlugin()).(Plugin), nil
|
||||
}
|
||||
|
||||
jsPluginValid := false
|
||||
if files, e := ioutil.ReadDir(jsPluginPath); e == nil {
|
||||
for _, f := range files {
|
||||
if !f.IsDir() {
|
||||
if strings.HasPrefix(strings.ToLower(f.Name()), ".") || strings.HasPrefix(strings.ToLower(f.Name()), "~") {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(strings.ToLower(f.Name()), ".js") {
|
||||
// this is a js file
|
||||
bytes, re := ioutil.ReadFile(path.Clean(jsPluginPath + string(os.PathSeparator) + f.Name()))
|
||||
if re != nil {
|
||||
logger.Verbosef("读取JS脚本错误: %s\n", re)
|
||||
continue
|
||||
}
|
||||
var script = string(bytes)
|
||||
if jsPlugin.LoadScript(script) == nil {
|
||||
jsPluginValid = true
|
||||
logger.Verbosef("加载JS脚本成功: %s\n", f.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if jsPluginValid {
|
||||
return interface{}(jsPlugin).(Plugin), nil
|
||||
}
|
||||
}
|
||||
|
||||
// default idle plugins
|
||||
return interface{}(NewIdlePlugin()).(Plugin), nil
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPluginUpload(t *testing.T) {
|
||||
pluginManager := NewPluginManager("D:\\smb\\feny\\goprojects\\dev")
|
||||
plugin, err := pluginManager.GetPlugin()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
ctx := &Context{
|
||||
AppName: "aliyunpan",
|
||||
Version: "v0.1.3",
|
||||
UserId: "11001d48564f43b3bc5662874f04bb11",
|
||||
Nickname: "tickstep",
|
||||
FileDriveId: "19519121",
|
||||
AlbumDriveId: "29519122",
|
||||
}
|
||||
params := &UploadFilePrepareParams{
|
||||
LocalFilePath: "D:\\Program Files\\aliyunpan\\Downloads\\11001d48564f43b3bc5662874f04bb11\\token.bat",
|
||||
LocalFileName: "token.bat",
|
||||
LocalFileSize: 125330,
|
||||
LocalFileType: "file",
|
||||
LocalFileUpdatedAt: "2022-04-14 07:05:12",
|
||||
DriveId: "19519221",
|
||||
DriveFilePath: "aliyunpan/Downloads/11001d48564f43b3bc5662874f04bb11/token.bat",
|
||||
}
|
||||
b, _ := jsoniter.Marshal(ctx)
|
||||
fmt.Println(string(b))
|
||||
b, _ = jsoniter.Marshal(params)
|
||||
fmt.Println(string(b))
|
||||
r, e := plugin.UploadFilePrepareCallback(ctx, params)
|
||||
if e != nil {
|
||||
fmt.Println(e)
|
||||
}
|
||||
fmt.Println(r)
|
||||
}
|
||||
|
||||
func TestPluginDownload(t *testing.T) {
|
||||
pluginManager := NewPluginManager("/Volumes/Downloads/dev/config/plugin")
|
||||
plugin, err := pluginManager.GetPlugin()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
ctx := &Context{
|
||||
AppName: "aliyunpan",
|
||||
Version: "v0.1.3",
|
||||
UserId: "11001d48564f43b3bc5662874f04bb11",
|
||||
Nickname: "tickstep",
|
||||
FileDriveId: "19519121",
|
||||
AlbumDriveId: "29519122",
|
||||
}
|
||||
params := &DownloadFilePrepareParams{
|
||||
DriveId: "19519221",
|
||||
DriveFilePath: "/test/aliyunpan/Downloads/token.bat",
|
||||
DriveFileName: "token.bat",
|
||||
DriveFileSize: 125330,
|
||||
DriveFileType: "file",
|
||||
DriveFileSha1: "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
DriveFileUpdatedAt: "2022-04-14 07:05:12",
|
||||
LocalFilePath: "aliyunpan\\Downloads\\token.bat",
|
||||
}
|
||||
b, _ := jsoniter.Marshal(ctx)
|
||||
fmt.Println(string(b))
|
||||
b, _ = jsoniter.Marshal(params)
|
||||
fmt.Println(string(b))
|
||||
r, e := plugin.DownloadFilePrepareCallback(ctx, params)
|
||||
if e != nil {
|
||||
fmt.Println(e)
|
||||
}
|
||||
fmt.Println(r)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"os"
|
||||
)
|
||||
|
||||
// HttpGet Http的get请求
|
||||
func HttpGet(header map[string]string, url string) string {
|
||||
client := requester.NewHTTPClient()
|
||||
body, err := client.Fetch("GET", url, nil, header)
|
||||
if err != nil {
|
||||
logger.Verboseln("js HttpJsonGet error ", err)
|
||||
return ""
|
||||
}
|
||||
return string(body)
|
||||
}
|
||||
|
||||
// HttpPost Http的post请求
|
||||
func HttpPost(header map[string]string, url string, data interface{}) string {
|
||||
client := requester.NewHTTPClient()
|
||||
body, err := client.Fetch("POST", url, data, header)
|
||||
if err != nil {
|
||||
logger.Verboseln("js HttpJsonPost error ", err)
|
||||
return ""
|
||||
}
|
||||
return string(body)
|
||||
}
|
||||
|
||||
// DeleteLocalFile 删除本地文件,不支持文件夹
|
||||
func DeleteLocalFile(localFilePath string) bool {
|
||||
err := os.Remove(localFilePath)
|
||||
if err != nil {
|
||||
// 删除失败
|
||||
return false
|
||||
} else {
|
||||
// 删除成功
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeleteLocalFile(t *testing.T) {
|
||||
fmt.Println(DeleteLocalFile("/Volumes/Downloads/dev/upload/2"))
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
package syncdrive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/bolt"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
// BoltDb 存储本地文件信息的数据库
|
||||
BoltDb struct {
|
||||
Path string
|
||||
db *bolt.DB
|
||||
locker *sync.Mutex
|
||||
}
|
||||
|
||||
BoltItem struct {
|
||||
FilePath string
|
||||
IsFolder bool
|
||||
Data string
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
DirBucketPrefix string = "##:@@"
|
||||
)
|
||||
|
||||
func NewBoltDb(dbFilePath string) *BoltDb {
|
||||
return &BoltDb{
|
||||
Path: dbFilePath,
|
||||
locker: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BoltDb) Open() (bool, error) {
|
||||
db, err := bolt.Open(b.Path, 0755, &bolt.Options{Timeout: 5 * time.Second})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
b.db = db
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (b *BoltDb) addOneItem(rootBucket *bolt.Bucket, item *BoltItem) (bool, error) {
|
||||
bkt := rootBucket
|
||||
parts := strings.Split(item.FilePath, "/")
|
||||
for _, p := range parts[:len(parts)-1] {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
bkt, _ = bkt.CreateBucketIfNotExists([]byte(DirBucketPrefix + p))
|
||||
}
|
||||
if bkt == nil {
|
||||
return false, fmt.Errorf("create or get bucket error")
|
||||
}
|
||||
|
||||
fileName := path.Base(item.FilePath)
|
||||
if e := bkt.Put([]byte(fileName), []byte(item.Data)); e != nil {
|
||||
return false, e
|
||||
}
|
||||
var err error
|
||||
if item.IsFolder {
|
||||
bkt, err = bkt.CreateBucketIfNotExists([]byte(DirBucketPrefix + fileName))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Add 增加一个数据项
|
||||
func (b *BoltDb) Add(item *BoltItem) (bool, error) {
|
||||
item.FilePath = FormatFilePath(item.FilePath)
|
||||
b.locker.Lock()
|
||||
defer b.locker.Unlock()
|
||||
|
||||
// add item
|
||||
// Start a writable transaction.
|
||||
tx, err := b.db.Begin(true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
rootBucket, er := tx.CreateBucketIfNotExists([]byte("/"))
|
||||
if er != nil {
|
||||
return false, er
|
||||
}
|
||||
if _, err := b.addOneItem(rootBucket, item); err != nil {
|
||||
// Commit the transaction and check for error.
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Commit the transaction and check for error.
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// AddItems 增加数据项
|
||||
func (b *BoltDb) AddItems(items []*BoltItem) (bool, error) {
|
||||
b.locker.Lock()
|
||||
defer b.locker.Unlock()
|
||||
|
||||
// add item
|
||||
// Start a writable transaction.
|
||||
tx, err := b.db.Begin(true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
rootBucket, er := tx.CreateBucketIfNotExists([]byte("/"))
|
||||
if er != nil {
|
||||
return false, er
|
||||
}
|
||||
for _, item := range items {
|
||||
item.FilePath = FormatFilePath(item.FilePath)
|
||||
if _, err := b.addOneItem(rootBucket, item); err != nil {
|
||||
// Commit the transaction and check for error.
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// Commit the transaction and check for error.
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Get 获取一个数据项,数据项不存在返回错误
|
||||
func (b *BoltDb) Get(filePath string) (string, error) {
|
||||
filePath = FormatFilePath(filePath)
|
||||
if filePath == "" {
|
||||
return "", fmt.Errorf("item is nil")
|
||||
}
|
||||
b.locker.Lock()
|
||||
defer b.locker.Unlock()
|
||||
|
||||
tx, err := b.db.Begin(false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
partsOrg := strings.Split(filePath, "/")
|
||||
parts := []string{}
|
||||
for _, p := range partsOrg {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, p)
|
||||
}
|
||||
|
||||
bkt := tx.Bucket([]byte("/"))
|
||||
if bkt == nil {
|
||||
return "", ErrItemNotExisted
|
||||
}
|
||||
for _, p := range parts[:len(parts)-1] {
|
||||
bkt = bkt.Bucket([]byte(DirBucketPrefix + p))
|
||||
if bkt == nil {
|
||||
return "", ErrItemNotExisted
|
||||
}
|
||||
}
|
||||
if bkt == nil {
|
||||
return "", ErrItemNotExisted
|
||||
}
|
||||
|
||||
data := bkt.Get([]byte(parts[len(parts)-1]))
|
||||
if data != nil {
|
||||
return string(data), nil
|
||||
}
|
||||
return "", ErrItemNotExisted
|
||||
}
|
||||
|
||||
func (b *BoltDb) GetFileList(filePath string) ([]string, error) {
|
||||
dataList := []string{}
|
||||
filePath = FormatFilePath(filePath)
|
||||
if filePath == "" {
|
||||
return dataList, fmt.Errorf("item is nil")
|
||||
}
|
||||
b.locker.Lock()
|
||||
defer b.locker.Unlock()
|
||||
|
||||
tx, err := b.db.Begin(false)
|
||||
if err != nil {
|
||||
return dataList, err
|
||||
}
|
||||
|
||||
partsOrg := strings.Split(filePath, "/")
|
||||
parts := []string{}
|
||||
for _, p := range partsOrg {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, p)
|
||||
}
|
||||
|
||||
bkt := tx.Bucket([]byte("/"))
|
||||
if bkt == nil {
|
||||
return dataList, ErrItemNotExisted
|
||||
}
|
||||
if len(parts) > 0 {
|
||||
for _, p := range parts[:len(parts)-1] {
|
||||
bkt = bkt.Bucket([]byte(DirBucketPrefix + p))
|
||||
if bkt == nil {
|
||||
return dataList, ErrItemNotExisted
|
||||
}
|
||||
}
|
||||
bkt = bkt.Bucket([]byte(DirBucketPrefix + parts[len(parts)-1]))
|
||||
if bkt == nil {
|
||||
return dataList, ErrItemNotExisted
|
||||
}
|
||||
}
|
||||
|
||||
if bkt == nil {
|
||||
return dataList, ErrItemNotExisted
|
||||
}
|
||||
|
||||
c := bkt.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
dataList = append(dataList, string(v))
|
||||
}
|
||||
return dataList, nil
|
||||
}
|
||||
|
||||
// Delete 删除一个数据项
|
||||
func (b *BoltDb) Delete(filePath string) (bool, error) {
|
||||
filePath = FormatFilePath(filePath)
|
||||
if filePath == "" {
|
||||
return false, fmt.Errorf("item is nil")
|
||||
}
|
||||
b.locker.Lock()
|
||||
defer b.locker.Unlock()
|
||||
|
||||
tx, err := b.db.Begin(true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
partsOrg := strings.Split(filePath, "/")
|
||||
parts := []string{}
|
||||
for _, p := range partsOrg {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, p)
|
||||
}
|
||||
|
||||
// get parent node
|
||||
bkt := tx.Bucket([]byte("/"))
|
||||
if bkt == nil {
|
||||
return false, ErrItemNotExisted
|
||||
}
|
||||
for _, p := range parts[:len(parts)-1] {
|
||||
bkt = bkt.Bucket([]byte(DirBucketPrefix + p))
|
||||
if bkt == nil {
|
||||
return false, ErrItemNotExisted
|
||||
}
|
||||
}
|
||||
if bkt == nil {
|
||||
return false, ErrItemNotExisted
|
||||
}
|
||||
|
||||
targetName := parts[len(parts)-1]
|
||||
dirBucket := bkt.Bucket([]byte(DirBucketPrefix + targetName))
|
||||
var er error
|
||||
if dirBucket != nil {
|
||||
// is dir, delete bucket
|
||||
er = bkt.DeleteBucket([]byte(DirBucketPrefix + targetName))
|
||||
}
|
||||
// delete item
|
||||
er = bkt.Delete([]byte(targetName))
|
||||
|
||||
// Commit the transaction and check for error.
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return er == nil, nil
|
||||
}
|
||||
|
||||
// Update 更新数据项,数据项不存在返回错误
|
||||
func (b *BoltDb) Update(filePath string, data string) (bool, error) {
|
||||
filePath = FormatFilePath(filePath)
|
||||
b.locker.Lock()
|
||||
defer b.locker.Unlock()
|
||||
|
||||
// update item
|
||||
// Start a writable transaction.
|
||||
tx, err := b.db.Begin(true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// get parent bucket of update item
|
||||
parts := strings.Split(filePath, "/")
|
||||
bkt := tx.Bucket([]byte("/"))
|
||||
if bkt == nil {
|
||||
return false, ErrItemNotExisted
|
||||
}
|
||||
for _, p := range parts[:len(parts)-1] {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
bkt = bkt.Bucket([]byte(DirBucketPrefix + p))
|
||||
if bkt == nil {
|
||||
return false, ErrItemNotExisted
|
||||
}
|
||||
}
|
||||
if bkt == nil {
|
||||
return false, ErrItemNotExisted
|
||||
}
|
||||
|
||||
// update content
|
||||
fileName := path.Base(filePath)
|
||||
if e := bkt.Put([]byte(fileName), []byte(data)); e != nil {
|
||||
return false, e
|
||||
}
|
||||
|
||||
// Commit the transaction and check for error.
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Close 关闭数据库
|
||||
func (b *BoltDb) Close() (bool, error) {
|
||||
b.locker.Lock()
|
||||
defer b.locker.Unlock()
|
||||
if b.db != nil {
|
||||
if e := b.db.Close(); e != nil {
|
||||
return false, e
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package syncdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/tickstep/bolt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPath(t *testing.T) {
|
||||
db, err := bolt.Open("D:\\smb\\feny\\goprojects\\dev\\sync_drive\\5b2d7c10-e927-4e72-8f9d-5abb3bb04814\\test.db", 0600, &bolt.Options{Timeout: 5 * time.Second})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// add item
|
||||
// Start a writable transaction.
|
||||
tx, err := db.Begin(true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
rootBucket, _ := tx.CreateBucketIfNotExists([]byte("/"))
|
||||
if e := rootBucket.Put([]byte("test"), []byte("test value")); e != nil {
|
||||
return
|
||||
}
|
||||
_, e := rootBucket.CreateBucketIfNotExists([]byte("test"))
|
||||
println(e)
|
||||
|
||||
// Commit the transaction and check for error.
|
||||
if err := tx.Commit(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func dosomething(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
fmt.Println("playing")
|
||||
return
|
||||
default:
|
||||
fmt.Println("I am working!")
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext(t *testing.T) {
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
cancelFunc()
|
||||
}()
|
||||
dosomething(ctx)
|
||||
}
|
||||
|
||||
func TestBolt(t *testing.T) {
|
||||
localFileDb := NewLocalSyncDb("D:\\smb\\feny\\goprojects\\dev\\sync_drive\\local.db")
|
||||
localFileDb.Open()
|
||||
defer localFileDb.Close()
|
||||
localFileDb.Add(&LocalFileItem{
|
||||
FileName: "dev",
|
||||
FileSize: 0,
|
||||
FileType: "folder",
|
||||
CreatedAt: "2022-05-12 10:21:14",
|
||||
UpdatedAt: "2022-05-12 10:21:14",
|
||||
FileExtension: ".db",
|
||||
Sha1Hash: "",
|
||||
Path: "D:\\smb\\feny\\goprojects\\dev",
|
||||
})
|
||||
localFileDb.Add(&LocalFileItem{
|
||||
FileName: "file1.db",
|
||||
FileSize: 0,
|
||||
FileType: "file",
|
||||
CreatedAt: "2022-05-12 10:21:14",
|
||||
UpdatedAt: "2022-05-12 10:21:14",
|
||||
FileExtension: ".db",
|
||||
Sha1Hash: "",
|
||||
Path: "D:\\smb\\feny\\goprojects\\dev\\file1.db",
|
||||
})
|
||||
go func(db LocalSyncDb) {
|
||||
for i := 1; i <= 10; i++ {
|
||||
sb := &strings.Builder{}
|
||||
fmt.Fprintf(sb, "D:\\smb\\feny\\goprojects\\dev\\go\\file%d.db", i)
|
||||
db.Add(&LocalFileItem{
|
||||
FileName: "file1.db",
|
||||
FileSize: 0,
|
||||
FileType: "file",
|
||||
CreatedAt: "2022-05-12 10:21:14",
|
||||
UpdatedAt: "2022-05-12 10:21:14",
|
||||
FileExtension: ".db",
|
||||
Sha1Hash: "",
|
||||
Path: sb.String(),
|
||||
})
|
||||
}
|
||||
}(localFileDb)
|
||||
time.Sleep(1 * time.Second)
|
||||
localFileDb.Add(&LocalFileItem{
|
||||
FileName: "file1.db",
|
||||
FileSize: 0,
|
||||
FileType: "file",
|
||||
CreatedAt: "2022-05-12 10:21:14",
|
||||
UpdatedAt: "2022-05-12 10:21:14",
|
||||
FileExtension: ".db",
|
||||
Sha1Hash: "",
|
||||
Path: "D:\\smb\\feny\\goprojects\\dev\\file3.db",
|
||||
})
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
func TestBoltUltraFiles(t *testing.T) {
|
||||
localFileDb := NewLocalSyncDb("/Volumes/DataDisk3T/test/local.bolt")
|
||||
localFileDb.Open()
|
||||
defer localFileDb.Close()
|
||||
|
||||
count := int64(0)
|
||||
walkFunc := func(parentDirPath string, infos []fs.FileInfo, err error) error {
|
||||
files := LocalFileList{}
|
||||
for _, info := range infos {
|
||||
if strings.Index(info.Name(), ".") == 0 {
|
||||
continue
|
||||
}
|
||||
count += 1
|
||||
file := parentDirPath + "/" + info.Name()
|
||||
fmt.Println(count, " - ", file)
|
||||
files = append(files, newLocalFileItem(info, file))
|
||||
}
|
||||
localFileDb.AddFileList(files)
|
||||
return nil
|
||||
}
|
||||
WalkAllFileFunc("/Volumes/Downloads", walkFunc)
|
||||
println("\ntotal: ", count)
|
||||
}
|
||||
|
||||
type MyWalkFunc func(parentDirPath string, infos []fs.FileInfo, err error) error
|
||||
|
||||
func WalkAllFileFunc(dirPath string, walkFn MyWalkFunc) error {
|
||||
info, err := os.Lstat(dirPath)
|
||||
if err != nil {
|
||||
infos := []os.FileInfo{}
|
||||
infos = append(infos, info)
|
||||
err = walkFn(dirPath, infos, err)
|
||||
} else {
|
||||
err = walkAllFileFunc(dirPath, info, walkFn)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func walkAllFileFunc(dirPath string, info os.FileInfo, walkFn MyWalkFunc) error {
|
||||
if !info.IsDir() {
|
||||
infos := []os.FileInfo{}
|
||||
infos = append(infos, info)
|
||||
return walkFn(dirPath, infos, nil)
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return walkFn(dirPath, nil, err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return nil
|
||||
}
|
||||
err = walkFn(dirPath, files, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, fi := range files {
|
||||
if fi.IsDir() {
|
||||
err = walkAllFileFunc(dirPath+"/"+fi.Name(), fi, walkFn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,702 @@
|
||||
package syncdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/internal/file/downloader"
|
||||
"github.com/tickstep/aliyunpan/internal/file/uploader"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panupload"
|
||||
"github.com/tickstep/aliyunpan/internal/localfile"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"github.com/tickstep/library-go/requester/rio"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
FileAction string
|
||||
FileActionTask struct {
|
||||
localFileDb LocalSyncDb
|
||||
panFileDb PanSyncDb
|
||||
syncFileDb SyncFileDb
|
||||
|
||||
panClient *aliyunpan.PanClient
|
||||
|
||||
syncItem *SyncFileItem
|
||||
maxDownloadRate int64 // 限制最大下载速度
|
||||
maxUploadRate int64 // 限制最大上传速度
|
||||
|
||||
localFolderCreateMutex *sync.Mutex
|
||||
panFolderCreateMutex *sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
func (f *FileActionTask) prompt(msg string) {
|
||||
if LogPrompt {
|
||||
fmt.Println("[" + utils.NowTimeStr() + "] " + msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileActionTask) HashCode() string {
|
||||
postfix := ""
|
||||
if f.syncItem.Action == SyncFileActionDownload {
|
||||
postfix = strings.ReplaceAll(f.syncItem.PanFile.Path, "\\", "/")
|
||||
} else if f.syncItem.Action == SyncFileActionUpload {
|
||||
postfix = strings.ReplaceAll(f.syncItem.LocalFile.Path, "\\", "/")
|
||||
}
|
||||
return string(f.syncItem.Action) + postfix
|
||||
}
|
||||
|
||||
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())
|
||||
if e := f.uploadFile(ctx); e != nil {
|
||||
// TODO: retry / cleanup downloading file
|
||||
return e
|
||||
} else {
|
||||
// upload success, post operation
|
||||
// save local file info into db
|
||||
if f.syncItem.UploadEntity != nil && f.syncItem.UploadEntity.FileId != "" {
|
||||
if file, er := f.panClient.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)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if f.syncItem.Action == SyncFileActionDownload {
|
||||
f.prompt("下载文件:" + f.syncItem.getPanFileFullPath())
|
||||
if e := f.downloadFile(ctx); e != nil {
|
||||
// TODO: retry / cleanup downloading file
|
||||
return e
|
||||
} else {
|
||||
// download success, post operation
|
||||
if b, er := utils.PathExists(f.syncItem.getLocalFileFullPath()); er == nil && b {
|
||||
// file existed
|
||||
// remove old local file
|
||||
logger.Verbosef("delete local old file")
|
||||
if err := os.Remove(f.syncItem.getLocalFileFullPath()); err != nil {
|
||||
// error
|
||||
logger.Verbosef("移除本地旧文件出错: %s, %s\n", f.syncItem.getLocalFileFullPath(), err)
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
// rename downloading file into target name file
|
||||
if err1 := os.Rename(f.syncItem.getLocalFileDownloadingFullPath(), f.syncItem.getLocalFileFullPath()); err1 != nil {
|
||||
logger.Verbosef("重命名下载文件出错: %s, %s\n", f.syncItem.getLocalFileDownloadingFullPath(), err1)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
return fmt.Errorf("重命名下载文件出错")
|
||||
}
|
||||
// success
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// change modify time of local file
|
||||
if err := os.Chtimes(f.syncItem.getLocalFileFullPath(), f.syncItem.PanFile.UpdateTime(), f.syncItem.PanFile.UpdateTime()); err != nil {
|
||||
logger.Verbosef(err.Error())
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// save local file info into db
|
||||
if file, er := os.Stat(f.syncItem.getLocalFileFullPath()); er == nil {
|
||||
f.localFileDb.Add(&LocalFileItem{
|
||||
FileName: file.Name(),
|
||||
FileSize: file.Size(),
|
||||
FileType: "file",
|
||||
CreatedAt: file.ModTime().Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: file.ModTime().Format("2006-01-02 15:04:05"),
|
||||
FileExtension: path.Ext(file.Name()),
|
||||
Sha1Hash: f.syncItem.PanFile.Sha1Hash,
|
||||
Path: f.syncItem.getLocalFileFullPath(),
|
||||
ScanTimeAt: utils.NowTimeStr(),
|
||||
ScanStatus: ScanStatusNormal,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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{
|
||||
DriveId: f.syncItem.PanFile.DriveId,
|
||||
FileId: f.syncItem.PanFile.FileId,
|
||||
})
|
||||
time.Sleep(time.Duration(200) * time.Millisecond)
|
||||
if apierr != nil {
|
||||
if apierr.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
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 durl == nil || durl.Url == "" {
|
||||
logger.Verbosef("无法获取有效的下载链接: %+v\n", durl)
|
||||
f.syncItem.Status = SyncFileStatusFailed
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return fmt.Errorf("无法获取有效的下载链接")
|
||||
}
|
||||
if durl.Url == aliyunpan.IllegalDownloadUrl {
|
||||
logger.Verbosef("无法获取有效的下载链接: %+v\n", durl)
|
||||
f.syncItem.Status = SyncFileStatusIllegal
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return fmt.Errorf("文件非法,无法下载")
|
||||
}
|
||||
localDir := filepath.Dir(f.syncItem.getLocalFileFullPath())
|
||||
if b, e := utils.PathExists(localDir); e == nil && !b {
|
||||
f.localFolderCreateMutex.Lock()
|
||||
os.MkdirAll(localDir, 0755)
|
||||
f.localFolderCreateMutex.Unlock()
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
writer, file, err := downloader.NewDownloaderWriterByFilename(f.syncItem.getLocalFileDownloadingFullPath(), os.O_CREATE|os.O_WRONLY, 0666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s, %s", "初始化下载发生错误", err)
|
||||
}
|
||||
defer file.Close()
|
||||
if f.syncItem.PanFile.FileSize == 0 {
|
||||
// zero file
|
||||
f.syncItem.Status = SyncFileStatusDownloading
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// 限速
|
||||
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())
|
||||
// }
|
||||
//}()
|
||||
}
|
||||
|
||||
client := requester.NewHTTPClient()
|
||||
client.SetKeepAlive(true)
|
||||
client.SetTimeout(10 * time.Minute)
|
||||
worker.SetClient(client)
|
||||
worker.SetPanClient(f.panClient)
|
||||
|
||||
writeMu := &sync.Mutex{}
|
||||
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
|
||||
f.syncItem.Status = SyncFileStatusDownloading
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// cancel routine & done
|
||||
logger.Verboseln("file download routine done")
|
||||
return nil
|
||||
default:
|
||||
logger.Verboseln("do file download process")
|
||||
if f.syncItem.DownloadRange.End > f.syncItem.PanFile.FileSize {
|
||||
f.syncItem.DownloadRange.End = f.syncItem.PanFile.FileSize
|
||||
}
|
||||
worker.SetRange(f.syncItem.DownloadRange) // 分片
|
||||
|
||||
// 检查上次执行是否有下载已完成
|
||||
if f.syncItem.DownloadRange.Begin == f.syncItem.PanFile.FileSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 下载分片
|
||||
// TODO: 下载失败,分片重试策略
|
||||
worker.Execute()
|
||||
|
||||
if worker.GetStatus().StatusCode() == downloader.StatusCodeSuccessed {
|
||||
if f.syncItem.DownloadRange.End == f.syncItem.PanFile.FileSize {
|
||||
// finished
|
||||
f.syncItem.Status = SyncFileStatusDownloading
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 下一个分片
|
||||
f.syncItem.DownloadRange.Begin = f.syncItem.DownloadRange.End
|
||||
f.syncItem.DownloadRange.End += f.syncItem.DownloadBlockSize
|
||||
|
||||
// 存储状态
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
}
|
||||
// TODO: 下载链接过期处理
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileActionTask) uploadFile(ctx context.Context) error {
|
||||
if b, e := utils.PathExists(f.syncItem.LocalFile.Path); e == nil {
|
||||
if !b {
|
||||
// 本地文件不存在,无法上传
|
||||
f.syncItem.Status = SyncFileStatusNotExisted
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
localFile := localfile.NewLocalFileEntity(f.syncItem.LocalFile.Path)
|
||||
err := localFile.OpenPath()
|
||||
if err != nil {
|
||||
logger.Verbosef("文件不可读 %s, 错误信息: %s\n", localFile.Path, err)
|
||||
return err
|
||||
}
|
||||
defer localFile.Close() // 关闭文件
|
||||
|
||||
// 网盘目标文件路径
|
||||
targetPanFilePath := f.syncItem.getPanFileFullPath()
|
||||
|
||||
if f.syncItem.UploadEntity == nil {
|
||||
// 计算文件SHA1
|
||||
sha1Str := ""
|
||||
|
||||
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 创建文件夹
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算proof code
|
||||
proofCode := ""
|
||||
localFileEntity, _ := os.Open(localFile.Path.RealPath)
|
||||
localFileInfo, _ := localFileEntity.Stat()
|
||||
proofCode = aliyunpan.CalcProofCode(f.panClient.GetAccessToken(), rio.NewFileReaderAtLen64(localFileEntity), localFileInfo.Size())
|
||||
|
||||
// 创建上传任务
|
||||
appCreateUploadFileParam := &aliyunpan.CreateFileUploadParam{
|
||||
DriveId: f.syncItem.DriveId,
|
||||
Name: filepath.Base(targetPanFilePath),
|
||||
Size: localFile.Length,
|
||||
ContentHash: sha1Str,
|
||||
ContentHashName: "sha1",
|
||||
CheckNameMode: "overwrite", // 覆盖云盘文件
|
||||
ParentFileId: panDirFileId,
|
||||
BlockSize: f.syncItem.UploadBlockSize,
|
||||
ProofCode: proofCode,
|
||||
ProofVersion: "v1",
|
||||
}
|
||||
if uploadOpEntity, err := f.panClient.CreateUploadFile(appCreateUploadFileParam); err != nil {
|
||||
logger.Verbosef("创建云盘上传任务失败: %s\n", panDirPath)
|
||||
return err
|
||||
} else {
|
||||
f.syncItem.UploadEntity = uploadOpEntity
|
||||
// 存储状态
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
}
|
||||
|
||||
// 秒传
|
||||
if f.syncItem.UploadEntity.RapidUpload {
|
||||
logger.Verbosef("秒传成功, 保存到网盘路径: %s\n", targetPanFilePath)
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
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)
|
||||
infoList := make([]aliyunpan.FileUploadPartInfoParam, 0)
|
||||
for _, item := range f.syncItem.UploadEntity.PartInfoList {
|
||||
infoList = append(infoList, aliyunpan.FileUploadPartInfoParam{
|
||||
PartNumber: item.PartNumber,
|
||||
})
|
||||
}
|
||||
refreshUploadParam := &aliyunpan.GetUploadUrlParam{
|
||||
DriveId: f.syncItem.UploadEntity.DriveId,
|
||||
FileId: f.syncItem.UploadEntity.FileId,
|
||||
PartInfoList: infoList,
|
||||
UploadId: f.syncItem.UploadEntity.UploadId,
|
||||
}
|
||||
newUploadInfo, err1 := f.panClient.GetUploadUrl(refreshUploadParam)
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
f.syncItem.UploadEntity.PartInfoList = newUploadInfo.PartInfoList
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建分片上传器
|
||||
// 阿里云盘默认就是分片上传,每一个分片对应一个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)
|
||||
|
||||
if f.syncItem.UploadRange == nil {
|
||||
f.syncItem.UploadRange = &transfer.Range{
|
||||
Begin: 0,
|
||||
End: f.syncItem.UploadBlockSize,
|
||||
}
|
||||
}
|
||||
|
||||
worker.Precreate()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// cancel routine & done
|
||||
logger.Verboseln("file upload routine done")
|
||||
return nil
|
||||
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)
|
||||
|
||||
if uploadDone, terr := worker.UploadFile(ctx, f.syncItem.UploadPartSeq, f.syncItem.UploadRange.Begin, f.syncItem.UploadRange.End, fileReader, uploadClient); terr == nil {
|
||||
if uploadDone {
|
||||
// 上传成功
|
||||
if f.syncItem.UploadRange.End == f.syncItem.LocalFile.FileSize {
|
||||
// commit
|
||||
worker.CommitFile()
|
||||
|
||||
// finished
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 下一个分片
|
||||
f.syncItem.UploadPartSeq += 1
|
||||
f.syncItem.UploadRange.Begin = f.syncItem.UploadRange.End
|
||||
f.syncItem.UploadRange.End += f.syncItem.UploadBlockSize
|
||||
|
||||
// 存储状态
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
} else {
|
||||
// TODO: 上传失败,重试策略
|
||||
logger.Verboseln("upload file part error")
|
||||
}
|
||||
} else {
|
||||
// error
|
||||
logger.Verboseln("error: ", terr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
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)
|
||||
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
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
||||
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{})
|
||||
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"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package syncdrive
|
||||
|
||||
const (
|
||||
// DownloadingFileSuffix 下载中文件后缀
|
||||
DownloadingFileSuffix string = ".aliyunpan"
|
||||
|
||||
// TimeSecondsOf30Seconds 30秒
|
||||
TimeSecondsOf30Seconds int64 = 30
|
||||
|
||||
// TimeSecondsOfOneMinute 一分钟秒数
|
||||
TimeSecondsOfOneMinute int64 = 60
|
||||
|
||||
// TimeSecondsOf2Minute 2分钟秒数
|
||||
TimeSecondsOf2Minute int64 = 2 * TimeSecondsOfOneMinute
|
||||
|
||||
// TimeSecondsOf5Minute 5分钟秒数
|
||||
TimeSecondsOf5Minute int64 = 5 * TimeSecondsOfOneMinute
|
||||
|
||||
// TimeSecondsOf10Minute 10分钟秒数
|
||||
TimeSecondsOf10Minute int64 = 10 * TimeSecondsOfOneMinute
|
||||
|
||||
// TimeSecondsOf30Minute 30分钟秒数
|
||||
TimeSecondsOf30Minute int64 = 30 * TimeSecondsOfOneMinute
|
||||
|
||||
// TimeSecondsOf60Minute 60分钟秒数
|
||||
TimeSecondsOf60Minute int64 = 60 * TimeSecondsOfOneMinute
|
||||
)
|
||||
|
||||
var (
|
||||
LogPrompt = false
|
||||
)
|
||||
@@ -0,0 +1,392 @@
|
||||
package syncdrive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
// SyncPriorityOption 同步优先级选项
|
||||
SyncPriorityOption string
|
||||
|
||||
// ScanStatus 扫描状态
|
||||
ScanStatus string
|
||||
|
||||
// PanFileItem 网盘文件信息
|
||||
PanFileItem struct {
|
||||
// 网盘ID
|
||||
DriveId string `json:"driveId"`
|
||||
// 域ID
|
||||
DomainId string `json:"domainId"`
|
||||
// FileId 文件ID
|
||||
FileId string `json:"fileId"`
|
||||
// FileName 文件名
|
||||
FileName string `json:"fileName"`
|
||||
// FileSize 文件大小
|
||||
FileSize int64 `json:"fileSize"`
|
||||
// 文件类别 folder / file
|
||||
FileType string `json:"fileType"`
|
||||
// 创建时间
|
||||
CreatedAt string `json:"createdAt"`
|
||||
// 最后修改时间
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
// 后缀名,例如:dmg
|
||||
FileExtension string `json:"fileExtension"`
|
||||
// 文件上传ID
|
||||
UploadId string `json:"uploadId"`
|
||||
// 父文件夹ID
|
||||
ParentFileId string `json:"parentFileId"`
|
||||
// 内容CRC64校验值,只有文件才会有
|
||||
Crc64Hash string `json:"crc64Hash"`
|
||||
// 内容Hash值,只有文件才会有
|
||||
Sha1Hash string `json:"sha1Hash"`
|
||||
// FilePath 文件的完整路径
|
||||
Path string `json:"path"`
|
||||
// Category 文件分类,例如:image/video/doc/others
|
||||
Category string `json:"category"`
|
||||
// ScanTimeAt 扫描时间
|
||||
ScanTimeAt string `json:"scanTimeAt"`
|
||||
// ScanStatus 扫描状态
|
||||
ScanStatus ScanStatus `json:"scanStatus"`
|
||||
}
|
||||
PanFileList []*PanFileItem
|
||||
|
||||
PanSyncDb interface {
|
||||
// Open 打开并准备数据库
|
||||
Open() (bool, error)
|
||||
// Add 存储一个数据项
|
||||
Add(item *PanFileItem) (bool, error)
|
||||
// AddFileList 存储批量数据项
|
||||
AddFileList(items PanFileList) (bool, error)
|
||||
// Get 获取一个数据项
|
||||
Get(filePath string) (*PanFileItem, error)
|
||||
// GetFileList 获取文件夹下的所有的文件列表
|
||||
GetFileList(filePath string) (PanFileList, error)
|
||||
// Delete 删除一个数据项,如果是文件夹,则会删除文件夹下面所有的文件列表
|
||||
Delete(filePath string) (bool, error)
|
||||
// Update 更新一个数据项数据
|
||||
Update(item *PanFileItem) (bool, error)
|
||||
// Close 关闭数据库
|
||||
Close() (bool, error)
|
||||
}
|
||||
|
||||
// LocalFileItem 本地文件信息
|
||||
LocalFileItem struct {
|
||||
// FileName 文件名
|
||||
FileName string `json:"fileName"`
|
||||
// FileSize 文件大小
|
||||
FileSize int64 `json:"fileSize"`
|
||||
// 文件类别 folder / file
|
||||
FileType string `json:"fileType"`
|
||||
// 创建时间
|
||||
CreatedAt string `json:"createdAt"`
|
||||
// 最后修改时间
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
// 后缀名,例如:dmg
|
||||
FileExtension string `json:"fileExtension"`
|
||||
// 内容Hash值,只有文件才会有
|
||||
Sha1Hash string `json:"sha1Hash"`
|
||||
// FilePath 文件的完整路径
|
||||
Path string `json:"path"`
|
||||
// ScanTimeAt 扫描时间
|
||||
ScanTimeAt string `json:"scanTimeAt"`
|
||||
// ScanStatus 扫描状态
|
||||
ScanStatus ScanStatus `json:"scanStatus"`
|
||||
}
|
||||
LocalFileList []*LocalFileItem
|
||||
|
||||
LocalSyncDb interface {
|
||||
// Open 打开并准备数据库
|
||||
Open() (bool, error)
|
||||
// Add 存储一个数据项
|
||||
Add(item *LocalFileItem) (bool, error)
|
||||
// AddFileList 存储批量数据项
|
||||
AddFileList(items LocalFileList) (bool, error)
|
||||
// Get 获取一个数据项
|
||||
Get(filePath string) (*LocalFileItem, error)
|
||||
// GetFileList 获取文件夹下的所有的文件列表
|
||||
GetFileList(filePath string) (LocalFileList, error)
|
||||
// Delete 删除一个数据项,如果是文件夹,则会删除文件夹下面所有的文件列表
|
||||
Delete(filePath string) (bool, error)
|
||||
// Update 更新一个数据项数据
|
||||
Update(item *LocalFileItem) (bool, error)
|
||||
// Close 关闭数据库
|
||||
Close() (bool, error)
|
||||
}
|
||||
|
||||
SyncFileAction string
|
||||
SyncFileStatus string
|
||||
SyncFileItem struct {
|
||||
Action SyncFileAction `json:"action"`
|
||||
Status SyncFileStatus `json:"status"`
|
||||
LocalFile *LocalFileItem `json:"localFile"`
|
||||
PanFile *PanFileItem `json:"panFile"`
|
||||
// LocalFolderPath 本地目录
|
||||
LocalFolderPath string `json:"localFolderPath"`
|
||||
// PanFolderPath 云盘目录
|
||||
PanFolderPath string `json:"panFolderPath"`
|
||||
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"`
|
||||
UploadEntity *aliyunpan.CreateFileUploadResult `json:"uploadEntity"`
|
||||
// UploadPartSeq 上传序号,从0开始
|
||||
UploadPartSeq int `json:"uploadPartSeq"`
|
||||
UploadBlockSize int64 `json:"uploadBlockSize"`
|
||||
}
|
||||
SyncFileList []*SyncFileItem
|
||||
|
||||
SyncFileDb interface {
|
||||
// Open 打开并准备数据库
|
||||
Open() (bool, error)
|
||||
// Add 存储一个数据项
|
||||
Add(item *SyncFileItem) (bool, error)
|
||||
// Get 获取一个数据项
|
||||
Get(id string) (*SyncFileItem, error)
|
||||
// GetFileList 获取文件夹下的所有的文件列表
|
||||
GetFileList(Status SyncFileStatus) (SyncFileList, error)
|
||||
// Delete 删除一个数据项,如果是文件夹,则会删除文件夹下面所有的文件列表
|
||||
Delete(id string) (bool, error)
|
||||
// Update 更新一个数据项数据
|
||||
Update(item *SyncFileItem) (bool, error)
|
||||
// Close 关闭数据库
|
||||
Close() (bool, error)
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
SyncFileStatusCreate SyncFileStatus = "create"
|
||||
SyncFileStatusUploading SyncFileStatus = "uploading"
|
||||
SyncFileStatusDownloading SyncFileStatus = "downloading"
|
||||
SyncFileStatusFailed SyncFileStatus = "failed"
|
||||
SyncFileStatusSuccess SyncFileStatus = "success"
|
||||
SyncFileStatusIllegal SyncFileStatus = "illegal"
|
||||
SyncFileStatusNotExisted SyncFileStatus = "notExisted"
|
||||
|
||||
SyncFileActionDownload SyncFileAction = "download"
|
||||
SyncFileActionUpload SyncFileAction = "upload"
|
||||
SyncFileActionDeleteLocal SyncFileAction = "delete_local"
|
||||
SyncFileActionDeletePan SyncFileAction = "delete_pan"
|
||||
SyncFileActionCreateLocalFolder SyncFileAction = "create_local_folder"
|
||||
SyncFileActionCreatePanFolder SyncFileAction = "create_pan_folder"
|
||||
|
||||
// ScanStatusNormal 正常
|
||||
ScanStatusNormal ScanStatus = "normal"
|
||||
// ScanStatusDiscard 已过期,已删除
|
||||
ScanStatusDiscard ScanStatus = "discard"
|
||||
|
||||
// SyncPriorityTimestampFirst 最新时间优先
|
||||
SyncPriorityTimestampFirst = "time"
|
||||
// SyncPriorityLocalFirst 本地文件优先
|
||||
SyncPriorityLocalFirst = "local"
|
||||
// SyncPriorityPanFirst 网盘文件优先
|
||||
SyncPriorityPanFirst = "pan"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrItemNotExisted error = fmt.Errorf("item is not existed")
|
||||
)
|
||||
|
||||
func NewPanFileItem(fe *aliyunpan.FileEntity) *PanFileItem {
|
||||
return &PanFileItem{
|
||||
DriveId: fe.DriveId,
|
||||
DomainId: fe.DomainId,
|
||||
FileId: fe.FileId,
|
||||
FileName: fe.FileName,
|
||||
FileSize: fe.FileSize,
|
||||
FileType: fe.FileType,
|
||||
CreatedAt: fe.CreatedAt,
|
||||
UpdatedAt: fe.UpdatedAt,
|
||||
FileExtension: fe.FileExtension,
|
||||
UploadId: fe.UploadId,
|
||||
ParentFileId: fe.ParentFileId,
|
||||
Crc64Hash: fe.Crc64Hash,
|
||||
Sha1Hash: fe.ContentHash,
|
||||
Path: fe.Path,
|
||||
Category: fe.Category,
|
||||
ScanStatus: ScanStatusNormal,
|
||||
}
|
||||
}
|
||||
|
||||
func (item *PanFileItem) Id() string {
|
||||
sb := &strings.Builder{}
|
||||
fmt.Fprintf(sb, "%s%s", strings.ReplaceAll(item.Path, "\\", "/"), item.UpdatedAt)
|
||||
return utils.Md5Str(sb.String())
|
||||
}
|
||||
|
||||
func (item *PanFileItem) FormatFileName() string {
|
||||
return item.FileName
|
||||
}
|
||||
|
||||
func (item *PanFileItem) FormatFilePath() string {
|
||||
return FormatFilePath(item.Path)
|
||||
}
|
||||
|
||||
func (item *PanFileItem) IsFolder() bool {
|
||||
return item.FileType == "folder"
|
||||
}
|
||||
|
||||
func (item *PanFileItem) UpdateTimeUnix() int64 {
|
||||
return item.UpdateTime().Unix()
|
||||
}
|
||||
|
||||
func (item *PanFileItem) UpdateTime() time.Time {
|
||||
return utils.ParseTimeStr(item.UpdatedAt)
|
||||
}
|
||||
|
||||
func (item *PanFileItem) HashCode() string {
|
||||
return item.Path
|
||||
}
|
||||
|
||||
func (item *PanFileItem) ScanTimeUnix() int64 {
|
||||
return item.ScanTime().Unix()
|
||||
}
|
||||
|
||||
func (item *PanFileItem) ScanTime() time.Time {
|
||||
return utils.ParseTimeStr(item.ScanTimeAt)
|
||||
}
|
||||
|
||||
func NewPanSyncDb(dbFilePath string) PanSyncDb {
|
||||
return interface{}(newPanSyncDbBolt(dbFilePath)).(PanSyncDb)
|
||||
}
|
||||
|
||||
func (item *LocalFileItem) Id() string {
|
||||
sb := &strings.Builder{}
|
||||
fmt.Fprintf(sb, "%s%s", strings.ReplaceAll(item.Path, "\\", "/"), item.UpdatedAt)
|
||||
return utils.Md5Str(sb.String())
|
||||
}
|
||||
|
||||
func (item *LocalFileItem) FormatFileName() string {
|
||||
return item.FileName
|
||||
}
|
||||
|
||||
func (item *LocalFileItem) FormatFilePath() string {
|
||||
return FormatFilePath(item.Path)
|
||||
}
|
||||
|
||||
func (item *LocalFileItem) IsFolder() bool {
|
||||
return item.FileType == "folder"
|
||||
}
|
||||
|
||||
func (item *LocalFileItem) IsFile() bool {
|
||||
return item.FileType == "file"
|
||||
}
|
||||
|
||||
func (item *LocalFileItem) UpdateTimeUnix() int64 {
|
||||
return item.UpdateTime().Unix()
|
||||
}
|
||||
|
||||
func (item *LocalFileItem) UpdateTime() time.Time {
|
||||
return utils.ParseTimeStr(item.UpdatedAt)
|
||||
}
|
||||
|
||||
func (item *LocalFileItem) ScanTimeUnix() int64 {
|
||||
return item.ScanTime().Unix()
|
||||
}
|
||||
|
||||
func (item *LocalFileItem) ScanTime() time.Time {
|
||||
return utils.ParseTimeStr(item.ScanTimeAt)
|
||||
}
|
||||
|
||||
func (item *LocalFileItem) HashCode() string {
|
||||
return item.Path
|
||||
}
|
||||
|
||||
func NewLocalSyncDb(dbFilePath string) LocalSyncDb {
|
||||
return interface{}(newLocalSyncDbBolt(dbFilePath)).(LocalSyncDb)
|
||||
}
|
||||
|
||||
func (l LocalFileList) FindFileByPath(filePath string) *LocalFileItem {
|
||||
for _, item := range l {
|
||||
if filePath == item.Path {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p PanFileList) FindFileByPath(filePath string) *PanFileItem {
|
||||
for _, item := range p {
|
||||
if strings.ReplaceAll(filePath, "\\", "/") == item.Path {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (item *SyncFileItem) Id() string {
|
||||
sb := &strings.Builder{}
|
||||
if item.Action == SyncFileActionDownload || item.Action == SyncFileActionDeleteLocal || item.Action == SyncFileActionCreateLocalFolder {
|
||||
fmt.Fprintf(sb, "%s%s", string(item.Action), item.PanFile.Id())
|
||||
} else if item.Action == SyncFileActionUpload || item.Action == SyncFileActionDeletePan || item.Action == SyncFileActionCreatePanFolder {
|
||||
fmt.Fprintf(sb, "%s%s", string(item.Action), item.LocalFile.Id())
|
||||
}
|
||||
return utils.Md5Str(sb.String())
|
||||
}
|
||||
|
||||
func (item *SyncFileItem) StatusUpdateTimeUnix() int64 {
|
||||
if ts, er := time.Parse("2006-01-02 15:04:05", item.StatusUpdateTime); er != nil {
|
||||
return ts.Unix()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// getPanFullPath 获取网盘文件的路径
|
||||
func (item *SyncFileItem) getPanFileFullPath() string {
|
||||
if item.PanFile != nil {
|
||||
return item.PanFile.Path
|
||||
}
|
||||
localPath := item.LocalFile.Path
|
||||
localPath = strings.ReplaceAll(localPath, "\\", "/")
|
||||
localRootPath := strings.ReplaceAll(item.LocalFolderPath, "\\", "/")
|
||||
|
||||
relativePath := strings.TrimPrefix(localPath, localRootPath)
|
||||
return path.Join(path.Clean(item.PanFolderPath), relativePath)
|
||||
}
|
||||
|
||||
// getLocalFullPath 获取本地文件的路径
|
||||
func (item *SyncFileItem) getLocalFileFullPath() string {
|
||||
if item.LocalFile != nil {
|
||||
return item.LocalFile.Path
|
||||
}
|
||||
panPath := item.PanFile.Path
|
||||
panPath = strings.ReplaceAll(panPath, "\\", "/")
|
||||
panRootPath := strings.ReplaceAll(item.PanFolderPath, "\\", "/")
|
||||
|
||||
relativePath := strings.TrimPrefix(panPath, panRootPath)
|
||||
return path.Join(path.Clean(item.LocalFolderPath), relativePath)
|
||||
}
|
||||
|
||||
// getLocalFileDownloadingFullPath 获取本地文件下载时的路径
|
||||
func (item *SyncFileItem) getLocalFileDownloadingFullPath() string {
|
||||
return item.getLocalFileFullPath() + DownloadingFileSuffix
|
||||
}
|
||||
|
||||
func (item *SyncFileItem) String() string {
|
||||
sb := &strings.Builder{}
|
||||
fp := ""
|
||||
if item.Action == SyncFileActionDownload {
|
||||
fp = item.PanFile.Path
|
||||
} else if item.Action == SyncFileActionUpload {
|
||||
fp = item.LocalFile.Path
|
||||
}
|
||||
fmt.Fprintf(sb, "ID:%s\nAction:%s\nStatus:%s\nPath:%s\n",
|
||||
item.Id(), item.Action, item.Status, fp)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (item *SyncFileItem) HashCode() string {
|
||||
return item.Id()
|
||||
}
|
||||
|
||||
func NewSyncFileDb(dbFilePath string) SyncFileDb {
|
||||
return interface{}(newSyncFileDbBolt(dbFilePath)).(SyncFileDb)
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
package syncdrive
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// PanSyncDbBolt 存储网盘文件信息的数据库
|
||||
PanSyncDbBolt struct {
|
||||
Path string
|
||||
db *BoltDb
|
||||
locker *sync.Mutex
|
||||
}
|
||||
|
||||
// LocalSyncDbBolt 存储本地文件信息的数据库
|
||||
LocalSyncDbBolt struct {
|
||||
Path string
|
||||
db *BoltDb
|
||||
locker *sync.Mutex
|
||||
}
|
||||
|
||||
// SyncFileDbBolt 存储同步文件状态信息的数据库
|
||||
SyncFileDbBolt struct {
|
||||
Path string
|
||||
db *BoltDb
|
||||
locker *sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
func newPanSyncDbBolt(dbFilePath string) *PanSyncDbBolt {
|
||||
return &PanSyncDbBolt{
|
||||
Path: dbFilePath,
|
||||
locker: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PanSyncDbBolt) Open() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Add 增加一个数据项
|
||||
func (p *PanSyncDbBolt) Add(item *PanFileItem) (bool, error) {
|
||||
if item == nil {
|
||||
return false, fmt.Errorf("item is nil")
|
||||
}
|
||||
p.locker.Lock()
|
||||
defer p.locker.Unlock()
|
||||
|
||||
p.db = NewBoltDb(p.Path)
|
||||
p.db.Open()
|
||||
defer p.db.Close()
|
||||
|
||||
data, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return p.db.Add(&BoltItem{
|
||||
FilePath: item.Path,
|
||||
IsFolder: item.IsFolder(),
|
||||
Data: string(data),
|
||||
})
|
||||
}
|
||||
|
||||
// AddFileList 增加批量数据项
|
||||
func (p *PanSyncDbBolt) AddFileList(items PanFileList) (bool, error) {
|
||||
if items == nil {
|
||||
return false, fmt.Errorf("item is nil")
|
||||
}
|
||||
p.locker.Lock()
|
||||
defer p.locker.Unlock()
|
||||
|
||||
p.db = NewBoltDb(p.Path)
|
||||
p.db.Open()
|
||||
defer p.db.Close()
|
||||
|
||||
boltItems := []*BoltItem{}
|
||||
for _, item := range items {
|
||||
data, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
boltItems = append(boltItems, &BoltItem{
|
||||
FilePath: item.Path,
|
||||
IsFolder: item.IsFolder(),
|
||||
Data: string(data),
|
||||
})
|
||||
}
|
||||
return p.db.AddItems(boltItems)
|
||||
}
|
||||
|
||||
// Get 获取一个数据项,数据项不存在返回错误
|
||||
func (p *PanSyncDbBolt) Get(filePath string) (*PanFileItem, error) {
|
||||
filePath = FormatFilePath(filePath)
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("item is nil")
|
||||
}
|
||||
p.locker.Lock()
|
||||
defer p.locker.Unlock()
|
||||
|
||||
p.db = NewBoltDb(p.Path)
|
||||
p.db.Open()
|
||||
defer p.db.Close()
|
||||
|
||||
data, err := p.db.Get(filePath)
|
||||
if err == nil && data != "" {
|
||||
item := &PanFileItem{}
|
||||
if err := json.Unmarshal([]byte(data), item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (p *PanSyncDbBolt) GetFileList(filePath string) (PanFileList, error) {
|
||||
filePath = FormatFilePath(filePath)
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("item is nil")
|
||||
}
|
||||
p.locker.Lock()
|
||||
defer p.locker.Unlock()
|
||||
|
||||
p.db = NewBoltDb(p.Path)
|
||||
p.db.Open()
|
||||
defer p.db.Close()
|
||||
|
||||
panFileList := PanFileList{}
|
||||
dataList, err := p.db.GetFileList(filePath)
|
||||
if err == nil && len(dataList) > 0 {
|
||||
for _, data := range dataList {
|
||||
if data == "" {
|
||||
continue
|
||||
}
|
||||
item := &PanFileItem{}
|
||||
if err := json.Unmarshal([]byte(data), item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
panFileList = append(panFileList, item)
|
||||
}
|
||||
return panFileList, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Delete 删除一个数据项
|
||||
func (p *PanSyncDbBolt) Delete(filePath string) (bool, error) {
|
||||
filePath = FormatFilePath(filePath)
|
||||
if filePath == "" {
|
||||
return false, fmt.Errorf("item is nil")
|
||||
}
|
||||
p.locker.Lock()
|
||||
defer p.locker.Unlock()
|
||||
|
||||
p.db = NewBoltDb(p.Path)
|
||||
p.db.Open()
|
||||
defer p.db.Close()
|
||||
|
||||
return p.db.Delete(filePath)
|
||||
}
|
||||
|
||||
// Update 更新数据项,数据项不存在返回错误
|
||||
func (p *PanSyncDbBolt) Update(item *PanFileItem) (bool, error) {
|
||||
if item == nil {
|
||||
return false, fmt.Errorf("item is nil")
|
||||
}
|
||||
p.locker.Lock()
|
||||
defer p.locker.Unlock()
|
||||
|
||||
p.db = NewBoltDb(p.Path)
|
||||
p.db.Open()
|
||||
defer p.db.Close()
|
||||
|
||||
data, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return p.db.Update(item.Path, string(data))
|
||||
}
|
||||
|
||||
// Close 关闭数据库
|
||||
func (p *PanSyncDbBolt) Close() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func newLocalSyncDbBolt(dbFilePath string) *LocalSyncDbBolt {
|
||||
return &LocalSyncDbBolt{
|
||||
Path: dbFilePath,
|
||||
locker: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *LocalSyncDbBolt) Open() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Add 增加一个数据项
|
||||
func (p *LocalSyncDbBolt) Add(item *LocalFileItem) (bool, error) {
|
||||
if item == nil {
|
||||
return false, fmt.Errorf("item is nil")
|
||||
}
|
||||
p.locker.Lock()
|
||||
defer p.locker.Unlock()
|
||||
|
||||
p.db = NewBoltDb(p.Path)
|
||||
p.db.Open()
|
||||
defer p.db.Close()
|
||||
|
||||
data, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return p.db.Add(&BoltItem{
|
||||
FilePath: item.Path,
|
||||
IsFolder: item.IsFolder(),
|
||||
Data: string(data),
|
||||
})
|
||||
}
|
||||
|
||||
// AddFileList 增加批量数据项
|
||||
func (p *LocalSyncDbBolt) AddFileList(items LocalFileList) (bool, error) {
|
||||
if items == nil {
|
||||
return false, fmt.Errorf("item is nil")
|
||||
}
|
||||
p.locker.Lock()
|
||||
defer p.locker.Unlock()
|
||||
|
||||
p.db = NewBoltDb(p.Path)
|
||||
p.db.Open()
|
||||
defer p.db.Close()
|
||||
|
||||
boltItems := []*BoltItem{}
|
||||
for _, item := range items {
|
||||
data, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
boltItems = append(boltItems, &BoltItem{
|
||||
FilePath: item.Path,
|
||||
IsFolder: item.IsFolder(),
|
||||
Data: string(data),
|
||||
})
|
||||
}
|
||||
return p.db.AddItems(boltItems)
|
||||
}
|
||||
|
||||
// Get 获取一个数据项,数据项不存在返回错误
|
||||
func (p *LocalSyncDbBolt) Get(filePath string) (*LocalFileItem, error) {
|
||||
filePath = FormatFilePath(filePath)
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("item is nil")
|
||||
}
|
||||
p.locker.Lock()
|
||||
defer p.locker.Unlock()
|
||||
|
||||
p.db = NewBoltDb(p.Path)
|
||||
p.db.Open()
|
||||
defer p.db.Close()
|
||||
|
||||
data, err := p.db.Get(filePath)
|
||||
if err == nil && data != "" {
|
||||
item := &LocalFileItem{}
|
||||
if err := json.Unmarshal([]byte(data), item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (p *LocalSyncDbBolt) GetFileList(filePath string) (LocalFileList, error) {
|
||||
filePath = FormatFilePath(filePath)
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("item is nil")
|
||||
}
|
||||
p.locker.Lock()
|
||||
defer p.locker.Unlock()
|
||||
|
||||
p.db = NewBoltDb(p.Path)
|
||||
p.db.Open()
|
||||
defer p.db.Close()
|
||||
|
||||
LocalFileList := LocalFileList{}
|
||||
dataList, err := p.db.GetFileList(filePath)
|
||||
if err == nil && len(dataList) > 0 {
|
||||
for _, data := range dataList {
|
||||
if data == "" {
|
||||
continue
|
||||
}
|
||||
item := &LocalFileItem{}
|
||||
if err := json.Unmarshal([]byte(data), item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
LocalFileList = append(LocalFileList, item)
|
||||
}
|
||||
return LocalFileList, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Delete 删除一个数据项
|
||||
func (p *LocalSyncDbBolt) Delete(filePath string) (bool, error) {
|
||||
filePath = FormatFilePath(filePath)
|
||||
if filePath == "" {
|
||||
return false, fmt.Errorf("item is nil")
|
||||
}
|
||||
p.locker.Lock()
|
||||
defer p.locker.Unlock()
|
||||
p.db = NewBoltDb(p.Path)
|
||||
p.db.Open()
|
||||
defer p.db.Close()
|
||||
return p.db.Delete(filePath)
|
||||
}
|
||||
|
||||
// Update 更新数据项,数据项不存在返回错误
|
||||
func (p *LocalSyncDbBolt) Update(item *LocalFileItem) (bool, error) {
|
||||
if item == nil {
|
||||
return false, fmt.Errorf("item is nil")
|
||||
}
|
||||
p.locker.Lock()
|
||||
defer p.locker.Unlock()
|
||||
|
||||
p.db = NewBoltDb(p.Path)
|
||||
p.db.Open()
|
||||
defer p.db.Close()
|
||||
|
||||
data, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return p.db.Update(item.Path, string(data))
|
||||
}
|
||||
|
||||
// Close 关闭数据库
|
||||
func (p *LocalSyncDbBolt) Close() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func newSyncFileDbBolt(dbFilePath string) *SyncFileDbBolt {
|
||||
return &SyncFileDbBolt{
|
||||
Path: dbFilePath,
|
||||
locker: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// Open 打开并准备数据库
|
||||
func (s *SyncFileDbBolt) Open() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Add 存储一个数据项
|
||||
func (s *SyncFileDbBolt) Add(item *SyncFileItem) (bool, error) {
|
||||
if item == nil {
|
||||
return false, fmt.Errorf("item is nil")
|
||||
}
|
||||
s.locker.Lock()
|
||||
defer s.locker.Unlock()
|
||||
|
||||
s.db = NewBoltDb(s.Path)
|
||||
s.db.Open()
|
||||
defer s.db.Close()
|
||||
|
||||
data, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return s.db.Add(&BoltItem{
|
||||
FilePath: "/" + item.Id(),
|
||||
IsFolder: false,
|
||||
Data: string(data),
|
||||
})
|
||||
}
|
||||
|
||||
// Get 获取一个数据项
|
||||
func (s *SyncFileDbBolt) Get(id string) (*SyncFileItem, error) {
|
||||
filePath := "/" + id
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("item is nil")
|
||||
}
|
||||
s.locker.Lock()
|
||||
defer s.locker.Unlock()
|
||||
|
||||
s.db = NewBoltDb(s.Path)
|
||||
s.db.Open()
|
||||
defer s.db.Close()
|
||||
|
||||
data, err := s.db.Get(filePath)
|
||||
if err == nil && data != "" {
|
||||
item := &SyncFileItem{}
|
||||
if err := json.Unmarshal([]byte(data), item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// GetFileList 获取文件夹下的所有的文件列表
|
||||
func (s *SyncFileDbBolt) GetFileList(Status SyncFileStatus) (SyncFileList, error) {
|
||||
filePath := "/"
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("item is nil")
|
||||
}
|
||||
s.locker.Lock()
|
||||
defer s.locker.Unlock()
|
||||
|
||||
s.db = NewBoltDb(s.Path)
|
||||
s.db.Open()
|
||||
defer s.db.Close()
|
||||
|
||||
panFileList := SyncFileList{}
|
||||
dataList, err := s.db.GetFileList(filePath)
|
||||
if err == nil && len(dataList) > 0 {
|
||||
for _, data := range dataList {
|
||||
if data == "" {
|
||||
continue
|
||||
}
|
||||
item := &SyncFileItem{}
|
||||
if err := json.Unmarshal([]byte(data), item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if item.Status == Status {
|
||||
panFileList = append(panFileList, item)
|
||||
}
|
||||
}
|
||||
return panFileList, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Delete 删除一个数据项,如果是文件夹,则会删除文件夹下面所有的文件列表
|
||||
func (s *SyncFileDbBolt) Delete(id string) (bool, error) {
|
||||
filePath := "/" + id
|
||||
if filePath == "" {
|
||||
return false, fmt.Errorf("item is nil")
|
||||
}
|
||||
s.locker.Lock()
|
||||
defer s.locker.Unlock()
|
||||
|
||||
s.db = NewBoltDb(s.Path)
|
||||
s.db.Open()
|
||||
defer s.db.Close()
|
||||
return s.db.Delete(filePath)
|
||||
}
|
||||
|
||||
// Update 更新一个数据项数据
|
||||
func (s *SyncFileDbBolt) Update(item *SyncFileItem) (bool, error) {
|
||||
if item == nil {
|
||||
return false, fmt.Errorf("item is nil")
|
||||
}
|
||||
s.locker.Lock()
|
||||
defer s.locker.Unlock()
|
||||
|
||||
s.db = NewBoltDb(s.Path)
|
||||
s.db.Open()
|
||||
defer s.db.Close()
|
||||
|
||||
data, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return s.db.Update("/"+item.Id(), string(data))
|
||||
}
|
||||
|
||||
// Close 关闭数据库
|
||||
func (s *SyncFileDbBolt) Close() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
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{})
|
||||
|
||||
// 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()
|
||||
defer b.Close()
|
||||
|
||||
fmt.Println(b.Get("/Parallels Desktop/v17/部分电脑安装v17可能有问题,请退回v16版本.txt"))
|
||||
}
|
||||
|
||||
func TestGetFileList(t *testing.T) {
|
||||
b := NewPanSyncDb("D:\\smb\\feny\\goprojects\\dev\\pan.db")
|
||||
b.Open()
|
||||
defer b.Close()
|
||||
|
||||
fmt.Println(b.GetFileList("/Parallels Desktop/v17"))
|
||||
}
|
||||
|
||||
func WalkAllFile(dirPath string, walkFn filepath.WalkFunc) error {
|
||||
dirPath = strings.ReplaceAll(dirPath, "\\", "/")
|
||||
info, err := os.Lstat(dirPath)
|
||||
if err != nil {
|
||||
err = walkFn(dirPath, nil, err)
|
||||
} else {
|
||||
err = walkAllFile(dirPath, info, walkFn)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func walkAllFile(dirPath string, info os.FileInfo, walkFn filepath.WalkFunc) error {
|
||||
if !info.IsDir() {
|
||||
return walkFn(dirPath, info, nil)
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return walkFn(dirPath, nil, err)
|
||||
}
|
||||
for _, fi := range files {
|
||||
subFilePath := dirPath + "/" + fi.Name()
|
||||
err = walkFn(subFilePath, fi, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
err = walkAllFile(subFilePath, fi, walkFn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestLocalSyncDb(t *testing.T) {
|
||||
b := NewLocalSyncDb("D:\\smb\\feny\\goprojects\\dev\\local.db")
|
||||
b.Open()
|
||||
defer b.Close()
|
||||
|
||||
var walkFunc filepath.WalkFunc
|
||||
walkFunc = func(file string, fi os.FileInfo, err error) error {
|
||||
fmt.Println(file)
|
||||
fileType := "file"
|
||||
if fi.IsDir() {
|
||||
fileType = "folder"
|
||||
}
|
||||
item := &LocalFileItem{
|
||||
FileName: path.Base(file),
|
||||
FileSize: fi.Size(),
|
||||
FileType: fileType,
|
||||
CreatedAt: fi.ModTime().Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: fi.ModTime().Format("2006-01-02 15:04:05"),
|
||||
FileExtension: "",
|
||||
Sha1Hash: "",
|
||||
Path: file,
|
||||
}
|
||||
if _, e := b.Add(item); e != nil {
|
||||
fmt.Println(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
WalkAllFile("D:\\smb\\feny\\goprojects\\dl\\a761171495", walkFunc)
|
||||
}
|
||||
|
||||
func TestLocalSyncDbAddFileList(t *testing.T) {
|
||||
b := NewLocalSyncDb("D:\\smb\\feny\\goprojects\\dev\\tmp.db")
|
||||
b.Open()
|
||||
defer b.Close()
|
||||
|
||||
fl := LocalFileList{}
|
||||
fl = append(fl, &LocalFileItem{
|
||||
FileName: "aliyunpan_command_history.txt",
|
||||
FileSize: 1542,
|
||||
FileType: "file",
|
||||
CreatedAt: "2020-12-12 12:51:12",
|
||||
UpdatedAt: "2020-12-12 12:51:12",
|
||||
FileExtension: "",
|
||||
Sha1Hash: "",
|
||||
Path: "D:\\smb\\feny\\goprojects\\dev\\aliyunpan_command_history.txt",
|
||||
})
|
||||
fl = append(fl, &LocalFileItem{
|
||||
FileName: "f1.txt",
|
||||
FileSize: 1542,
|
||||
FileType: "file",
|
||||
CreatedAt: "2020-12-12 12:51:12",
|
||||
UpdatedAt: "2020-12-12 12:51:12",
|
||||
FileExtension: "",
|
||||
Sha1Hash: "",
|
||||
Path: "D:\\smb\\feny\\goprojects\\dev\\f1.txt",
|
||||
})
|
||||
fl = append(fl, &LocalFileItem{
|
||||
FileName: "f2.txt",
|
||||
FileSize: 1542,
|
||||
FileType: "file",
|
||||
CreatedAt: "2020-12-12 12:51:12",
|
||||
UpdatedAt: "2020-12-12 12:51:12",
|
||||
FileExtension: "",
|
||||
Sha1Hash: "",
|
||||
Path: "D:\\smb\\feny\\goprojects\\dev\\f2.txt",
|
||||
})
|
||||
fl = append(fl, &LocalFileItem{
|
||||
FileName: "f1.txt",
|
||||
FileSize: 1542,
|
||||
FileType: "file",
|
||||
CreatedAt: "2020-12-12 12:51:12",
|
||||
UpdatedAt: "2020-12-12 12:51:12",
|
||||
FileExtension: "",
|
||||
Sha1Hash: "",
|
||||
Path: "D:\\smb\\feny\\goprojects\\dev\\fo\\f1.txt",
|
||||
})
|
||||
fmt.Println(b.AddFileList(fl))
|
||||
}
|
||||
|
||||
func TestLocalGet(t *testing.T) {
|
||||
b := NewLocalSyncDb("/Volumes/Downloads/dev/sync_drive/840f28af799747848c0b3155e0bdfeab/local.bolt")
|
||||
b.Open()
|
||||
defer b.Close()
|
||||
v, _ := b.Get("/Volumes/Downloads/dev/upload/未命名文件夹/[HAIDAN.VIDEO].绣春刀.2014.mp4.torrent")
|
||||
fmt.Println(utils.ObjectToJsonStr(v, true))
|
||||
}
|
||||
|
||||
func TestLocalGetFileList(t *testing.T) {
|
||||
b := NewLocalSyncDb("D:\\smb\\feny\\goprojects\\dev\\local.db")
|
||||
b.Open()
|
||||
defer b.Close()
|
||||
|
||||
fmt.Println(b.GetFileList("D:/smb/feny/goprojects/dl/a761171495"))
|
||||
}
|
||||
|
||||
func TestSyncDbAdd(t *testing.T) {
|
||||
b := NewSyncFileDb("D:\\smb\\feny\\goprojects\\dev\\sync_drive\\sync.db")
|
||||
b.Open()
|
||||
defer b.Close()
|
||||
|
||||
b.Add(&SyncFileItem{
|
||||
Action: SyncFileActionDownload,
|
||||
Status: SyncFileStatusCreate,
|
||||
LocalFile: nil,
|
||||
PanFile: &PanFileItem{
|
||||
Category: "others",
|
||||
Crc64Hash: "16173291050517323365",
|
||||
CreatedAt: "2021-07-09 23:17:52",
|
||||
DomainId: "bj29",
|
||||
DriveId: "19519221",
|
||||
FileExtension: "apk",
|
||||
FileId: "60e868a0a005315fe2b149b4ade47b2df8bdccee",
|
||||
FileName: "10000996@yunpan-release.apk",
|
||||
FileSize: 61029089,
|
||||
FileType: "file",
|
||||
ParentFileId: "60f29e3ac420ae8c08e645db8b80b881e2a3633c",
|
||||
Path: "/sync_drive/10000996@yunpan-release.apk",
|
||||
Sha1Hash: "05A7597967C3C947D84564A7D55309DEA56ED985",
|
||||
UpdatedAt: "2021-07-21 17:05:49",
|
||||
UploadId: "rapid-9141d56d-21c3-4fcc-b41d-5df7bf47540f",
|
||||
},
|
||||
StatusUpdateTime: "",
|
||||
})
|
||||
b.Add(&SyncFileItem{
|
||||
Action: SyncFileActionDownload,
|
||||
Status: SyncFileStatusCreate,
|
||||
LocalFile: nil,
|
||||
PanFile: &PanFileItem{
|
||||
Category: "others",
|
||||
Crc64Hash: "16173291050517323365",
|
||||
CreatedAt: "2021-07-09 23:17:52",
|
||||
DomainId: "bj29",
|
||||
DriveId: "19519221",
|
||||
FileExtension: "apk",
|
||||
FileId: "60e868a0a005315fe2b149b4ade47b2df8bdccee",
|
||||
FileName: "10000996@yunpan-release-1.apk",
|
||||
FileSize: 61029089,
|
||||
FileType: "file",
|
||||
ParentFileId: "60f29e3ac420ae8c08e645db8b80b881e2a3633c",
|
||||
Path: "/sync_drive/10000996@yunpan-release-1.apk",
|
||||
Sha1Hash: "05A7597967C3C947D84564A7D55309DEA56ED985",
|
||||
UpdatedAt: "2021-07-21 17:05:49",
|
||||
UploadId: "rapid-9141d56d-21c3-4fcc-b41d-5df7bf47540f",
|
||||
},
|
||||
StatusUpdateTime: "",
|
||||
})
|
||||
b.Add(&SyncFileItem{
|
||||
Action: SyncFileActionUpload,
|
||||
Status: SyncFileStatusCreate,
|
||||
LocalFile: &LocalFileItem{
|
||||
FileName: "f1.txt",
|
||||
FileSize: 1542,
|
||||
FileType: "file",
|
||||
CreatedAt: "2020-12-12 12:51:12",
|
||||
UpdatedAt: "2020-12-12 12:51:12",
|
||||
FileExtension: ".txt",
|
||||
Sha1Hash: "",
|
||||
Path: "D:\\smb\\feny\\goprojects\\dev\\fo\\f1.txt",
|
||||
},
|
||||
PanFile: nil,
|
||||
StatusUpdateTime: "",
|
||||
})
|
||||
//b.AddUnique(&SyncFileItem{
|
||||
// Action: SyncFileActionDownload,
|
||||
// Status: SyncFileStatusCreate,
|
||||
// LocalFile: nil,
|
||||
// PanFile: &PanFileItem{
|
||||
// Category: "others",
|
||||
// Crc64Hash: "16173291050517323365",
|
||||
// CreatedAt: "2021-07-09 23:17:52",
|
||||
// DomainId: "bj29",
|
||||
// DriveId: "19519221",
|
||||
// FileExtension: "apk",
|
||||
// FileId: "60e868a0a005315fe2b149b4ade47b2df8bdccee",
|
||||
// FileName: "10000996@yunpan-release-1.apk",
|
||||
// FileSize: 61029089,
|
||||
// FileType: "file",
|
||||
// ParentFileId: "60f29e3ac420ae8c08e645db8b80b881e2a3633c",
|
||||
// Path: "/sync_drive/10000996@yunpan-release-1.apk",
|
||||
// Sha1Hash: "05A7597967C3C947D84564A7D55309DEA56ED985",
|
||||
// UpdatedAt: "2021-07-21 17:05:49",
|
||||
// UploadId: "rapid-9141d56d-21c3-4fcc-b41d-5df7bf47540f",
|
||||
// },
|
||||
// StatusUpdateTime: "",
|
||||
//})
|
||||
//b.AddUnique(&SyncFileItem{
|
||||
// Action: SyncFileActionDownload,
|
||||
// Status: SyncFileStatusCreate,
|
||||
// LocalFile: nil,
|
||||
// PanFile: &PanFileItem{
|
||||
// Category: "others",
|
||||
// Crc64Hash: "16173291050517323365",
|
||||
// CreatedAt: "2021-07-09 23:17:52",
|
||||
// DomainId: "bj29",
|
||||
// DriveId: "19519221",
|
||||
// FileExtension: "apk",
|
||||
// FileId: "60e868a0a005315fe2b149b4ade47b2df8bdccee",
|
||||
// FileName: "10000996@yunpan-release-2.apk",
|
||||
// FileSize: 61029089,
|
||||
// FileType: "file",
|
||||
// ParentFileId: "60f29e3ac420ae8c08e645db8b80b881e2a3633c",
|
||||
// Path: "/sync_drive/10000996@yunpan-release-2.apk",
|
||||
// Sha1Hash: "05A7597967C3C947D84564A7D55309DEA56ED985",
|
||||
// UpdatedAt: "2021-07-21 17:05:49",
|
||||
// UploadId: "rapid-9141d56d-21c3-4fcc-b41d-5df7bf47540f",
|
||||
// },
|
||||
// StatusUpdateTime: "",
|
||||
//})
|
||||
}
|
||||
|
||||
func TestSyncDbClear(t *testing.T) {
|
||||
b := NewSyncFileDb("D:\\smb\\feny\\goprojects\\dev\\sync.bolt")
|
||||
b.Open()
|
||||
defer b.Close()
|
||||
|
||||
files, _ := b.GetFileList(SyncFileStatusSuccess)
|
||||
for _, file := range files {
|
||||
b.Delete(file.Id())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package syncdrive
|
||||
|
||||
import (
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FormatFilePath 格式化文件路径
|
||||
func FormatFilePath(filePath string) string {
|
||||
if filePath == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 是否是windows路径
|
||||
matched, _ := regexp.MatchString("^([a-zA-Z]:)", filePath)
|
||||
if matched {
|
||||
// 去掉卷标签,例如:D:
|
||||
filePath = string([]rune(filePath)[2:])
|
||||
}
|
||||
filePath = strings.ReplaceAll(filePath, "\\", "/")
|
||||
return path.Clean(filePath)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package syncdrive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatFilePath(t *testing.T) {
|
||||
fmt.Println(FormatFilePath("D:\\-beyond\\p\\9168473.html"))
|
||||
}
|
||||
|
||||
func TestFormatFilePath2(t *testing.T) {
|
||||
fmt.Println(FormatFilePath("/my/folder/test.txt"))
|
||||
}
|
||||
@@ -0,0 +1,711 @@
|
||||
package syncdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/internal/waitgroup"
|
||||
"github.com/tickstep/aliyunpan/library/collection"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
TaskStep string
|
||||
SyncMode string
|
||||
|
||||
// SyncTask 同步任务
|
||||
SyncTask struct {
|
||||
// Name 任务名称
|
||||
Name string `json:"name"`
|
||||
// Id 任务ID
|
||||
Id string `json:"id"`
|
||||
// DriveId 网盘ID,目前支持文件网盘
|
||||
DriveId string `json:"-"`
|
||||
// LocalFolderPath 本地目录
|
||||
LocalFolderPath string `json:"localFolderPath"`
|
||||
// PanFolderPath 云盘目录
|
||||
PanFolderPath string `json:"panFolderPath"`
|
||||
// Mode 同步模式
|
||||
Mode SyncMode `json:"mode"`
|
||||
// Priority 优先级选项
|
||||
Priority SyncPriorityOption `json:"priority"`
|
||||
// LastSyncTime 上一次同步时间
|
||||
LastSyncTime string `json:"lastSyncTime"`
|
||||
|
||||
syncDbFolderPath string
|
||||
localFileDb LocalSyncDb
|
||||
panFileDb PanSyncDb
|
||||
syncFileDb SyncFileDb
|
||||
|
||||
wg *waitgroup.WaitGroup
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
|
||||
panUser *config.PanUser
|
||||
panClient *aliyunpan.PanClient
|
||||
|
||||
syncOption SyncOption
|
||||
|
||||
fileActionTaskManager *FileActionTaskManager
|
||||
|
||||
plugin plugins.Plugin
|
||||
pluginMutex *sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// UploadOnly 单向上传,即备份本地文件到云盘
|
||||
UploadOnly SyncMode = "upload"
|
||||
// DownloadOnly 只下载,即备份云盘文件到本地
|
||||
DownloadOnly SyncMode = "download"
|
||||
// SyncTwoWay 双向同步,本地和云盘文件完全保持一致
|
||||
SyncTwoWay SyncMode = "sync"
|
||||
|
||||
// StepScanFile 任务步骤,扫描文件建立同步数据库
|
||||
StepScanFile TaskStep = "scan"
|
||||
// StepDiffFile 任务步骤,对比文件
|
||||
StepDiffFile TaskStep = "diff"
|
||||
// StepSyncFile 任务步骤,同步文件
|
||||
StepSyncFile TaskStep = "sync"
|
||||
)
|
||||
|
||||
func (t *SyncTask) NameLabel() string {
|
||||
return t.Name + "(" + t.Id + ")"
|
||||
}
|
||||
|
||||
func (t *SyncTask) String() string {
|
||||
builder := &strings.Builder{}
|
||||
builder.WriteString("任务: " + t.NameLabel() + "\n")
|
||||
mode := "双向备份"
|
||||
if t.Mode == UploadOnly {
|
||||
mode = "备份本地文件(只上传)"
|
||||
}
|
||||
if t.Mode == DownloadOnly {
|
||||
mode = "备份云盘文件(只下载)"
|
||||
}
|
||||
builder.WriteString("同步模式: " + mode + "\n")
|
||||
if t.Mode == SyncTwoWay {
|
||||
priority := "时间优先"
|
||||
if t.syncOption.SyncPriority == SyncPriorityLocalFirst {
|
||||
priority = "本地文件优先"
|
||||
} else if t.syncOption.SyncPriority == SyncPriorityPanFirst {
|
||||
priority = "网盘文件优先"
|
||||
} else {
|
||||
priority = "时间优先"
|
||||
}
|
||||
builder.WriteString("优先选项: " + priority + "\n")
|
||||
}
|
||||
builder.WriteString("本地目录: " + t.LocalFolderPath + "\n")
|
||||
builder.WriteString("云盘目录: " + t.PanFolderPath + "\n")
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func (t *SyncTask) setupDb() error {
|
||||
t.localFileDb = NewLocalSyncDb(t.localSyncDbFullPath())
|
||||
t.panFileDb = NewPanSyncDb(t.panSyncDbFullPath())
|
||||
t.syncFileDb = NewSyncFileDb(t.syncFileDbFullPath())
|
||||
if _, e := t.localFileDb.Open(); e != nil {
|
||||
return e
|
||||
}
|
||||
if _, e := t.panFileDb.Open(); e != nil {
|
||||
return e
|
||||
}
|
||||
if _, e := t.syncFileDb.Open(); e != nil {
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start 启动同步任务
|
||||
// 扫描本地和云盘文件信息并存储到本地数据库
|
||||
func (t *SyncTask) Start(step TaskStep) error {
|
||||
if t.ctx != nil {
|
||||
return fmt.Errorf("task have starting")
|
||||
}
|
||||
|
||||
// 同步模式下,如果本地磁盘有问题,会导致云盘备份删除文件,需要验证本地磁盘可靠性以避免误删云盘文件
|
||||
if t.Mode == SyncTwoWay {
|
||||
// check local disk unplug issue
|
||||
fi, err := os.Stat(t.localSyncDbFullPath())
|
||||
if err == nil && fi.Size() > 0 { // local db existed
|
||||
// check local sync dir state
|
||||
if b, e := utils.PathExists(t.LocalFolderPath); e == nil {
|
||||
if !b {
|
||||
// maybe the local disk unplug, check
|
||||
return fmt.Errorf("异常:本地同步目录不存在,本任务已经停止。如需继续同步请手动删除同步数据库再重试。")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check root dir & init
|
||||
if b, e := utils.PathExists(t.LocalFolderPath); e == nil {
|
||||
if !b {
|
||||
// create local root folder
|
||||
os.MkdirAll(t.LocalFolderPath, 0755)
|
||||
}
|
||||
}
|
||||
if _, er := t.panClient.FileInfoByPath(t.DriveId, t.PanFolderPath); er != nil {
|
||||
if er.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
t.panClient.MkdirByFullPath(t.DriveId, t.PanFolderPath)
|
||||
}
|
||||
}
|
||||
|
||||
// setup sync db file
|
||||
t.setupDb()
|
||||
|
||||
if t.fileActionTaskManager == nil {
|
||||
t.fileActionTaskManager = NewFileActionTaskManager(t)
|
||||
}
|
||||
|
||||
if t.plugin == nil {
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
t.plugin, _ = pluginManger.GetPlugin()
|
||||
}
|
||||
if t.pluginMutex == nil {
|
||||
t.pluginMutex = &sync.Mutex{}
|
||||
}
|
||||
|
||||
t.wg = waitgroup.NewWaitGroup(0)
|
||||
|
||||
var cancel context.CancelFunc
|
||||
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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitToStop 等待异步任务执行完成后停止
|
||||
func (t *SyncTask) WaitToStop() error {
|
||||
// wait for finished
|
||||
t.wg.Wait()
|
||||
|
||||
t.Stop()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop 停止同步任务
|
||||
func (t *SyncTask) Stop() error {
|
||||
if t.ctx == nil {
|
||||
return nil
|
||||
}
|
||||
// cancel all sub task & process
|
||||
t.cancelFunc()
|
||||
|
||||
// wait for finished
|
||||
t.wg.Wait()
|
||||
|
||||
t.ctx = nil
|
||||
t.cancelFunc = nil
|
||||
|
||||
// stop file action task (block routine)
|
||||
t.fileActionTaskManager.Stop()
|
||||
|
||||
// release resources
|
||||
if t.localFileDb != nil {
|
||||
t.localFileDb.Close()
|
||||
}
|
||||
if t.panFileDb != nil {
|
||||
t.panFileDb.Close()
|
||||
}
|
||||
if t.syncFileDb != nil {
|
||||
t.syncFileDb.Close()
|
||||
}
|
||||
|
||||
// record the sync time
|
||||
t.LastSyncTime = utils.NowTimeStr()
|
||||
return nil
|
||||
}
|
||||
|
||||
// panSyncDbFullPath 云盘文件数据库
|
||||
func (t *SyncTask) panSyncDbFullPath() string {
|
||||
dir := path.Join(t.syncDbFolderPath, t.Id)
|
||||
if b, _ := utils.PathExists(dir); !b {
|
||||
os.MkdirAll(dir, 0755)
|
||||
}
|
||||
return path.Join(dir, "pan.bolt")
|
||||
}
|
||||
|
||||
// localSyncDbFullPath 本地文件数据库
|
||||
func (t *SyncTask) localSyncDbFullPath() string {
|
||||
dir := path.Join(t.syncDbFolderPath, t.Id)
|
||||
if b, _ := utils.PathExists(dir); !b {
|
||||
os.MkdirAll(dir, 0755)
|
||||
}
|
||||
return path.Join(dir, "local.bolt")
|
||||
}
|
||||
|
||||
// syncFileDbFullPath 同步文件过程数据库
|
||||
func (t *SyncTask) syncFileDbFullPath() string {
|
||||
dir := path.Join(t.syncDbFolderPath, t.Id)
|
||||
if b, _ := utils.PathExists(dir); !b {
|
||||
os.MkdirAll(dir, 0755)
|
||||
}
|
||||
return path.Join(dir, "sync.bolt")
|
||||
}
|
||||
|
||||
func newLocalFileItem(file os.FileInfo, fullPath string) *LocalFileItem {
|
||||
ft := "file"
|
||||
if file.IsDir() {
|
||||
ft = "folder"
|
||||
}
|
||||
return &LocalFileItem{
|
||||
FileName: file.Name(),
|
||||
FileSize: file.Size(),
|
||||
FileType: ft,
|
||||
CreatedAt: file.ModTime().Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: file.ModTime().Format("2006-01-02 15:04:05"),
|
||||
FileExtension: path.Ext(file.Name()),
|
||||
Sha1Hash: "",
|
||||
Path: fullPath,
|
||||
ScanTimeAt: utils.NowTimeStr(),
|
||||
ScanStatus: ScanStatusNormal,
|
||||
}
|
||||
}
|
||||
|
||||
// discardLocalFileDb 清理本地数据库中无效的数据项
|
||||
func (t *SyncTask) discardLocalFileDb(filePath string, startTimeUnix int64) bool {
|
||||
files, e := t.localFileDb.GetFileList(filePath)
|
||||
r := false
|
||||
if e != nil {
|
||||
return r
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.ScanTimeAt == "" || file.ScanTimeUnix() < startTimeUnix {
|
||||
if t.Mode == DownloadOnly {
|
||||
// delete discard local file info directly
|
||||
t.localFileDb.Delete(file.Path)
|
||||
logger.Verboseln("label discard local file from DB: ", utils.ObjectToJsonStr(file, false))
|
||||
} else {
|
||||
// label file discard
|
||||
file.ScanStatus = ScanStatusDiscard
|
||||
t.localFileDb.Update(file)
|
||||
logger.Verboseln("label local file discard: ", utils.ObjectToJsonStr(file, false))
|
||||
}
|
||||
r = true
|
||||
} else {
|
||||
if file.IsFolder() {
|
||||
b := t.discardLocalFileDb(file.Path, startTimeUnix)
|
||||
if b {
|
||||
r = b
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (t *SyncTask) skipLocalFile(file *LocalFileItem) bool {
|
||||
// 插件回调
|
||||
pluginParam := &plugins.SyncScanLocalFilePrepareParams{
|
||||
LocalFilePath: file.Path,
|
||||
LocalFileName: file.FileName,
|
||||
LocalFileSize: file.FileSize,
|
||||
LocalFileType: file.FileType,
|
||||
LocalFileUpdatedAt: file.UpdatedAt,
|
||||
DriveId: t.DriveId,
|
||||
}
|
||||
t.pluginMutex.Lock()
|
||||
defer t.pluginMutex.Unlock()
|
||||
if result, er := t.plugin.SyncScanLocalFilePrepareCallback(plugins.GetContext(t.panUser), pluginParam); er == nil && result != nil {
|
||||
if strings.Compare("no", result.SyncScanLocalApproved) == 0 {
|
||||
// skip this file
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// scanLocalFile 本地文件循环扫描进程
|
||||
func (t *SyncTask) scanLocalFile(ctx context.Context, scanFileOnly bool) {
|
||||
t.wg.AddDelta()
|
||||
defer t.wg.Done()
|
||||
|
||||
type folderItem struct {
|
||||
fileInfo os.FileInfo
|
||||
path string
|
||||
}
|
||||
|
||||
// init the root folders info
|
||||
pathParts := strings.Split(strings.ReplaceAll(t.LocalFolderPath, "\\", "/"), "/")
|
||||
fullPath := ""
|
||||
for _, p := range pathParts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(p, ":") {
|
||||
// windows volume label, e.g: C:/ D:/
|
||||
fullPath += p
|
||||
continue
|
||||
}
|
||||
fullPath += "/" + p
|
||||
fi, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
// may be permission deny
|
||||
continue
|
||||
}
|
||||
t.localFileDb.Add(newLocalFileItem(fi, fullPath))
|
||||
}
|
||||
|
||||
folderQueue := collection.NewFifoQueue()
|
||||
rootFolder, err := os.Stat(t.LocalFolderPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
folderQueue.Push(&folderItem{
|
||||
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")
|
||||
return
|
||||
default:
|
||||
// 采用广度优先遍历(BFS)进行文件遍历
|
||||
if delayTimeCount > 0 {
|
||||
time.Sleep(1 * time.Second)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 // 重置标记
|
||||
}
|
||||
|
||||
if scanFileOnly {
|
||||
// 全盘扫描文件一次,建立好文件同步数据库,然后退出
|
||||
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 // 重置标记
|
||||
}
|
||||
continue
|
||||
}
|
||||
item := obj.(*folderItem)
|
||||
files, err1 := ioutil.ReadDir(item.path)
|
||||
if err1 != nil {
|
||||
continue
|
||||
}
|
||||
if len(files) == 0 {
|
||||
continue
|
||||
}
|
||||
localFileAppendList := LocalFileList{}
|
||||
for _, file := range files {
|
||||
if strings.HasSuffix(file.Name(), DownloadingFileSuffix) {
|
||||
// 下载中文件,跳过
|
||||
continue
|
||||
}
|
||||
|
||||
localFile := newLocalFileItem(file, item.path+"/"+file.Name())
|
||||
if t.skipLocalFile(localFile) {
|
||||
fmt.Println("插件禁止扫描本地文件: ", localFile.Path)
|
||||
continue
|
||||
}
|
||||
|
||||
if scanFileOnly {
|
||||
fmt.Println("扫描本地文件:" + item.path + "/" + file.Name())
|
||||
}
|
||||
|
||||
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
|
||||
if file.IsDir() {
|
||||
folderQueue.Push(&folderItem{
|
||||
fileInfo: file,
|
||||
path: item.path + "/" + file.Name(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// discardPanFileDb 清理云盘数据库中无效的数据项
|
||||
func (t *SyncTask) discardPanFileDb(filePath string, startTimeUnix int64) bool {
|
||||
files, e := t.panFileDb.GetFileList(filePath)
|
||||
r := false
|
||||
if e != nil {
|
||||
return r
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.ScanTimeUnix() < startTimeUnix {
|
||||
if t.Mode == UploadOnly {
|
||||
// delete discard pan file info directly
|
||||
t.panFileDb.Delete(file.Path)
|
||||
logger.Verboseln("delete discard pan file from DB: ", utils.ObjectToJsonStr(file, false))
|
||||
} else {
|
||||
// label file discard
|
||||
file.ScanStatus = ScanStatusDiscard
|
||||
t.panFileDb.Update(file)
|
||||
logger.Verboseln("label pan file discard: ", utils.ObjectToJsonStr(file, false))
|
||||
}
|
||||
r = true
|
||||
} else {
|
||||
if file.IsFolder() {
|
||||
b := t.discardPanFileDb(file.Path, startTimeUnix)
|
||||
if b {
|
||||
r = b
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (t *SyncTask) skipPanFile(file *PanFileItem) bool {
|
||||
// 插件回调
|
||||
pluginParam := &plugins.SyncScanPanFilePrepareParams{
|
||||
DriveId: file.DriveId,
|
||||
DriveFileName: file.FileName,
|
||||
DriveFilePath: file.Path,
|
||||
DriveFileSha1: file.Sha1Hash,
|
||||
DriveFileSize: file.FileSize,
|
||||
DriveFileType: file.FileType,
|
||||
DriveFileUpdatedAt: file.UpdatedAt,
|
||||
}
|
||||
t.pluginMutex.Lock()
|
||||
defer t.pluginMutex.Unlock()
|
||||
if result, er := t.plugin.SyncScanPanFilePrepareCallback(plugins.GetContext(t.panUser), pluginParam); er == nil && result != nil {
|
||||
if strings.Compare("no", result.SyncScanPanApproved) == 0 {
|
||||
// skip this file
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// scanPanFile 云盘文件循环扫描进程
|
||||
func (t *SyncTask) scanPanFile(ctx context.Context, scanFileOnly bool) {
|
||||
t.wg.AddDelta()
|
||||
defer t.wg.Done()
|
||||
|
||||
// init the root folders info
|
||||
pathParts := strings.Split(strings.ReplaceAll(t.PanFolderPath, "\\", "/"), "/")
|
||||
fullPath := ""
|
||||
for _, p := range pathParts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
fullPath += "/" + p
|
||||
}
|
||||
fi, err := t.panClient.FileInfoByPath(t.DriveId, fullPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pFile := NewPanFileItem(fi)
|
||||
pFile.ScanTimeAt = utils.NowTimeStr()
|
||||
t.panFileDb.Add(pFile)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
folderQueue := collection.NewFifoQueue()
|
||||
rootPanFile := fi
|
||||
folderQueue.Push(rootPanFile)
|
||||
startTimeOfThisLoop := time.Now().Unix()
|
||||
delayTimeCount := int64(0)
|
||||
isPanFolderModify := false
|
||||
|
||||
// 触发一次文件对比任务
|
||||
t.fileActionTaskManager.AddPanFolderModifyCount()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// cancel routine & done
|
||||
logger.Verboseln("pan file routine done")
|
||||
return
|
||||
default:
|
||||
// 采用广度优先遍历(BFS)进行文件遍历
|
||||
if delayTimeCount > 0 {
|
||||
time.Sleep(1 * time.Second)
|
||||
delayTimeCount -= 1
|
||||
continue
|
||||
} else if delayTimeCount == 0 {
|
||||
delayTimeCount -= 1
|
||||
startTimeOfThisLoop = time.Now().Unix()
|
||||
logger.Verboseln("do scan pan file process at ", utils.NowTimeStr())
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
if scanFileOnly {
|
||||
// 全盘扫描文件一次,建立好文件同步数据库,然后退出
|
||||
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
|
||||
}
|
||||
continue
|
||||
}
|
||||
item := obj.(*aliyunpan.FileEntity)
|
||||
files, err1 := t.panClient.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{}
|
||||
for _, file := range files {
|
||||
file.Path = path.Join(item.Path, file.FileName)
|
||||
panFile := NewPanFileItem(file)
|
||||
if t.skipPanFile(panFile) {
|
||||
logger.Verboseln("插件禁止扫描云盘文件: ", 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
time.Sleep(5 * time.Second) // 延迟避免触发风控
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package syncdrive
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
// SyncOption 同步选项
|
||||
SyncOption struct {
|
||||
FileDownloadParallel int // 文件下载并发数
|
||||
FileUploadParallel int // 文件上传并发数
|
||||
FileDownloadBlockSize int64 // 文件下载分片大小
|
||||
FileUploadBlockSize int64 // 文件上传分片大小
|
||||
UseInternalUrl bool // 是否使用阿里ECS内部链接
|
||||
|
||||
MaxDownloadRate int64 // 限制最大下载速度
|
||||
MaxUploadRate int64 // 限制最大上传速度
|
||||
|
||||
// 优先级选项
|
||||
SyncPriority SyncPriorityOption
|
||||
|
||||
// 本地文件修改检测间隔
|
||||
LocalFileModifiedCheckIntervalSec int
|
||||
}
|
||||
|
||||
// SyncTaskManager 同步任务管理器
|
||||
SyncTaskManager struct {
|
||||
syncDriveConfig *SyncDriveConfig
|
||||
syncOption SyncOption
|
||||
PanUser *config.PanUser
|
||||
DriveId string
|
||||
PanClient *aliyunpan.PanClient
|
||||
SyncConfigFolderPath string
|
||||
|
||||
// useConfigFile 是否使用配置文件启动
|
||||
useConfigFile bool
|
||||
}
|
||||
|
||||
// SyncDriveConfig 同步盘配置文件
|
||||
SyncDriveConfig struct {
|
||||
ConfigVer string `json:"configVer"`
|
||||
SyncTaskList []*SyncTask `json:"syncTaskList"`
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSyncTaskListEmpty error = fmt.Errorf("no sync task")
|
||||
)
|
||||
|
||||
func NewSyncTaskManager(user *config.PanUser, driveId string, panClient *aliyunpan.PanClient, syncConfigFolderPath string,
|
||||
option SyncOption) *SyncTaskManager {
|
||||
return &SyncTaskManager{
|
||||
PanUser: user,
|
||||
DriveId: driveId,
|
||||
PanClient: panClient,
|
||||
SyncConfigFolderPath: syncConfigFolderPath,
|
||||
syncOption: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SyncTaskManager) parseConfigFile() error {
|
||||
/** 样例
|
||||
{
|
||||
"configVer": "1.0",
|
||||
"syncTaskList": [
|
||||
{
|
||||
"name": "NS游戏备份",
|
||||
"id": "5b2d7c10-e927-4e72-8f9d-5abb3bb04814",
|
||||
"driveId": "19519111",
|
||||
"localFolderPath": "D:\\smb\\datadisk\\game",
|
||||
"panFolderPath": "/sync_drive/game",
|
||||
"mode": "sync",
|
||||
"lastSyncTime": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
configFilePath := m.ConfigFilePath()
|
||||
r := &SyncDriveConfig{
|
||||
ConfigVer: "1.0",
|
||||
SyncTaskList: []*SyncTask{},
|
||||
}
|
||||
m.syncDriveConfig = r
|
||||
|
||||
if b, _ := utils.PathExists(configFilePath); b != true {
|
||||
//text := utils.ObjectToJsonStr(r, true)
|
||||
//ioutil.WriteFile(ConfigFilePath, []byte(text), 0755)
|
||||
return fmt.Errorf("备份配置文件不存在:" + m.ConfigFilePath())
|
||||
}
|
||||
data, e := ioutil.ReadFile(configFilePath)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
if len(data) > 0 {
|
||||
if err2 := json.Unmarshal(data, m.syncDriveConfig); err2 != nil {
|
||||
logger.Verboseln("parse sync drive config json error ", err2)
|
||||
return err2
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *SyncTaskManager) ConfigFilePath() string {
|
||||
return path.Join(m.SyncConfigFolderPath, "sync_drive_config.json")
|
||||
}
|
||||
|
||||
// Start 启动同步进程
|
||||
func (m *SyncTaskManager) Start(tasks []*SyncTask, step TaskStep) (bool, error) {
|
||||
if tasks != nil && len(tasks) > 0 {
|
||||
m.syncDriveConfig = &SyncDriveConfig{
|
||||
ConfigVer: "1.0",
|
||||
SyncTaskList: []*SyncTask{},
|
||||
}
|
||||
m.syncDriveConfig.SyncTaskList = tasks
|
||||
m.useConfigFile = false
|
||||
} else {
|
||||
if er := m.parseConfigFile(); er != nil {
|
||||
return false, er
|
||||
}
|
||||
m.useConfigFile = true
|
||||
}
|
||||
if m.syncDriveConfig.SyncTaskList == nil || len(m.syncDriveConfig.SyncTaskList) == 0 {
|
||||
return false, ErrSyncTaskListEmpty
|
||||
}
|
||||
|
||||
// start the sync task one by one
|
||||
for _, task := range m.syncDriveConfig.SyncTaskList {
|
||||
if len(task.Id) == 0 {
|
||||
task.Id = utils.UuidStr()
|
||||
}
|
||||
// check pan path
|
||||
if !utils.IsPanAbsPath(task.PanFolderPath) {
|
||||
task.PanFolderPath = "/" + task.PanFolderPath
|
||||
}
|
||||
|
||||
// check local path
|
||||
if !utils.IsLocalAbsPath(task.LocalFolderPath) {
|
||||
fmt.Println("任务启动失败,本地路径不是绝对路径: ", task.LocalFolderPath)
|
||||
continue
|
||||
}
|
||||
task.panUser = m.PanUser
|
||||
task.DriveId = m.DriveId
|
||||
task.syncDbFolderPath = m.SyncConfigFolderPath
|
||||
task.panClient = m.PanClient
|
||||
task.syncOption = m.syncOption
|
||||
if task.Priority != "" {
|
||||
task.syncOption.SyncPriority = task.Priority
|
||||
} else {
|
||||
task.Priority = SyncPriorityTimestampFirst
|
||||
task.syncOption.SyncPriority = SyncPriorityTimestampFirst
|
||||
}
|
||||
task.LocalFolderPath = path.Clean(task.LocalFolderPath)
|
||||
task.PanFolderPath = path.Clean(task.PanFolderPath)
|
||||
if e := task.Start(step); e != nil {
|
||||
logger.Verboseln(e)
|
||||
fmt.Printf("启动同步任务[%s]出错: %s\n", task.Id, e.Error())
|
||||
continue
|
||||
}
|
||||
fmt.Println("\n启动同步任务")
|
||||
fmt.Println(task)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
// save config file
|
||||
if m.useConfigFile {
|
||||
ioutil.WriteFile(m.ConfigFilePath(), []byte(utils.ObjectToJsonStr(m.syncDriveConfig, true)), 0755)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Stop 停止同步进程
|
||||
func (m *SyncTaskManager) Stop(step TaskStep) (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()
|
||||
}
|
||||
|
||||
if e != nil {
|
||||
logger.Verboseln(e)
|
||||
fmt.Println("stop sync task error: ", task.NameLabel())
|
||||
continue
|
||||
}
|
||||
fmt.Println("正在停止同步任务: ", task.NameLabel())
|
||||
}
|
||||
|
||||
// save config file
|
||||
if m.useConfigFile {
|
||||
ioutil.WriteFile(m.ConfigFilePath(), []byte(utils.ObjectToJsonStr(m.syncDriveConfig, true)), 0755)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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{})
|
||||
user, _ := panClient.GetUserInfo()
|
||||
|
||||
manager := NewSyncTaskManager(
|
||||
nil,
|
||||
user.FileDriveId,
|
||||
panClient,
|
||||
"D:\\smb\\feny\\goprojects\\dev\\sync_drive",
|
||||
SyncOption{},
|
||||
)
|
||||
|
||||
manager.Start(nil)
|
||||
time.Sleep(30 * time.Minute)
|
||||
manager.Stop()
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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{})
|
||||
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()
|
||||
}
|
||||
@@ -116,6 +116,12 @@ func (te *TaskExecutor) Execute() {
|
||||
return
|
||||
}
|
||||
|
||||
// 取消下载
|
||||
if result.Cancel {
|
||||
task.Unit.OnCancel(result)
|
||||
return
|
||||
}
|
||||
|
||||
if result.Succeed {
|
||||
task.Unit.OnSuccess(result)
|
||||
task.Unit.OnComplete(result)
|
||||
|
||||
@@ -29,14 +29,17 @@ type (
|
||||
OnFailed(lastRunResult *TaskUnitRunResult)
|
||||
// 每次执行结束执行的方法, 不管成功失败
|
||||
OnComplete(lastRunResult *TaskUnitRunResult)
|
||||
// 取消下载
|
||||
OnCancel(lastRunResult *TaskUnitRunResult)
|
||||
// 重试等待的时间
|
||||
RetryWait() time.Duration
|
||||
}
|
||||
|
||||
// 任务单元执行结果
|
||||
TaskUnitRunResult struct {
|
||||
Succeed bool // 是否执行成功
|
||||
NeedRetry bool // 是否需要重试
|
||||
Succeed bool // 是否执行成功
|
||||
NeedRetry bool // 是否需要重试
|
||||
Cancel bool // 是否取消了任务
|
||||
|
||||
// 以下是额外的信息
|
||||
Err error // 错误信息
|
||||
|
||||
@@ -51,6 +51,10 @@ func (tu *TestUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
}
|
||||
}
|
||||
|
||||
func (tu *TestUnit) OnCancel(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
|
||||
}
|
||||
|
||||
func (tu *TestUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
fmt.Printf("[%s] prepare retry, times [%d/%d]...\n", tu.taskInfo.Id(), tu.taskInfo.Retry(), tu.taskInfo.MaxRetry())
|
||||
}
|
||||
|
||||
+113
-6
@@ -15,12 +15,20 @@ package utils
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"crypto/md5"
|
||||
"flag"
|
||||
"fmt"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/tickstep/library-go/ids"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -104,11 +112,11 @@ func ParseVersionNum(versionStr string) int {
|
||||
versionStr = strings.ReplaceAll(versionStr, "-dev", "")
|
||||
versionStr = strings.ReplaceAll(versionStr, "v", "")
|
||||
versionParts := strings.Split(versionStr, ".")
|
||||
verNum := parseInt(versionParts[0]) * 1e4 + parseInt(versionParts[1]) * 1e2 + parseInt(versionParts[2])
|
||||
verNum := parseInt(versionParts[0])*1e4 + parseInt(versionParts[1])*1e2 + parseInt(versionParts[2])
|
||||
return verNum
|
||||
}
|
||||
func parseInt(numStr string) int {
|
||||
num,e := strconv.Atoi(numStr)
|
||||
num, e := strconv.Atoi(numStr)
|
||||
if e != nil {
|
||||
return 0
|
||||
}
|
||||
@@ -117,7 +125,10 @@ func parseInt(numStr string) int {
|
||||
|
||||
func ConvertTime(t time.Duration) string {
|
||||
seconds := int64(t.Seconds())
|
||||
return ConvertTimeSecond(seconds)
|
||||
}
|
||||
|
||||
func ConvertTimeSecond(seconds int64) string {
|
||||
MT := int64(1 * 60)
|
||||
HT := int64(1 * 60 * 60)
|
||||
|
||||
@@ -128,22 +139,118 @@ func ConvertTime(t time.Duration) string {
|
||||
return fmt.Sprintf("%d秒", seconds)
|
||||
}
|
||||
if seconds >= MT && seconds < HT {
|
||||
return fmt.Sprintf("%d分%d秒", seconds / MT, seconds % MT)
|
||||
return fmt.Sprintf("%d分%d秒", seconds/MT, seconds%MT)
|
||||
}
|
||||
if seconds >= HT {
|
||||
h := seconds / HT
|
||||
tmp := seconds % HT
|
||||
return fmt.Sprintf("%d小时%d分%d秒", h, tmp / MT, tmp % MT)
|
||||
return fmt.Sprintf("%d小时%d分%d秒", h, tmp/MT, tmp%MT)
|
||||
}
|
||||
return "0秒"
|
||||
}
|
||||
|
||||
// HasSuffix 判断是否以某字符串作为结尾
|
||||
func HasSuffix(s ,suffix string) bool {
|
||||
func HasSuffix(s, suffix string) bool {
|
||||
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
|
||||
}
|
||||
|
||||
// HasPrefix 判断是否以某字符串作为开始
|
||||
func HasPrefix(s, prefix string) bool {
|
||||
return len(s) >= len(prefix) && s[0:len(prefix)]== prefix
|
||||
return len(s) >= len(prefix) && s[0:len(prefix)] == prefix
|
||||
}
|
||||
|
||||
// GetUniqueKeyStr 获取本机唯一标识
|
||||
func GetUniqueKeyStr() string {
|
||||
keyStr := ids.GetUniqueId("", 32)
|
||||
if len(keyStr) == 0 || keyStr == "" {
|
||||
// default
|
||||
keyStr = "AE8627B0296A4126A1434999C45ECAB2"
|
||||
}
|
||||
return keyStr
|
||||
}
|
||||
|
||||
// PathExists 文件路径是否存在
|
||||
func PathExists(path string) (bool, error) {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// ObjectToJsonStr 转换成json字符串
|
||||
func ObjectToJsonStr(v interface{}, useIndent bool) string {
|
||||
r := ""
|
||||
if useIndent {
|
||||
if data, err := jsoniter.MarshalIndent(v, "", " "); err == nil {
|
||||
r = string(data)
|
||||
}
|
||||
} else {
|
||||
if data, err := jsoniter.MarshalIndent(v, "", ""); err == nil {
|
||||
r = string(data)
|
||||
}
|
||||
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func UuidStr() string {
|
||||
u4 := uuid.NewV4()
|
||||
return u4.String()
|
||||
}
|
||||
|
||||
// NowTimeStr 当前时间字符串,格式为:2006-01-02 15:04:05
|
||||
func NowTimeStr() string {
|
||||
return time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func defaultZeroTime() time.Time {
|
||||
value := "1971-01-01 08:00:00"
|
||||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||||
if t, e := time.ParseInLocation("2006-01-02 15:04:05", value, cz); e == nil {
|
||||
return t
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// ParseTimeStr 反解析时间字符串
|
||||
func ParseTimeStr(value string) time.Time {
|
||||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||||
if t, e := time.ParseInLocation("2006-01-02 15:04:05", value, cz); e == nil {
|
||||
return t
|
||||
}
|
||||
return defaultZeroTime()
|
||||
}
|
||||
|
||||
// Md5Str MD5哈希计算
|
||||
func Md5Str(text string) string {
|
||||
h := md5.New()
|
||||
h.Write([]byte(text))
|
||||
re := h.Sum(nil)
|
||||
sb := &strings.Builder{}
|
||||
fmt.Fprintf(sb, "%x", re)
|
||||
return strings.ToLower(sb.String())
|
||||
}
|
||||
|
||||
// IsLocalAbsPath 是否是本地绝对路径
|
||||
func IsLocalAbsPath(filePath string) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
// 是否是windows路径
|
||||
matched, _ := regexp.MatchString("^([a-zA-Z]:)", filePath)
|
||||
if matched {
|
||||
// windows volume label
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
return path.IsAbs(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
// IsPanAbsPath 是否是云盘绝对路径
|
||||
func IsPanAbsPath(filePath string) bool {
|
||||
return path.IsAbs(filePath)
|
||||
}
|
||||
|
||||
@@ -19,3 +19,23 @@ func TestConvertTime(t *testing.T) {
|
||||
seconds = time.Duration(1246852) * time.Second
|
||||
fmt.Println(ConvertTime(seconds))
|
||||
}
|
||||
|
||||
func TestUuidStr(t *testing.T) {
|
||||
fmt.Println(UuidStr())
|
||||
}
|
||||
|
||||
func TestMd5Str(t *testing.T) {
|
||||
fmt.Println(Md5Str("123456"))
|
||||
}
|
||||
|
||||
func TestParseTimeStr(t *testing.T) {
|
||||
fmt.Println(ParseTimeStr(""))
|
||||
}
|
||||
|
||||
func TestIsAbsPath_ReturnTrue(t *testing.T) {
|
||||
fmt.Println(IsLocalAbsPath("D:\\my\\folder\\test"))
|
||||
}
|
||||
|
||||
func TestIsAbsPath_ReturnFalse(t *testing.T) {
|
||||
fmt.Println(IsLocalAbsPath("my\\folder\\test"))
|
||||
}
|
||||
|
||||
@@ -21,23 +21,23 @@ import (
|
||||
)
|
||||
|
||||
type FileDownloadStream struct {
|
||||
readOffset int64
|
||||
resp *http.Response
|
||||
timestamp int64
|
||||
readOffset int64
|
||||
resp *http.Response
|
||||
timestamp int64
|
||||
}
|
||||
|
||||
type FileUploadStream struct {
|
||||
fileUploadInfoEntity *aliyunpan.CreateFileUploadResult
|
||||
|
||||
filePath string
|
||||
fileSize int64
|
||||
fileId string
|
||||
fileWritePos int64
|
||||
filePath string
|
||||
fileSize int64
|
||||
fileId string
|
||||
fileWritePos int64
|
||||
fileUploadUrlIndex int
|
||||
|
||||
chunkBuffer []byte
|
||||
chunkPos int64
|
||||
chunkSize int64
|
||||
chunkPos int64
|
||||
chunkSize int64
|
||||
|
||||
timestamp int64
|
||||
|
||||
@@ -45,14 +45,14 @@ type FileUploadStream struct {
|
||||
}
|
||||
|
||||
type PanClientProxy struct {
|
||||
PanUser *config.PanUser
|
||||
PanDriveId string
|
||||
PanUser *config.PanUser
|
||||
PanDriveId string
|
||||
PanTransferUrlType int
|
||||
|
||||
mutex sync.Mutex
|
||||
|
||||
// 网盘文件路径到网盘文件信息实体映射缓存
|
||||
filePathCacheMap cachemap.CacheOpMap
|
||||
filePathCacheMap cachemap.CacheOpMap
|
||||
|
||||
// 网盘文件夹路径到文件夹下面所有子文件映射缓存
|
||||
fileDirectoryListCacheMap cachemap.CacheOpMap
|
||||
@@ -79,7 +79,6 @@ const FileDownloadUrlExpiredSeconds = 14400
|
||||
// FileUploadExpiredMinute 文件上传数据流过期时间
|
||||
const FileUploadExpiredMinute = 1440 // 24小时
|
||||
|
||||
|
||||
// getDownloadFileUrl 获取文件下载URL
|
||||
func (p *PanClientProxy) getFileDownloadUrl(urlResult *aliyunpan.GetFileDownloadUrlResult) string {
|
||||
if urlResult == nil {
|
||||
@@ -128,15 +127,15 @@ func (p *PanClientProxy) cacheFilesDirectoriesList(pathStr string) (fdl aliyunpa
|
||||
return nil
|
||||
}
|
||||
fileListParam := &aliyunpan.FileListParam{
|
||||
DriveId: p.PanDriveId,
|
||||
DriveId: p.PanDriveId,
|
||||
ParentFileId: fi.FileId,
|
||||
Limit: 200,
|
||||
Limit: 200,
|
||||
}
|
||||
fdl, apiError = p.PanUser.PanClient().FileListGetAll(fileListParam)
|
||||
fdl, apiError = p.PanUser.PanClient().FileListGetAll(fileListParam, 200)
|
||||
if apiError != nil {
|
||||
return nil
|
||||
}
|
||||
if len(fdl) == 0{
|
||||
if len(fdl) == 0 {
|
||||
// 空目录不缓存
|
||||
return nil
|
||||
}
|
||||
@@ -194,7 +193,7 @@ func (p *PanClientProxy) cacheFilePathEntity(fe *aliyunpan.FileEntity) {
|
||||
}
|
||||
|
||||
func (p *PanClientProxy) cacheFilePathEntityList(fdl aliyunpan.FileList) {
|
||||
for _,entity := range fdl {
|
||||
for _, entity := range fdl {
|
||||
pathStr := formatPathStyle(entity.Path)
|
||||
p.filePathCacheMap.CacheOperation(p.PanDriveId, pathStr, func() expires.DataExpires {
|
||||
return expires.NewDataExpires(entity, CacheExpiredMinute*time.Minute)
|
||||
@@ -285,7 +284,7 @@ func (p *PanClientProxy) cacheFileDownloadStream(sessionId, fileId string, offse
|
||||
logger.Verboseln(sessionId + " create new cache for offset = " + strconv.Itoa(int(offset)))
|
||||
return expires.NewDataExpires(&FileDownloadStream{
|
||||
readOffset: offset,
|
||||
resp: resp,
|
||||
resp: resp,
|
||||
timestamp: time.Now().Unix(),
|
||||
}, CacheExpiredMinute*time.Minute)
|
||||
})
|
||||
@@ -330,7 +329,6 @@ func (p *PanClientProxy) cacheFileUploadStream(userId, pathStr string, fileSize
|
||||
parentFileId = parentFileEntity.FileId
|
||||
}
|
||||
|
||||
|
||||
// 检查同名文件是否存在
|
||||
efi, apierr := p.PanUser.PanClient().FileInfoByPath(p.PanDriveId, pathStr)
|
||||
if apierr != nil {
|
||||
@@ -346,7 +344,7 @@ func (p *PanClientProxy) cacheFileUploadStream(userId, pathStr string, fileSize
|
||||
// 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}})
|
||||
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
|
||||
@@ -362,16 +360,16 @@ func (p *PanClientProxy) cacheFileUploadStream(userId, pathStr string, fileSize
|
||||
|
||||
// create new upload file
|
||||
appCreateUploadFileParam := &aliyunpan.CreateFileUploadParam{
|
||||
DriveId: p.PanDriveId,
|
||||
Name: filepath.Base(pathStr),
|
||||
Size: fileSize,
|
||||
ContentHash: "",
|
||||
DriveId: p.PanDriveId,
|
||||
Name: filepath.Base(pathStr),
|
||||
Size: fileSize,
|
||||
ContentHash: "",
|
||||
ContentHashName: "none",
|
||||
CheckNameMode: "refuse",
|
||||
ParentFileId: parentFileId,
|
||||
BlockSize: chunkSize,
|
||||
ProofCode: "",
|
||||
ProofVersion: "v1",
|
||||
CheckNameMode: "refuse",
|
||||
ParentFileId: parentFileId,
|
||||
BlockSize: chunkSize,
|
||||
ProofCode: "",
|
||||
ProofVersion: "v1",
|
||||
}
|
||||
|
||||
uploadOpEntity, apierr := p.PanUser.PanClient().CreateUploadFile(appCreateUploadFileParam)
|
||||
@@ -407,13 +405,13 @@ func (p *PanClientProxy) FileInfoByPath(pathStr string) (fileInfo *aliyunpan.Fil
|
||||
}
|
||||
|
||||
// FileListGetAll 获取文件路径下的所有子文件列表
|
||||
func (p *PanClientProxy) FileListGetAll(pathStr string) (aliyunpan.FileList, *apierror.ApiError) {
|
||||
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)
|
||||
r, er := p.PanUser.PanClient().MkdirByFullPath(p.PanDriveId, pathStr)
|
||||
if er != nil {
|
||||
return nil, er
|
||||
}
|
||||
@@ -422,7 +420,7 @@ func (p *PanClientProxy) mkdir(pathStr string, perm os.FileMode) (*aliyunpan.Mkd
|
||||
p.deleteOneFilesDirectoriesListCache(path.Dir(pathStr))
|
||||
|
||||
if r.FileId != "" {
|
||||
fe,_ := p.PanUser.PanClient().FileInfoById(p.PanDriveId, r.FileId)
|
||||
fe, _ := p.PanUser.PanClient().FileInfoById(p.PanDriveId, r.FileId)
|
||||
if fe != nil {
|
||||
fe.Path = pathStr
|
||||
p.cacheFilePathEntity(fe)
|
||||
@@ -451,7 +449,7 @@ func (p *PanClientProxy) Rename(oldpath, newpath string) error {
|
||||
if er != nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
_,e := p.PanUser.PanClient().FileRename(p.PanDriveId, oldFile.FileId, path.Base(newpath))
|
||||
_, e := p.PanUser.PanClient().FileRename(p.PanDriveId, oldFile.FileId, path.Base(newpath))
|
||||
if e != nil {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
@@ -477,7 +475,7 @@ func (p *PanClientProxy) Move(oldpath, newpath string) error {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
newFileParentDir,er := p.cacheFilePath(path.Dir(newpath))
|
||||
newFileParentDir, er := p.cacheFilePath(path.Dir(newpath))
|
||||
if er != nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
@@ -490,7 +488,7 @@ func (p *PanClientProxy) Move(oldpath, newpath string) error {
|
||||
}
|
||||
params := []*aliyunpan.FileMoveParam{}
|
||||
params = append(params, ¶m)
|
||||
_,e := p.PanUser.PanClient().FileMove(params)
|
||||
_, e := p.PanUser.PanClient().FileMove(params)
|
||||
if e != nil {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
@@ -556,7 +554,7 @@ func (p *PanClientProxy) DownloadFilePart(sessionId, fileId string, offset int64
|
||||
|
||||
// RemoveAll 删除文件
|
||||
func (p *PanClientProxy) RemoveAll(pathStr string) error {
|
||||
fi,er := p.FileInfoByPath(pathStr)
|
||||
fi, er := p.FileInfoByPath(pathStr)
|
||||
if er != nil {
|
||||
return er
|
||||
}
|
||||
@@ -590,7 +588,7 @@ func (p *PanClientProxy) UploadFilePrepare(userId, pathStr string, fileSize int6
|
||||
}
|
||||
|
||||
// remove old file cache
|
||||
oldFus,err := p.UploadFileCache(userId, pathStr)
|
||||
oldFus, err := p.UploadFileCache(userId, pathStr)
|
||||
if err != nil {
|
||||
logger.Verboseln("query upload file cache error: ", err)
|
||||
}
|
||||
@@ -625,7 +623,7 @@ func (p *PanClientProxy) needToUploadChunk(fus *FileUploadStream) bool {
|
||||
}
|
||||
|
||||
// maybe the final part
|
||||
if fus.fileUploadUrlIndex == (len(fus.fileUploadInfoEntity.PartInfoList)-1) {
|
||||
if fus.fileUploadUrlIndex == (len(fus.fileUploadInfoEntity.PartInfoList) - 1) {
|
||||
finalPartSize := fus.fileSize % fus.chunkSize
|
||||
if finalPartSize == 0 {
|
||||
finalPartSize = fus.chunkSize
|
||||
@@ -637,7 +635,6 @@ func (p *PanClientProxy) needToUploadChunk(fus *FileUploadStream) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// isUrlExpired 上传链接是否已过期。过期返回True
|
||||
func (p *PanClientProxy) isUrlExpired(urlStr string) bool {
|
||||
u, err := url.Parse(urlStr)
|
||||
@@ -645,7 +642,7 @@ func (p *PanClientProxy) isUrlExpired(urlStr string) bool {
|
||||
return true
|
||||
}
|
||||
expiredTimeSecStr := u.Query().Get("x-oss-expires")
|
||||
expiredTimeSec,_ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
|
||||
expiredTimeSec, _ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
|
||||
if (time.Now().Unix() - 10) >= expiredTimeSec {
|
||||
// expired
|
||||
return true
|
||||
@@ -669,7 +666,7 @@ func (p *PanClientProxy) UploadFilePart(userId, pathStr string, offset int64, bu
|
||||
|
||||
// write buffer to chunk buffer
|
||||
uploadCount := 0
|
||||
for _,b := range buffer {
|
||||
for _, b := range buffer {
|
||||
fus.chunkBuffer[fus.chunkPos] = b
|
||||
fus.chunkPos += 1
|
||||
fus.fileWritePos += 1
|
||||
@@ -688,14 +685,14 @@ func (p *PanClientProxy) UploadFilePart(userId, pathStr string, offset int64, bu
|
||||
}
|
||||
uploadPartInfo := fus.fileUploadInfoEntity.PartInfoList[fus.fileUploadUrlIndex]
|
||||
cd := &aliyunpan.FileUploadChunkData{
|
||||
Reader: uploadChunk,
|
||||
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 {
|
||||
for _, item := range fus.fileUploadInfoEntity.PartInfoList {
|
||||
infoList = append(infoList, aliyunpan.FileUploadPartInfoParam{
|
||||
PartNumber: item.PartNumber,
|
||||
})
|
||||
@@ -731,9 +728,9 @@ func (p *PanClientProxy) UploadFilePart(userId, pathStr string, offset int64, bu
|
||||
// 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,
|
||||
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)
|
||||
@@ -750,4 +747,4 @@ func (p *PanClientProxy) UploadFilePart(userId, pathStr string, offset int64, bu
|
||||
}
|
||||
|
||||
return uploadCount, nil
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user