├── .github
└── ISSUE_TEMPLATE
│ ├── 1_BUG_REPORT.md
│ └── 2_FEATURE_REQUEST.md
├── .gitignore
├── LICENSE
├── README.md
├── aliyunpan.exe.manifest
├── assets
├── aliyunpan.ico
├── aliyunpan.png
├── binary
│ └── tzdata.zip
├── images
│ ├── aliyunpan_cps_qrcode.png
│ ├── app-deregister-device.png
│ ├── debug-log-screenshot.png
│ ├── download_file_ecs_speed_screenshot.gif
│ ├── download_file_speed_screenshot.gif
│ ├── how-to-get-refresh-token-cmd.png
│ ├── how-to-get-refresh-token.png
│ ├── login-screenshot-1.png
│ ├── login-screenshot-2.png
│ ├── login-screenshot-3.png
│ ├── login-screenshot-4.png
│ ├── multi_user_download.png
│ ├── sync_command-basic_logic.jpg
│ ├── upload_file_speed_screenshot.gif
│ ├── web-deregister-device.png
│ ├── webdav-screenshot.png
│ ├── win10-alisync-service-bg.png
│ ├── win10-alisync-service.png
│ └── win10-env-debug-config.png
├── plugin
│ └── js
│ │ ├── download_handler.js.sample
│ │ ├── remove_handler.js.sample
│ │ ├── sync_handler.js.sample
│ │ ├── token_handler.js.sample
│ │ └── upload_handler.js.sample
├── scripts
│ ├── sync.bat
│ ├── sync.sh
│ ├── webdav.bat
│ └── webdav.sh
├── service
│ └── linux
│ │ ├── aliyunpansync.service
│ │ ├── start.sh
│ │ └── stop.sh
└── sync_drive
│ └── sync_drive_config.json.sample
├── build.sh
├── cmder
├── cmder_helper.go
├── cmdliner
│ ├── args
│ │ └── args.go
│ ├── clear.go
│ ├── clear_windows.go
│ ├── cmdliner.go
│ └── linehistory.go
├── cmdtable
│ └── cmdtable.go
└── cmdutil
│ ├── addr.go
│ ├── cmdutil.go
│ ├── escaper
│ └── escaper.go
│ ├── file.go
│ └── jsonhelper
│ └── jsonhelper.go
├── docker
└── sync
│ ├── Dockerfile
│ ├── app.sh
│ ├── docker-compose.yml
│ └── health_check.sh
├── docs
├── complie_project.md
├── manual.md
└── plugin_manual.md
├── entitlements.xml
├── go.mod
├── go.sum
├── internal
├── command
│ ├── album.go
│ ├── album_web.go
│ ├── cd.go
│ ├── command.go
│ ├── command_test.go
│ ├── cp.go
│ ├── download.go
│ ├── drive_list.go
│ ├── login.go
│ ├── ls_search.go
│ ├── mkdir.go
│ ├── mv.go
│ ├── quota.go
│ ├── recycle.go
│ ├── rename.go
│ ├── rename_test.go
│ ├── rm.go
│ ├── rm_test.go
│ ├── save.go
│ ├── share.go
│ ├── share_web.go
│ ├── sync.go
│ ├── tree.go
│ ├── upload.go
│ ├── user_info.go
│ ├── utils.go
│ └── xcp.go
├── command_local
│ ├── lcd.go
│ ├── lls.go
│ └── utils.go
├── config
│ ├── cache.go
│ ├── errors.go
│ ├── pan_client.go
│ ├── pan_config.go
│ ├── pan_config_export.go
│ ├── pan_user.go
│ ├── utils.go
│ └── utils_test.go
├── file
│ ├── downloader
│ │ ├── config.go
│ │ ├── downloader.go
│ │ ├── instance_state.go
│ │ ├── loadbalance.go
│ │ ├── monitor.go
│ │ ├── resetcontroler.go
│ │ ├── sort.go
│ │ ├── status.go
│ │ ├── utils.go
│ │ ├── worker.go
│ │ └── writer.go
│ └── uploader
│ │ ├── block.go
│ │ ├── block_test.go
│ │ ├── error.go
│ │ ├── instance_state.go
│ │ ├── multiuploader.go
│ │ ├── multiworker.go
│ │ ├── readed.go
│ │ ├── status.go
│ │ └── uploader.go
├── functions
│ ├── common.go
│ ├── pandownload
│ │ ├── download_statistic.go
│ │ ├── download_task_unit.go
│ │ ├── errors.go
│ │ └── utils.go
│ ├── panlogin
│ │ ├── login_helper.go
│ │ └── login_helper_test.go
│ ├── panupload
│ │ ├── upload.go
│ │ ├── upload_database.go
│ │ ├── upload_statistic.go
│ │ ├── upload_task_unit.go
│ │ └── utils.go
│ └── statistic.go
├── global
│ ├── pan_file.go
│ └── vars.go
├── localfile
│ ├── checksum_write.go
│ ├── errors.go
│ ├── file.go
│ ├── localfile.go
│ ├── symlink.go
│ └── symlink_test.go
├── log
│ ├── file_record.go
│ └── file_record_test.go
├── panupdate
│ ├── github.go
│ ├── panupdate.go
│ └── updatefile.go
├── plugins
│ ├── idle_plugin.go
│ ├── js_plugin.go
│ ├── plugin.go
│ ├── plugin_manager.go
│ ├── plugin_manager_test.go
│ ├── plugin_util.go
│ └── plugin_util_test.go
├── syncdrive
│ ├── bolt_db.go
│ ├── bolt_db_test.go
│ ├── file_action_task.go
│ ├── file_action_task_mgr.go
│ ├── sync_constants.go
│ ├── sync_db.go
│ ├── sync_db_bolt.go
│ ├── sync_db_test.go
│ ├── sync_db_util.go
│ ├── sync_db_util_test.go
│ ├── sync_task.go
│ ├── sync_task_mgr.go
│ └── utils.go
├── taskframework
│ ├── executor.go
│ ├── task_unit.go
│ ├── taskframework_test.go
│ └── taskinfo.go
├── utils
│ ├── utils.go
│ └── utils_test.go
└── waitgroup
│ ├── wait_group.go
│ └── wait_group_test.go
├── library
├── collection
│ ├── queue.go
│ └── queue_test.go
├── crypto
│ └── crypto.go
├── filelocker
│ ├── errors.go
│ ├── file_locker.go
│ ├── file_locker_test.go
│ ├── locker_solaris.go
│ ├── locker_unix.go
│ └── locker_windows.go
├── homedir
│ └── homedir.go
├── nets
│ └── util.go
└── requester
│ └── transfer
│ ├── download_instanceinfo.go
│ ├── download_status.go
│ └── rangelist.go
├── main.go
├── resource_windows_386.syso
├── resource_windows_amd64.syso
├── versioninfo.json
└── win_build.bat
/.github/ISSUE_TEMPLATE/1_BUG_REPORT.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 问题反馈
3 | about: 如果你在使用过程中发现问题,请使用该模板进行反馈。
4 | labels: Bug
5 | ---
6 |
7 | ### Ⅰ. 使用环境
8 | 1. 操作系统:(Linux? macOS? Windows?)
9 | 2. aliyunpan版本号:(v0.3.6? v0.3.7?...)
10 |
11 | ### Ⅱ. 问题描述
12 |
13 |
14 | ### Ⅲ. 期望的结果
15 |
16 |
17 | ### Ⅳ. 如何复现问题
18 |
22 | 1. xxx
23 | 2. xxx
24 | 3. xxx
25 |
26 | ### Ⅴ. 请提供相关的错误日志
27 |
28 | ```log
29 | (日志请贴在这里。如果日志很多,可以以附件的形式上传)
30 |
31 | ```
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/2_FEATURE_REQUEST.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 提新需求
3 | about: 如果你想提出一个新需求,新功能建议,请使用此模板。
4 | labels: Feature Request
5 | ---
6 |
7 | ### Ⅰ. 你当前使用环境
8 | 1. 操作系统:(Linux? macOS? Windows?)
9 | 2. aliyunpan版本号:(v0.3.6? v0.3.7?...)
10 |
11 | ### Ⅱ. 请描述你想要的新功能
12 |
13 |
14 | ### Ⅲ. 请描述新功能的使用场景
15 |
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.dll
4 | *.so
5 | *.dylib
6 | aliyunpan
7 | aliyunpan.exe
8 | cmd/AndroidNDKBuild/AndroidNDKBuild
9 |
10 | # Test binary, build with `go test -c`
11 | *.test
12 |
13 | # Output of the go coverage tool, specifically when used with LiteIDE
14 | *.out
15 | out/
16 | *.dl
17 |
18 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
19 | .glide/
20 |
21 | # Others
22 | .DS_Store
23 | *.proc
24 | *.txt
25 | *.log
26 | *.gz
27 | captcha.png
28 | aliyunpan_config.json
29 | aliyunpan_command_history.txt
30 | aliyunpan_uploading.json
31 | test/
32 | download/
33 | *-downloading
34 |
35 | # GoLand
36 | .idea/
37 |
38 | demos/
39 | tests.sh
40 | main_test.go
41 | license_header.sh
42 | account.txt
43 | git_push.sh
44 | git_pull.sh
45 | docker/sync/data
46 | clear_docker.sh
47 | upload.sh
--------------------------------------------------------------------------------
/aliyunpan.exe.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | true
12 |
13 |
14 |
--------------------------------------------------------------------------------
/assets/aliyunpan.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/aliyunpan.ico
--------------------------------------------------------------------------------
/assets/aliyunpan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/aliyunpan.png
--------------------------------------------------------------------------------
/assets/binary/tzdata.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/binary/tzdata.zip
--------------------------------------------------------------------------------
/assets/images/aliyunpan_cps_qrcode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/aliyunpan_cps_qrcode.png
--------------------------------------------------------------------------------
/assets/images/app-deregister-device.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/app-deregister-device.png
--------------------------------------------------------------------------------
/assets/images/debug-log-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/debug-log-screenshot.png
--------------------------------------------------------------------------------
/assets/images/download_file_ecs_speed_screenshot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/download_file_ecs_speed_screenshot.gif
--------------------------------------------------------------------------------
/assets/images/download_file_speed_screenshot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/download_file_speed_screenshot.gif
--------------------------------------------------------------------------------
/assets/images/how-to-get-refresh-token-cmd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/how-to-get-refresh-token-cmd.png
--------------------------------------------------------------------------------
/assets/images/how-to-get-refresh-token.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/how-to-get-refresh-token.png
--------------------------------------------------------------------------------
/assets/images/login-screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/login-screenshot-1.png
--------------------------------------------------------------------------------
/assets/images/login-screenshot-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/login-screenshot-2.png
--------------------------------------------------------------------------------
/assets/images/login-screenshot-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/login-screenshot-3.png
--------------------------------------------------------------------------------
/assets/images/login-screenshot-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/login-screenshot-4.png
--------------------------------------------------------------------------------
/assets/images/multi_user_download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/multi_user_download.png
--------------------------------------------------------------------------------
/assets/images/sync_command-basic_logic.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/sync_command-basic_logic.jpg
--------------------------------------------------------------------------------
/assets/images/upload_file_speed_screenshot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/upload_file_speed_screenshot.gif
--------------------------------------------------------------------------------
/assets/images/web-deregister-device.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/web-deregister-device.png
--------------------------------------------------------------------------------
/assets/images/webdav-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/webdav-screenshot.png
--------------------------------------------------------------------------------
/assets/images/win10-alisync-service-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/win10-alisync-service-bg.png
--------------------------------------------------------------------------------
/assets/images/win10-alisync-service.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/win10-alisync-service.png
--------------------------------------------------------------------------------
/assets/images/win10-env-debug-config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/assets/images/win10-env-debug-config.png
--------------------------------------------------------------------------------
/assets/plugin/js/remove_handler.js.sample:
--------------------------------------------------------------------------------
1 | // ==========================================================================================
2 | // aliyunpan JS插件回调处理函数
3 | // 支持 JavaScript ECMAScript 5.1 语言规范
4 | //
5 | // 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
6 | // ==========================================================================================
7 |
8 |
9 | // ------------------------------------------------------------------------------------------
10 | // 函数说明:删除文件前的回调函数
11 | //
12 | // 参数说明
13 | // context - 当前调用的上下文信息
14 | // {
15 | // "appName": "aliyunpan",
16 | // "version": "v0.1.3",
17 | // "userId": "11001d48564f43b3bc5662874f04bb11",
18 | // "nickname": "tickstep",
19 | // "fileDriveId": "19519111",
20 | // "resourceDriveId": "29519122"
21 | // }
22 | // appName - 应用名称,当前固定为aliyunpan
23 | // version - 版本号
24 | // userId - 当前登录用户的ID
25 | // nickname - 用户昵称
26 | // fileDriveId - 用户备份网盘ID
27 | // resourceDriveId - 用户资源网盘ID
28 | //
29 | // params - 删除文件前的调用参数
30 | // {
31 | // "count": 10,
32 | // "items": [{}, {}, ...],
33 | // }
34 | // count - 文件总数
35 | // items - 文件列表,文件详情请查看下面 RemoveFilePrepareItem 定义
36 | //
37 | // RemoveFilePrepareItem - 文件详情参数
38 | // {
39 | // "driveId": "19519221",
40 | // "driveFileId": "65f6c5161ee4a1b9a02b41399282f281c6e6df21",
41 | // "driveFileName": "token.bat",
42 | // "driveFilePath": "/aliyunpan/Downloads/token.bat",
43 | // "driveFileSize": 125330,
44 | // "driveFileType": "file",
45 | // "driveFileUpdatedAt": "2025-03-02 10:39:14",
46 | // "driveFileCreatedAt": "2025-03-02 10:39:14"
47 | // }
48 | // driveId - 网盘ID
49 | // driveFileId - 网盘文件的ID
50 | // driveFileName - 网盘文件名
51 | // driveFilePath - 网盘文件绝对完整路径
52 | // driveFileSize - 网盘文件大小,单位B
53 | // driveFileType - 网盘文件类型,file-文件,folder-文件夹
54 | // driveFileUpdatedAt - 网盘文件修改时间
55 | // driveFileCreatedAt - 网盘文件创建时间
56 | //
57 | // 返回值说明
58 | // {
59 | // "result": [{}, {}, ...],
60 | // }
61 | // result - 结果列表,每个文件的确认结果,文件详细定义请查看 RemoveFilePrepareResultItem 定义
62 | //
63 | // RemoveFilePrepareResultItem - 每项文件确认结果
64 | // {
65 | // "driveId": "19519221",
66 | // "driveFileId": "65f6c5161ee4a1b9a02b41399282f281c6e6df21",
67 | // "removeApproved": "yes"
68 | // }
69 | // driveId - 网盘ID
70 | // driveFileId - 网盘文件的ID
71 | // removeApproved - 该文件是否能删除,yes-确认删除,no-不能删除
72 | // ------------------------------------------------------------------------------------------
73 | function removeFilePrepareCallback(context, params) {
74 | console.log(params)
75 | var result = {"result":[]};
76 | for(var i = 0; i < params.items.length; i++) {
77 | var fileItem = params.items[i]
78 | console.log(fileItem);
79 | var fileItemApprovedResult = {
80 | "driveId": fileItem["driveId"],
81 | "driveFileId": fileItem["driveFileId"],
82 | "removeApproved": "yes"
83 | }
84 |
85 | // 可以通过名称过滤
86 | if (fileItem["driveFileType"] == "file") { // 文件
87 | if (fileItem["driveFileName"] == "password.key") {
88 | fileItemApprovedResult["removeApproved"] = "no"; // 禁止删除password.key文件
89 | }
90 | } else if (params["driveFileType"] == "folder") { // 文件夹
91 | // 对文件夹进行处理
92 | }
93 |
94 | // 可以通过时间过滤,例如:保留最近10天的文件
95 | // ...
96 |
97 | // 可以通过数量过滤,例如:保留最新5个文件
98 | // ...
99 |
100 | // 放入结果列表
101 | result["result"].push(fileItemApprovedResult)
102 | }
103 | // 返回
104 | return result;
105 | }
106 |
--------------------------------------------------------------------------------
/assets/plugin/js/token_handler.js.sample:
--------------------------------------------------------------------------------
1 | // ==========================================================================================
2 | // aliyunpan JS插件回调处理函数
3 | // 支持 JavaScript ECMAScript 5.1 语言规范
4 | //
5 | // 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
6 | // ==========================================================================================
7 |
8 |
9 | // ------------------------------------------------------------------------------------------
10 | // 函数说明:用户Token刷新完成后回调函数
11 | //
12 | // 参数说明
13 | // context - 当前调用的上下文信息
14 | // {
15 | // "appName": "aliyunpan",
16 | // "version": "v0.1.3",
17 | // "userId": "11001d48564f43b3bc5662874f04bb11",
18 | // "nickname": "tickstep",
19 | // "fileDriveId": "19519111",
20 | // "resourceDriveId": "29519122"
21 | // }
22 | // appName - 应用名称,当前固定为aliyunpan
23 | // version - 版本号
24 | // userId - 当前登录用户的ID
25 | // nickname - 用户昵称
26 | // fileDriveId - 用户备份网盘ID
27 | // resourceDriveId - 用户资源网盘ID
28 | //
29 | // params - Token刷新参数
30 | // {
31 | // "result": "success",
32 | // "message": "ok",
33 | // "oldToken": "aa31fcc229c54d5ab6d8bfb17aff3711",
34 | // "newToken": "bb31fcc229c54d5ab6d8bfb17aff3722",
35 | // "updatedAt": "2022-04-14 07:05:12"
36 | // }
37 | // result - Token刷新的结果,success-成功,fail-失败
38 | // message - 消息说明,如果失败这里会有原因说明
39 | // oldToken - 刷新前的Token
40 | // newToken - 刷新后的Token,只有result为success的才有该值
41 | // updatedAt - Token刷新的时间
42 | //
43 | // 返回值说明
44 | // (没有返回值)
45 | // ------------------------------------------------------------------------------------------
46 | function userTokenRefreshFinishCallback(context, params) {
47 | console.log(params)
48 | }
49 |
--------------------------------------------------------------------------------
/assets/plugin/js/upload_handler.js.sample:
--------------------------------------------------------------------------------
1 | // ==========================================================================================
2 | // aliyunpan JS插件回调处理函数
3 | // 支持 JavaScript ECMAScript 5.1 语言规范
4 | //
5 | // 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
6 | // ==========================================================================================
7 |
8 |
9 | // ------------------------------------------------------------------------------------------
10 | // 函数说明:上传文件前的回调函数
11 | //
12 | // 参数说明
13 | // context - 当前调用的上下文信息
14 | // {
15 | // "appName": "aliyunpan",
16 | // "version": "v0.1.3",
17 | // "userId": "11001d48564f43b3bc5662874f04bb11",
18 | // "nickname": "tickstep",
19 | // "fileDriveId": "19519111",
20 | // "resourceDriveId": "29519122"
21 | // }
22 | // appName - 应用名称,当前固定为aliyunpan
23 | // version - 版本号
24 | // userId - 当前登录用户的ID
25 | // nickname - 用户昵称
26 | // fileDriveId - 用户备份网盘ID
27 | // resourceDriveId - 用户资源网盘ID
28 | //
29 | // params - 文件上传前参数
30 | // {
31 | // "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
32 | // "localFileName": "token.bat",
33 | // "localFileSize": 125330,
34 | // "localFileType": "file",
35 | // "localFileUpdatedAt": "2022-04-14 07:05:12",
36 | // "driveId": "19519221",
37 | // "driveFilePath": "aliyunpan/Downloads/token.bat"
38 | // }
39 | // localFilePath - 本地文件绝对完整路径
40 | // localFileName - 本地文件名
41 | // localFileSize - 本地文件大小,单位B
42 | // localFileType - 本地文件类型,file-文件,folder-文件夹
43 | // localFileUpdatedAt - 文件修改时间
44 | // driveId - 准备上传的目标网盘ID
45 | // driveFilePath - 准备上传的目标网盘保存的路径,这个是相对路径,相对指定上传的目标文件夹
46 | //
47 | // 返回值说明
48 | // {
49 | // "uploadApproved": "yes",
50 | // "driveFilePath": "newfolder/token.bat"
51 | // }
52 | // uploadApproved - 该文件是否确认上传,yes-允许上传,no-禁止上传
53 | // driveFilePath - 文件保存的网盘路径,这个是相对路径,如果为空""代表保持原本的目标路径。
54 | // 这个改动要小心,会导致重名文件只会上传一个
55 | // ------------------------------------------------------------------------------------------
56 | function uploadFilePrepareCallback(context, params) {
57 | var result = {
58 | "uploadApproved": "yes",
59 | "driveFilePath": ""
60 | };
61 |
62 | // 所有的.dmg文件,网盘保存的文件增加后缀名为.exe,本地文件不改动
63 | if (params["localFilePath"].lastIndexOf(".dmg") > 0) {
64 | result["driveFilePath"] = params["driveFilePath"] + ".exe";
65 | }
66 |
67 | // 禁止.txt文件上传
68 | if (params["localFilePath"].lastIndexOf(".txt") > 0) {
69 | result["uploadApproved"] = "no";
70 | }
71 |
72 | // 禁止password.key文件上传
73 | if (params["localFileName"] == "password.key") {
74 | result["uploadApproved"] = "no";
75 | }
76 |
77 | return result;
78 | }
79 |
80 |
81 | // ------------------------------------------------------------------------------------------
82 | // 函数说明:上传文件结束的回调函数
83 | //
84 | // 参数说明
85 | // context - 当前调用的上下文信息
86 | // {
87 | // "appName": "aliyunpan",
88 | // "version": "v0.1.3",
89 | // "userId": "11001d48564f43b3bc5662874f04bb11",
90 | // "nickname": "tickstep",
91 | // "fileDriveId": "19519111",
92 | // "resourceDriveId": "29519122"
93 | // }
94 | // appName - 应用名称,当前固定为aliyunpan
95 | // version - 版本号
96 | // userId - 当前登录用户的ID
97 | // nickname - 用户昵称
98 | // fileDriveId - 用户备份网盘ID
99 | // resourceDriveId - 用户资源网盘ID
100 | //
101 | // params - 文件上传结束参数
102 | // {
103 | // "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
104 | // "localFileName": "token.bat",
105 | // "localFileSize": 125330,
106 | // "localFileType": "file",
107 | // "localFileUpdatedAt": "2022-04-14 07:05:12",
108 | // "localFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
109 | // "uploadResult": "success",
110 | // "driveId": "19519221",
111 | // "driveFilePath": "/tmp/test/aliyunpan/Downloads/token.bat"
112 | // }
113 | // localFilePath - 本地文件绝对完整路径
114 | // localFileName - 本地文件名
115 | // localFileSize - 本地文件大小,单位B
116 | // localFileType - 本地文件类型,file-文件,folder-文件夹
117 | // localFileUpdatedAt - 文件修改时间
118 | // localFileSha1 - 本地文件的SHA1。这个值不一定会有
119 | // uploadResult - 上传结果,success-成功,fail-失败
120 | // driveId - 目标网盘ID
121 | // driveFilePath - 文件网盘保存的绝对路径
122 | //
123 | // 返回值说明
124 | // (没有返回值)
125 | // ------------------------------------------------------------------------------------------
126 | function uploadFileFinishCallback(context, params) {
127 | console.log(params)
128 | }
--------------------------------------------------------------------------------
/assets/scripts/sync.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | REM 配置环境变量
4 | REM set ALIYUNPAN_CONFIG_DIR=d:\path\to\your\aliyunpan\config
5 |
6 | REM 指定refresh token用于登录
7 | aliyunpan login -RefreshToken=9078907....adg9087
8 |
9 | REM 上传下载链接类型:1-默认 2-阿里ECS环境
10 | aliyunpan config set -transfer_url_type 1
11 |
12 | REM 指定配置参数并进行启动
13 | REM 支持的模式:upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)
14 | aliyunpan sync start -ldir "D:\tickstep\Documents\设计文档" -pdir "/备份盘/我的文档" -mode "upload"
--------------------------------------------------------------------------------
/assets/scripts/sync.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # 请更改成你自己电脑上aliyunpan执行文件所在的目录
4 | #cd /path/to/aliyunpan/folder
5 |
6 | # 配置环境变量
7 | #export ALIYUNPAN_CONFIG_DIR=/path/to/your/aliyunpan/config
8 |
9 | chmod +x ./aliyunpan
10 |
11 | # 指定refresh token用于登录
12 | ./aliyunpan login -RefreshToken=9078907....adg9087
13 |
14 | # 上传下载链接类型:1-默认 2-阿里ECS环境
15 | ./aliyunpan config set -transfer_url_type 1
16 |
17 | # 指定配置参数并进行启动
18 | # 支持的模式:upload(备份本地文件到云盘),download(备份云盘文件到本地),sync(双向同步备份)
19 | ./aliyunpan sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload"
20 |
21 |
22 |
--------------------------------------------------------------------------------
/assets/scripts/webdav.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | REM 配置环境变量
4 | REM set ALIYUNPAN_CONFIG_DIR=d:\path\to\your\aliyunpan\config
5 |
6 | REM 指定refresh token用于登录
7 | aliyunpan login -RefreshToken=9078907....adg9087
8 |
9 | REM 上传下载链接类型:1-默认 2-阿里ECS环境
10 | aliyunpan config set -transfer_url_type 1
11 |
12 | REM 指定webdav启动参数并进行启动。你可以按照自己的需求更改以下参数,例如用户名和密码
13 | aliyunpan webdav start -ip "0.0.0.0" -port 23077 -webdav_user "admin" -webdav_password "admin" -pan_dir_path "/" -bs 1024
--------------------------------------------------------------------------------
/assets/scripts/webdav.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # 请更改成你自己电脑上aliyunpan执行文件所在的目录
4 | #cd /path/to/aliyunpan/folder
5 |
6 | # 配置环境变量
7 | #export ALIYUNPAN_CONFIG_DIR=/path/to/your/aliyunpan/config
8 |
9 | chmod +x ./aliyunpan
10 |
11 | # 指定refresh token用于登录
12 | ./aliyunpan login -RefreshToken=9078907....adg9087
13 |
14 | # 上传下载链接类型:1-默认 2-阿里ECS环境
15 | ./aliyunpan config set -transfer_url_type 1
16 |
17 | # 指定webdav启动参数并进行启动。你可以按照自己的需求更改以下参数,例如用户名、密码和网盘目录
18 | ./aliyunpan webdav start -ip "0.0.0.0" -port 23077 -webdav_user "admin" -webdav_password "admin" -pan_dir_path "/" -bs 1024
19 |
20 |
21 |
--------------------------------------------------------------------------------
/assets/service/linux/aliyunpansync.service:
--------------------------------------------------------------------------------
1 | # linux service 配置文件模版
2 | # centos系统默认存放路径为:/lib/systemd/system/aliyunpansync.service
3 | # 启动服务:systemctl start aliyunpansync
4 | # 停止服务:systemctl stop aliyunpansync
5 |
6 | [Unit]
7 | Description=aliyunpansync
8 | After=network.target
9 |
10 | [Service]
11 | Type=forking
12 | ExecStart=/path/to/start.sh
13 | ExecStop=/path/to/stop.sh
14 | PrivateTmp=true
15 | Restart=always
16 |
17 | [Install]
18 | WantedBy=multi-user.target
--------------------------------------------------------------------------------
/assets/service/linux/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # 更改路径为你电脑sync.sh文件所在文件夹路径
4 | cd /path/to/your/sync.sh/folder
5 | nohup ./sync.sh >/dev/null 2>&1 &
--------------------------------------------------------------------------------
/assets/service/linux/stop.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | PROCESS=`ps -ef|grep aliyunpan|grep -v grep|grep -v PPID|awk '{ print $2}'`
4 | for i in $PROCESS
5 | do
6 | # kill进程
7 | kill -9 $i
8 | break
9 | done
10 |
--------------------------------------------------------------------------------
/assets/sync_drive/sync_drive_config.json.sample:
--------------------------------------------------------------------------------
1 | {
2 | "configVer": "1.0",
3 | "syncTaskList": [
4 | {
5 | "name": "设计文档备份",
6 | "localFolderPath": "D:\\tickstep\\Documents\\设计文档",
7 | "panFolderPath": "/sync_drive/我的文档",
8 | "mode": "upload",
9 | "priority": "local"
10 | }
11 | ]
12 | }
--------------------------------------------------------------------------------
/cmder/cmder_helper.go:
--------------------------------------------------------------------------------
1 | package cmder
2 |
3 | import (
4 | "github.com/urfave/cli"
5 | )
6 |
7 | var (
8 | appInstance *cli.App
9 | )
10 |
11 | func SetApp(app *cli.App) {
12 | appInstance = app
13 | }
14 |
15 | func App() *cli.App {
16 | return appInstance
17 | }
18 |
--------------------------------------------------------------------------------
/cmder/cmdliner/args/args.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package args
15 |
16 | import (
17 | "strings"
18 | "unicode"
19 | )
20 |
21 | const (
22 | CharEscape = '\\'
23 | CharSingleQuote = '\''
24 | CharDoubleQuote = '"'
25 | CharBackQuote = '`'
26 | )
27 |
28 | // IsQuote 是否为引号
29 | func IsQuote(r rune) bool {
30 | return r == CharSingleQuote || r == CharDoubleQuote || r == CharBackQuote
31 | }
32 |
33 | // Parse 解析line, 忽略括号
34 | func Parse(line string) (lineArgs []string) { // 在函数中定义的返回值变量,会自动赋为 zero-value,即相当于 var lineArgs string[]
35 | var (
36 | rl = []rune(line + " ")
37 | buf = strings.Builder{}
38 | quoteChar rune
39 | nextChar rune
40 | escaped bool
41 | in bool
42 | )
43 |
44 | var (
45 | isSpace bool
46 | )
47 |
48 | for k, r := range rl {
49 | isSpace = unicode.IsSpace(r)
50 | if !isSpace && !in {
51 | in = true
52 | }
53 |
54 | switch {
55 | case escaped: // 已转义, 跳过
56 | escaped = false
57 | //pass
58 | case r == CharEscape: // 转义模式
59 | if k+1+1 < len(rl) { // 不是最后一个字符, 多+1是因为最后一个空格
60 | nextChar = rl[k+1]
61 | // 仅支持转义这些字符, 否则原样输出反斜杠
62 | if unicode.IsSpace(nextChar) || IsQuote(nextChar) || nextChar == CharEscape {
63 | escaped = true
64 | continue
65 | }
66 | }
67 | // pass
68 | case IsQuote(r):
69 | if quoteChar == 0 { //未引
70 | quoteChar = r
71 | continue
72 | }
73 |
74 | if quoteChar == r { //取消引
75 | quoteChar = 0
76 | continue
77 | }
78 | case isSpace:
79 | if !in { // 忽略多余的空格
80 | continue
81 | }
82 | if quoteChar == 0 { // 未在引号内
83 | lineArgs = append(lineArgs, buf.String())
84 | buf.Reset()
85 | in = false
86 | continue
87 | }
88 | }
89 |
90 | buf.WriteRune(r)
91 | }
92 |
93 | // Go 允许在定义函数时,命名返回值,当然这些变量可以在函数中使用。
94 | // 在 return 语句中,无需显示的返回这些值,Go 会自动将其返回。当然 return 语句还是必须要写的,否则编译器会报错。
95 | // 相当于 return lineArgs
96 | return
97 | }
98 |
--------------------------------------------------------------------------------
/cmder/cmdliner/clear.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package cmdliner
4 |
5 | import (
6 | "fmt"
7 | )
8 |
9 | // ClearScreen 清空屏幕
10 | func (pl *CmdLiner) ClearScreen() {
11 | ClearScreen()
12 | }
13 |
14 | // ClearScreen 清空屏幕
15 | func ClearScreen() {
16 | fmt.Print("\x1b[H\x1b[2J")
17 | }
18 |
--------------------------------------------------------------------------------
/cmder/cmdliner/clear_windows.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package cmdliner
15 |
16 | import (
17 | "syscall"
18 | "unsafe"
19 | )
20 |
21 | const (
22 | std_output_handle = uint32(-11 & 0xFFFFFFFF)
23 | )
24 |
25 | var (
26 | kernel32 = syscall.NewLazyDLL("kernel32.dll")
27 |
28 | procGetStdHandle = kernel32.NewProc("GetStdHandle")
29 | procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
30 | procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
31 | procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
32 | )
33 |
34 | type (
35 | coord struct {
36 | x, y int16
37 | }
38 | smallRect struct {
39 | left, top, right, bottom int16
40 | }
41 | consoleScreenBufferInfo struct {
42 | dwSize coord
43 | dwCursorPosition coord
44 | wAttributes int16
45 | srWindow smallRect
46 | dwMaximumWindowSize coord
47 | }
48 | )
49 |
50 | // ClearScreen 清空屏幕
51 | func (pl *CmdLiner) ClearScreen() {
52 | ClearScreen()
53 | }
54 |
55 | // ClearScreen 清空屏幕
56 | func ClearScreen() {
57 | out, _, _ := procGetStdHandle.Call(uintptr(std_output_handle))
58 | hOut := syscall.Handle(out)
59 |
60 | var sbi consoleScreenBufferInfo
61 | procGetConsoleScreenBufferInfo.Call(uintptr(hOut), uintptr(unsafe.Pointer(&sbi)))
62 |
63 | var numWritten uint32
64 | procFillConsoleOutputCharacter.Call(uintptr(hOut), uintptr(' '),
65 | uintptr(sbi.dwSize.x)*uintptr(sbi.dwSize.y),
66 | 0,
67 | uintptr(unsafe.Pointer(&numWritten)))
68 | procSetConsoleCursorPosition.Call(uintptr(hOut), 0)
69 | }
70 |
--------------------------------------------------------------------------------
/cmder/cmdliner/cmdliner.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package cmdliner
15 |
16 | import (
17 | "github.com/peterh/liner"
18 | )
19 |
20 | // CmdLiner 封装 *liner.State, 提供更简便的操作
21 | type CmdLiner struct {
22 | State *liner.State
23 | History *LineHistory
24 |
25 | tmode liner.ModeApplier
26 | lmode liner.ModeApplier
27 |
28 | paused bool
29 | }
30 |
31 | // NewLiner 返回 *CmdLiner, 默认设置允许 Ctrl+C 结束
32 | func NewLiner() *CmdLiner {
33 | pl := &CmdLiner{}
34 | pl.tmode, _ = liner.TerminalMode()
35 |
36 | line := liner.NewLiner()
37 | pl.lmode, _ = liner.TerminalMode()
38 |
39 | line.SetMultiLineMode(true)
40 | line.SetCtrlCAborts(true)
41 |
42 | pl.State = line
43 |
44 | return pl
45 | }
46 |
47 | // Pause 暂停服务
48 | func (pl *CmdLiner) Pause() error {
49 | if pl.paused {
50 | panic("CmdLiner already paused")
51 | }
52 |
53 | pl.paused = true
54 | pl.DoWriteHistory()
55 |
56 | return pl.tmode.ApplyMode()
57 | }
58 |
59 | // Resume 恢复服务
60 | func (pl *CmdLiner) Resume() error {
61 | if !pl.paused {
62 | panic("CmdLiner is not paused")
63 | }
64 |
65 | pl.paused = false
66 |
67 | return pl.lmode.ApplyMode()
68 | }
69 |
70 | // Close 关闭服务
71 | func (pl *CmdLiner) Close() (err error) {
72 | err = pl.State.Close()
73 | if err != nil {
74 | return err
75 | }
76 |
77 | if pl.History != nil && pl.History.historyFile != nil {
78 | return pl.History.historyFile.Close()
79 | }
80 |
81 | return nil
82 | }
83 |
--------------------------------------------------------------------------------
/cmder/cmdliner/linehistory.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package cmdliner
15 |
16 | import (
17 | "fmt"
18 | "os"
19 | )
20 |
21 | // LineHistory 命令行历史
22 | type LineHistory struct {
23 | historyFilePath string
24 | historyFile *os.File
25 | }
26 |
27 | // NewLineHistory 设置历史
28 | func NewLineHistory(filePath string) (lh *LineHistory, err error) {
29 | lh = &LineHistory{
30 | historyFilePath: filePath,
31 | }
32 |
33 | lh.historyFile, err = os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0644)
34 | if err != nil {
35 | return nil, err
36 | }
37 |
38 | return lh, nil
39 | }
40 |
41 | // DoWriteHistory 执行写入历史
42 | func (pl *CmdLiner) DoWriteHistory() (err error) {
43 | if pl.History == nil {
44 | return fmt.Errorf("history not set")
45 | }
46 |
47 | pl.History.historyFile, err = os.Create(pl.History.historyFilePath)
48 | if err != nil {
49 | return fmt.Errorf("写入历史错误, %s", err)
50 | }
51 |
52 | _, err = pl.State.WriteHistory(pl.History.historyFile)
53 | if err != nil {
54 | return fmt.Errorf("写入历史错误: %s", err)
55 | }
56 |
57 | return nil
58 | }
59 |
60 | // ReadHistory 读取历史
61 | func (pl *CmdLiner) ReadHistory() (err error) {
62 | if pl.History == nil {
63 | return fmt.Errorf("history not set")
64 | }
65 |
66 | _, err = pl.State.ReadHistory(pl.History.historyFile)
67 | return err
68 | }
69 |
--------------------------------------------------------------------------------
/cmder/cmdtable/cmdtable.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package cmdtable
15 |
16 | import (
17 | "github.com/olekukonko/tablewriter"
18 | "io"
19 | )
20 |
21 | type CmdTable struct {
22 | *tablewriter.Table
23 | }
24 |
25 | // NewTable 预设了一些配置
26 | func NewTable(wt io.Writer) CmdTable {
27 | tb := tablewriter.NewWriter(wt)
28 | tb.SetAutoWrapText(false)
29 | tb.SetBorder(false)
30 | tb.SetHeaderLine(false)
31 | tb.SetColumnSeparator("")
32 | return CmdTable{tb}
33 | }
34 |
--------------------------------------------------------------------------------
/cmder/cmdutil/addr.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package cmdutil
15 |
16 | import (
17 | "net"
18 | )
19 |
20 | // ListAddresses 列出本地可用的 IP 地址
21 | func ListAddresses() (addresses []string) {
22 | iFaces, _ := net.Interfaces()
23 | addresses = make([]string, 0, len(iFaces))
24 | for k := range iFaces {
25 | iFaceAddrs, _ := iFaces[k].Addrs()
26 | for l := range iFaceAddrs {
27 | switch v := iFaceAddrs[l].(type) {
28 | case *net.IPNet:
29 | addresses = append(addresses, v.IP.String())
30 | case *net.IPAddr:
31 | addresses = append(addresses, v.IP.String())
32 | }
33 | }
34 | }
35 | return
36 | }
37 |
38 | // ParseHost 解析地址中的host
39 | func ParseHost(address string) string {
40 | h, _, err := net.SplitHostPort(address)
41 | if err != nil {
42 | return address
43 | }
44 | return h
45 | }
46 |
--------------------------------------------------------------------------------
/cmder/cmdutil/cmdutil.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package cmdutil
15 |
16 | import (
17 | "compress/gzip"
18 | "flag"
19 | "io"
20 | "io/ioutil"
21 | "net/http/cookiejar"
22 | "net/url"
23 | "strings"
24 | )
25 |
26 | // TrimPathPrefix 去除目录的前缀
27 | func TrimPathPrefix(path, prefixPath string) string {
28 | if prefixPath == "/" {
29 | return path
30 | }
31 | return strings.TrimPrefix(path, prefixPath)
32 | }
33 |
34 | // ContainsString 检测字符串是否在字符串数组里
35 | func ContainsString(ss []string, s string) bool {
36 | for k := range ss {
37 | if ss[k] == s {
38 | return true
39 | }
40 | }
41 | return false
42 | }
43 |
44 | // GetURLCookieString 返回cookie字串
45 | func GetURLCookieString(urlString string, jar *cookiejar.Jar) string {
46 | u, _ := url.Parse(urlString)
47 | cookies := jar.Cookies(u)
48 | cookieString := ""
49 | for _, v := range cookies {
50 | cookieString += v.String() + "; "
51 | }
52 | cookieString = strings.TrimRight(cookieString, "; ")
53 | return cookieString
54 | }
55 |
56 | // DecompressGZIP 对 io.Reader 数据, 进行 gzip 解压
57 | func DecompressGZIP(r io.Reader) ([]byte, error) {
58 | gzipReader, err := gzip.NewReader(r)
59 | if err != nil {
60 | return nil, err
61 | }
62 | gzipReader.Close()
63 | return ioutil.ReadAll(gzipReader)
64 | }
65 |
66 | // FlagProvided 检测命令行是否提供名为 name 的 flag, 支持多个name(names)
67 | func FlagProvided(names ...string) bool {
68 | if len(names) == 0 {
69 | return false
70 | }
71 | var targetFlag *flag.Flag
72 | for _, name := range names {
73 | targetFlag = flag.Lookup(name)
74 | if targetFlag == nil {
75 | return false
76 | }
77 | if targetFlag.DefValue == targetFlag.Value.String() {
78 | return false
79 | }
80 | }
81 | return true
82 | }
83 |
84 | // Trigger 用于触发事件
85 | func Trigger(f func()) {
86 | if f == nil {
87 | return
88 | }
89 | go f()
90 | }
91 |
92 | // TriggerOnSync 用于触发事件, 同步触发
93 | func TriggerOnSync(f func()) {
94 | if f == nil {
95 | return
96 | }
97 | f()
98 | }
99 |
--------------------------------------------------------------------------------
/cmder/cmdutil/escaper/escaper.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package escaper
15 |
16 | import (
17 | "strings"
18 | )
19 |
20 | type (
21 | // RuneFunc 判断指定rune
22 | RuneFunc func(r rune) bool
23 | )
24 |
25 | // EscapeByRuneFunc 通过runeFunc转义, runeFunc返回真, 则转义
26 | func EscapeByRuneFunc(s string, runeFunc RuneFunc) string {
27 | if runeFunc == nil {
28 | return s
29 | }
30 |
31 | var (
32 | builder = &strings.Builder{}
33 | rs = []rune(s)
34 | )
35 |
36 | for k := range rs {
37 | if !runeFunc(rs[k]) {
38 | builder.WriteRune(rs[k])
39 | continue
40 | }
41 |
42 | if k >= 1 && rs[k-1] == '\\' {
43 | builder.WriteRune(rs[k])
44 | continue
45 | }
46 | builder.WriteString(`\`)
47 | builder.WriteRune(rs[k])
48 | }
49 | return builder.String()
50 | }
51 |
52 | // Escape 转义指定的escapeRunes, 在escapeRunes的前面加上一个反斜杠
53 | func Escape(s string, escapeRunes []rune) string {
54 | return EscapeByRuneFunc(s, func(r rune) bool {
55 | for k := range escapeRunes {
56 | if escapeRunes[k] == r {
57 | return true
58 | }
59 | }
60 | return false
61 | })
62 | }
63 |
64 | // EscapeStrings 转义字符串数组
65 | func EscapeStrings(ss []string, escapeRunes []rune) {
66 | for k := range ss {
67 | ss[k] = Escape(ss[k], escapeRunes)
68 | }
69 | }
70 |
71 | // EscapeStringsByRuneFunc 转义字符串数组, 通过runeFunc
72 | func EscapeStringsByRuneFunc(ss []string, runeFunc RuneFunc) {
73 | for k := range ss {
74 | ss[k] = EscapeByRuneFunc(ss[k], runeFunc)
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/cmder/cmdutil/file.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package cmdutil
15 |
16 | import (
17 | "github.com/kardianos/osext"
18 | "github.com/tickstep/library-go/logger"
19 | "os"
20 | "path"
21 | "path/filepath"
22 | "runtime"
23 | "strings"
24 | )
25 |
26 | func IsPipeInput() bool {
27 | fileInfo, err := os.Stdin.Stat()
28 | if err != nil {
29 | return false
30 | }
31 | return (fileInfo.Mode() & os.ModeNamedPipe) == os.ModeNamedPipe
32 | }
33 |
34 | // IsIPhoneOS 是否为苹果移动设备
35 | func IsIPhoneOS() bool {
36 | if runtime.GOOS == "darwin" && (runtime.GOARCH == "arm" || runtime.GOARCH == "arm64") {
37 | _, err := os.Stat("Info.plist")
38 | return err == nil
39 | }
40 | return false
41 | }
42 |
43 | // ChWorkDir 切换回工作目录
44 | func ChWorkDir() {
45 | if !IsIPhoneOS() {
46 | return
47 | }
48 |
49 | dir, err := filepath.Abs("")
50 | if err != nil {
51 | return
52 | }
53 |
54 | subPath := filepath.Dir(os.Args[0])
55 | os.Chdir(strings.TrimSuffix(dir, subPath))
56 | }
57 |
58 | // Executable 获取程序所在的真实目录或真实相对路径
59 | func Executable() string {
60 | executablePath, err := osext.Executable()
61 | if err != nil {
62 | logger.Verbosef("DEBUG: osext.Executable: %s\n", err)
63 | executablePath, err = filepath.Abs(filepath.Dir(os.Args[0]))
64 | if err != nil {
65 | logger.Verbosef("DEBUG: filepath.Abs: %s\n", err)
66 | executablePath = filepath.Dir(os.Args[0])
67 | }
68 | }
69 |
70 | if IsIPhoneOS() {
71 | executablePath = filepath.Join(strings.TrimSuffix(executablePath, os.Args[0]), filepath.Base(os.Args[0]))
72 | }
73 |
74 | // 读取链接
75 | linkedExecutablePath, err := filepath.EvalSymlinks(executablePath)
76 | if err != nil {
77 | logger.Verbosef("DEBUG: filepath.EvalSymlinks: %s\n", err)
78 | return executablePath
79 | }
80 | return linkedExecutablePath
81 | }
82 |
83 | // ExecutablePath 获取程序所在目录
84 | func ExecutablePath() string {
85 | return filepath.Dir(Executable())
86 | }
87 |
88 | // ExecutablePathJoin 返回程序所在目录的子目录
89 | func ExecutablePathJoin(subPath string) string {
90 | return filepath.Join(ExecutablePath(), subPath)
91 | }
92 |
93 | // WalkDir 获取指定目录及所有子目录下的所有文件,可以匹配后缀过滤。
94 | // 支持 Linux/macOS 软链接
95 | func WalkDir(dirPth, suffix string) (files []string, err error) {
96 | files = make([]string, 0, 32)
97 | suffix = strings.ToUpper(suffix) //忽略后缀匹配的大小写
98 |
99 | var walkFunc filepath.WalkFunc
100 | walkFunc = func(filename string, fi os.FileInfo, err error) error { //遍历目录
101 | if err != nil {
102 | return err
103 | }
104 | if fi.IsDir() { // 忽略目录
105 | return nil
106 | }
107 | if fi.Mode()&os.ModeSymlink != 0 { // 读取 symbol link
108 | err = filepath.Walk(filename+string(os.PathSeparator), walkFunc)
109 | return err
110 | }
111 |
112 | if strings.HasSuffix(strings.ToUpper(fi.Name()), suffix) {
113 | files = append(files, path.Clean(filename))
114 | }
115 | return nil
116 | }
117 |
118 | err = filepath.Walk(dirPth, walkFunc)
119 | return files, err
120 | }
121 |
122 | // ConvertToUnixPathSeparator 将 windows 目录分隔符转换为 Unix 的
123 | func ConvertToUnixPathSeparator(p string) string {
124 | return strings.Replace(p, "\\", "/", -1)
125 | }
126 |
127 | // ConvertToWindowsPathSeparator 将路径中的所有分隔符转换成windows格式
128 | func ConvertToWindowsPathSeparator(p string) string {
129 | return strings.Replace(p, "/", "\\", -1)
130 | }
131 |
--------------------------------------------------------------------------------
/cmder/cmdutil/jsonhelper/jsonhelper.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package jsonhelper
15 |
16 | import (
17 | "github.com/json-iterator/go"
18 | "io"
19 | )
20 |
21 | // UnmarshalData 将 r 中的 json 格式的数据, 解析到 data
22 | func UnmarshalData(r io.Reader, data interface{}) error {
23 | d := jsoniter.NewDecoder(r)
24 | return d.Decode(data)
25 | }
26 |
27 | // MarshalData 将 data, 生成 json 格式的数据, 写入 w 中
28 | func MarshalData(w io.Writer, data interface{}) error {
29 | e := jsoniter.NewEncoder(w)
30 | return e.Encode(data)
31 | }
32 |
--------------------------------------------------------------------------------
/docker/sync/Dockerfile:
--------------------------------------------------------------------------------
1 | # alpine:3.21
2 |
3 | # 参数
4 | ARG DOCKER_IMAGE_HASH
5 |
6 | #FROM alpine@sha256:$DOCKER_IMAGE_HASH
7 | #FROM alpine
8 | FROM alpine:$DOCKER_IMAGE_HASH
9 |
10 | LABEL author="tickstep"
11 | LABEL email="tickstep@outlook.com"
12 | LABEL version="1.0"
13 | LABEL description="sync & backup service for aliyun cloud drive"
14 |
15 | # 时区
16 | ENV TZ=Asia/Shanghai
17 | # 手动下载tzdata安装包,注意要下载对应架构的: https://dl-cdn.alpinelinux.org/alpine/v3.15/community/
18 | RUN apk add -U tzdata
19 | # 更新Alpine的软件源为国内镜像站点,提高下载速度。安装tzdata包
20 | # RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
21 | # RUN apk add --no-cache tzdata
22 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
23 |
24 | # 创建运行目录
25 | RUN mkdir -p /home/app
26 | VOLUME /home/app
27 | WORKDIR /home/app
28 | RUN cd /home/app
29 |
30 | # 创建配置文件目录
31 | RUN mkdir -p /home/app/config
32 |
33 | # 创建数据文件夹
34 | RUN mkdir -p /home/app/data
35 | RUN chmod 777 /home/app/data
36 |
37 | # 复制文件
38 | COPY ./docker/sync/app.sh /home/app/app.sh
39 | RUN chmod +x /home/app/app.sh
40 | COPY ./docker/sync/health_check.sh /home/app/health_check.sh
41 | RUN chmod +x /home/app/health_check.sh
42 |
43 | COPY ./out/binary_files/aliyunpan /home/app
44 | RUN mkdir -p /home/app/config/plugin
45 | COPY ./out/binary_files/plugin /home/app/config/plugin
46 | RUN mkdir -p /home/app/config/sync_drive
47 | COPY ./out/binary_files/sync_drive /home/app/config/sync_drive
48 | #RUN chmod +x /home/app/aliyunpan
49 |
50 | # 健康检查
51 | HEALTHCHECK --start-period=5s --interval=10s --timeout=5s --retries=3 CMD /bin/sh /home/app/health_check.sh
52 |
53 | # 环境变量
54 | ENV ALIYUNPAN_DOCKER=1
55 | ENV ALIYUNPAN_CONFIG_DIR=/home/app/config
56 |
57 | ENV ALIYUNPAN_DOWNLOAD_PARALLEL=2
58 | ENV ALIYUNPAN_UPLOAD_PARALLEL=2
59 | ENV ALIYUNPAN_DOWNLOAD_BLOCK_SIZE=1024
60 | ENV ALIYUNPAN_UPLOAD_BLOCK_SIZE=10240
61 | ENV ALIYUNPAN_LOCAL_DIR=/home/app/data
62 | ENV ALIYUNPAN_PAN_DIR=/sync_drive
63 | ENV ALIYUNPAN_SYNC_MODE=upload
64 | ENV ALIYUNPAN_SYNC_POLICY=increment
65 | ENV ALIYUNPAN_SYNC_DRIVE=backup
66 | ENV ALIYUNPAN_SYNC_CYCLE=infinity
67 | ENV ALIYUNPAN_SYNC_PRIORITY=time
68 | ENV ALIYUNPAN_SYNC_LOG=false
69 | ENV ALIYUNPAN_LOCAL_DELAY_TIME=3
70 | ENV ALIYUNPAN_SCAN_INTERVAL_TIME=1
71 | ENV ALIYUNPAN_DEVICE_ID=""
72 | ENV ALIYUNPAN_IP_TYPE="ipv4"
73 |
74 | # 运行
75 | ENTRYPOINT ./app.sh
--------------------------------------------------------------------------------
/docker/sync/app.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | cd /home/app
3 | chmod +x ./aliyunpan
4 |
5 | # sync config file
6 | readonly sync_drive_config_file="$ALIYUNPAN_CONFIG_DIR/sync_drive/sync_drive_config.json"
7 | if test -s $sync_drive_config_file
8 | then
9 | echo "using existed sync_drive_config.json file"
10 | else
11 | echo "generate sync_drive_config.json file"
12 | tee $sync_drive_config_file << EOF
13 | {
14 | "configVer": "1.0",
15 | "syncTaskList": [
16 | {
17 | "name": "阿里云盘备份",
18 | "id": "5b2d7c10-e927-4e72-8f9d-5abb3bb04815",
19 | "localFolderPath": "$ALIYUNPAN_LOCAL_DIR",
20 | "panFolderPath": "$ALIYUNPAN_PAN_DIR",
21 | "mode": "$ALIYUNPAN_SYNC_MODE",
22 | "policy": "$ALIYUNPAN_SYNC_POLICY",
23 | "driveName": "$ALIYUNPAN_SYNC_DRIVE",
24 | "lastSyncTime": "2022-06-12 19:28:20"
25 | }
26 | ]
27 | }
28 | EOF
29 | fi
30 |
31 | sleep 2s
32 |
33 | # device-id
34 | #if [[ -z $ALIYUNPAN_DEVICE_ID ]];
35 | #then
36 | # echo "the program use random device id"
37 | #else
38 | # echo "set device id"
39 | # ./aliyunpan config set -device_id ${ALIYUNPAN_DEVICE_ID}
40 | #fi
41 |
42 | # show docker IPs
43 | ./aliyunpan tool getip
44 |
45 | # check login already or not
46 | ./aliyunpan who
47 | if [ $? -eq 0 ]
48 | then
49 | echo "cache token is valid, not need to re-login"
50 | else
51 | echo "token is invalid, please use the valid aliyunpan_config.json file and retry"
52 | # ./aliyunpan login -RefreshToken=${ALIYUNPAN_REFRESH_TOKEN}
53 | fi
54 |
55 | if [ "$ALIYUNPAN_SYNC_LOG" = "true" ]
56 | then
57 | ./aliyunpan config set -file_record_config 1
58 | else
59 | ./aliyunpan config set -file_record_config 2
60 | fi
61 |
62 | # IP type
63 | ./aliyunpan config set -ip_type ${ALIYUNPAN_IP_TYPE}
64 |
65 | # run
66 | ./aliyunpan sync start -dp ${ALIYUNPAN_DOWNLOAD_PARALLEL} -up ${ALIYUNPAN_UPLOAD_PARALLEL} -dbs ${ALIYUNPAN_DOWNLOAD_BLOCK_SIZE} -ubs ${ALIYUNPAN_UPLOAD_BLOCK_SIZE} -log ${ALIYUNPAN_SYNC_LOG} -ldt ${ALIYUNPAN_LOCAL_DELAY_TIME} -cycle ${ALIYUNPAN_SYNC_CYCLE} -sit ${ALIYUNPAN_SCAN_INTERVAL_TIME}
67 |
--------------------------------------------------------------------------------
/docker/sync/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | sync:
5 | image: tickstep/aliyunpan-sync:v0.1.6
6 | container_name: aliyunpan-sync
7 | restart: always
8 | volumes:
9 | # (必须)映射的本地目录
10 | - ./data:/home/app/data:rw
11 | # (可选)可以指定JS插件sync_handler.js用于过滤文件,详见插件说明
12 | #- ./plugin/js/sync_handler.js:/home/app/config/plugin/js/sync_handler.js
13 | # (推荐)挂载sync_drive同步数据库到本地,这样即使容器销毁,同步数据库还可以用于以后使用
14 | #- ./sync_drive:/home/app/config/sync_drive
15 | # (必须)映射token凭据文件
16 | - /your/file/path/for/aliyunpan_config.json:/home/app/config/aliyunpan_config.json
17 | environment:
18 | # 时区,东8区
19 | - TZ=Asia/Shanghai
20 | # 下载文件并发数
21 | - ALIYUNPAN_DOWNLOAD_PARALLEL=2
22 | # 上传文件并发数
23 | - ALIYUNPAN_UPLOAD_PARALLEL=2
24 | # 下载数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
25 | - ALIYUNPAN_DOWNLOAD_BLOCK_SIZE=1024
26 | # 上传数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
27 | - ALIYUNPAN_UPLOAD_BLOCK_SIZE=10240
28 | # 指定网盘文件夹作为备份目标目录,不要指定根目录
29 | - ALIYUNPAN_PAN_DIR=/my_sync_dir
30 | # 备份模式:upload(备份本地文件到云盘), download(备份云盘文件到本地)
31 | - ALIYUNPAN_SYNC_MODE=upload
32 | # 备份策略: exclusive(排他备份文件,目标目录多余的文件会被删除),increment(增量备份文件,目标目录多余的文件不会被删除)
33 | - ALIYUNPAN_SYNC_POLICY=increment
34 | # 备份周期, 支持两种: infinity(永久循环备份),onetime(只运行一次备份)
35 | - ALIYUNPAN_SYNC_CYCLE=infinity
36 | # 网盘:backup(备份盘), resource(资源盘)
37 | - ALIYUNPAN_SYNC_DRIVE=backup
38 | # 是否显示文件备份过程日志,true-显示,false-不显示
39 | - ALIYUNPAN_SYNC_LOG=true
40 | # 本地文件修改检测延迟间隔,单位秒。如果本地文件会被频繁修改,例如录制视频文件,配置好该时间可以避免上传未录制好的文件
41 | - ALIYUNPAN_LOCAL_DELAY_TIME=3
42 | # 扫描文件间隔时间,单位:分钟
43 | - ALIYUNPAN_SCAN_INTERVAL_TIME=1
--------------------------------------------------------------------------------
/docker/sync/health_check.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # 检查aliyunpan进程是否存在
3 | ps | awk '{print $4}' | grep aliyunpan
4 | if [ $? == 0 ]
5 | then
6 | echo $?
7 | exit 0
8 | else
9 | echo $?
10 | exit 1
11 | fi
12 |
13 |
14 |
--------------------------------------------------------------------------------
/docs/complie_project.md:
--------------------------------------------------------------------------------
1 | # 关于 Windows EXE ICO 和应用信息编译
2 | 为了编译出来的windows的exe文件带有ico和应用程序信息,需要使用 github.com/josephspurrier/goversioninfo/cmd/goversioninfo 工具
3 |
4 | 工具安装,运行下面的命令即可生成工具。也可以直接用 bin/ 文件夹下面的编译好的
5 | ```
6 | go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo
7 | go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo
8 | ```
9 |
10 | versioninfo.json - 里面有exe程序信息以及ico的配置
11 | 使用 goversioninfo 工具运行以下命令
12 | ```
13 | goversioninfo -o=resource_windows_386.syso
14 | goversioninfo -64 -o=resource_windows_amd64.syso
15 | ```
16 | 即可编译出.syso资源库,再使用 go build 编译之后,exe文件就会拥有应用程序信息和ico图标
--------------------------------------------------------------------------------
/entitlements.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | application-identifier
6 | com.tickstep.aliyunpan
7 | get-task-allow
8 |
9 | platform-application
10 |
11 | keychain-access-groups
12 |
13 | com.tickstep.aliyunpan
14 |
15 |
16 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/tickstep/aliyunpan
2 |
3 | go 1.22
4 |
5 | require (
6 | github.com/GeertJohan/go.incremental v1.0.0
7 | github.com/adrg/xdg v0.4.0
8 | github.com/deckarep/golang-set v1.8.0
9 | github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1
10 | github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e
11 | github.com/json-iterator/go v1.1.12
12 | github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1
13 | github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb
14 | github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce
15 | github.com/peterh/liner v1.2.1
16 | github.com/satori/go.uuid v1.2.0
17 | github.com/tickstep/aliyunpan-api v0.2.6
18 | github.com/tickstep/bolt v1.3.4
19 | github.com/tickstep/library-go v0.1.3
20 | github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f
21 | )
22 |
23 | require (
24 | github.com/boltdb/bolt v1.3.1 // indirect
25 | github.com/btcsuite/btcd v0.22.1 // indirect
26 | github.com/cpuguy83/go-md2man v1.0.10 // indirect
27 | github.com/denisbrodbeck/machineid v1.0.1 // indirect
28 | github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
29 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
30 | github.com/mattn/go-runewidth v0.0.9 // indirect
31 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
32 | github.com/modern-go/reflect2 v1.0.2 // indirect
33 | github.com/russross/blackfriday v1.5.2 // indirect
34 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
35 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect
36 | golang.org/x/text v0.3.7 // indirect
37 | )
38 |
39 | //replace github.com/boltdb/bolt => github.com/tickstep/bolt v1.3.4
40 | //replace github.com/tickstep/bolt => /Users/tickstep/Documents/Workspace/go/projects/bolt
41 | //replace github.com/tickstep/library-go => /Users/tickstep/Documents/Workspace/go/projects/library-go
42 | //replace github.com/tickstep/aliyunpan-api => /Users/tickstep/Documents/Workspace/go/projects/aliyunpan-api
43 |
--------------------------------------------------------------------------------
/internal/command/cd.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package command
15 |
16 | import (
17 | "fmt"
18 | "github.com/tickstep/aliyunpan/cmder"
19 | "github.com/tickstep/aliyunpan/internal/config"
20 | "github.com/urfave/cli"
21 | )
22 |
23 | func CmdCd() cli.Command {
24 | return cli.Command{
25 | Name: "cd",
26 | Category: "阿里云盘",
27 | Usage: "切换工作目录",
28 | Description: `
29 | aliyunpan cd <目录, 绝对路径或相对路径>
30 |
31 | 示例:
32 |
33 | 切换 /我的资源 工作目录:
34 | aliyunpan cd /我的资源
35 |
36 | 切换 /我的资源 工作目录,使用通配符:
37 | aliyunpan cd /我的*
38 |
39 | 切换上级目录:
40 | aliyunpan cd ..
41 |
42 | 切换根目录:
43 | aliyunpan cd /
44 | `,
45 | Before: ReloadConfigFunc,
46 | After: SaveConfigFunc,
47 | Action: func(c *cli.Context) error {
48 | if c.NArg() == 0 {
49 | cli.ShowCommandHelp(c, c.Command.Name)
50 | return nil
51 | }
52 | if config.Config.ActiveUser() == nil {
53 | fmt.Println("未登录账号")
54 | return nil
55 | }
56 | RunChangeDirectory(parseDriveId(c), c.Args().Get(0))
57 | return nil
58 | },
59 | Flags: []cli.Flag{
60 | cli.StringFlag{
61 | Name: "driveId",
62 | Usage: "网盘ID",
63 | Value: "",
64 | },
65 | },
66 | }
67 | }
68 |
69 | func CmdPwd() cli.Command {
70 | return cli.Command{
71 | Name: "pwd",
72 | Usage: "输出工作目录",
73 | UsageText: cmder.App().Name + " pwd",
74 | Category: "阿里云盘",
75 | Before: ReloadConfigFunc,
76 | Action: func(c *cli.Context) error {
77 | if config.Config.ActiveUser() == nil {
78 | fmt.Println("未登录账号")
79 | return nil
80 | }
81 | activeUser := config.Config.ActiveUser()
82 | if activeUser.IsFileDriveActive() {
83 | fmt.Println(activeUser.Workdir)
84 | } else if activeUser.IsResourceDriveActive() {
85 | fmt.Println(activeUser.ResourceWorkdir)
86 | } else if activeUser.IsAlbumDriveActive() {
87 | fmt.Println(activeUser.AlbumWorkdir)
88 | }
89 | return nil
90 | },
91 | }
92 | }
93 |
94 | func RunChangeDirectory(driveId, targetPath string) {
95 | user := config.Config.ActiveUser()
96 | targetPath = user.PathJoin(driveId, targetPath)
97 |
98 | // 获取目标路径文件信息
99 | targetPathInfo, err := user.PanClient().OpenapiPanClient().FileInfoByPath(driveId, targetPath)
100 | if err != nil {
101 | fmt.Println(err)
102 | return
103 | }
104 |
105 | // 适配通配符路径获取目标文件信息(弃用,容易触发风控)
106 | //files, err := matchPathByShellPattern(driveId, targetPath)
107 | //if err != nil {
108 | // fmt.Println(err)
109 | // return
110 | //}
111 | //
112 | //var targetPathInfo *aliyunpan.FileEntity
113 | //if len(files) == 1 {
114 | // targetPathInfo = files[0]
115 | //} else {
116 | // for _, f := range files {
117 | // if f.IsFolder() {
118 | // targetPathInfo = f
119 | // break
120 | // }
121 | // }
122 | //}
123 |
124 | if targetPathInfo == nil {
125 | fmt.Println("路径不存在")
126 | return
127 | }
128 |
129 | if !targetPathInfo.IsFolder() {
130 | fmt.Printf("错误: %s 不是一个目录 (文件夹)\n", targetPathInfo.Path)
131 | return
132 | }
133 |
134 | if user.IsFileDriveActive() {
135 | user.Workdir = targetPathInfo.Path
136 | user.WorkdirFileEntity = *targetPathInfo
137 | } else if user.IsResourceDriveActive() {
138 | user.ResourceWorkdir = targetPathInfo.Path
139 | user.ResourceWorkdirFileEntity = *targetPathInfo
140 | } else if user.IsAlbumDriveActive() {
141 | user.AlbumWorkdir = targetPathInfo.Path
142 | user.AlbumWorkdirFileEntity = *targetPathInfo
143 | }
144 |
145 | fmt.Printf("改变工作目录: %s\n", targetPathInfo.Path)
146 | }
147 |
--------------------------------------------------------------------------------
/internal/command/command_test.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestRapidUploadItem_createRapidUploadLink(t *testing.T) {
8 |
9 | }
10 |
11 | func TestRapidUploadItem_newRapidUploadItem(t *testing.T) {
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/internal/command/cp.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package command
15 |
16 | import (
17 | "fmt"
18 | "github.com/tickstep/aliyunpan-api/aliyunpan"
19 | "github.com/tickstep/aliyunpan/cmder/cmdtable"
20 | "github.com/tickstep/aliyunpan/internal/config"
21 | "github.com/urfave/cli"
22 | "os"
23 | "strconv"
24 | )
25 |
26 | func CmdCp() cli.Command {
27 | return cli.Command{
28 | Name: "cp",
29 | Usage: "复制文件/目录",
30 | UsageText: `
31 | aliyunpan cp <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>`,
32 | Description: `
33 | 同网盘内复制文件或者目录。支持通配符匹配复制文件,通配符当前只能匹配文件名,不能匹配文件路径。
34 |
35 | 示例:
36 |
37 | 将 /我的资源/1.mp4 复制到 根目录 /
38 | aliyunpan cp /我的资源/1.mp4 /
39 |
40 | 将 /我的资源 目录下所有的.png文件 复制到 /我的图片 目录下面,使用通配符匹配
41 | aliyunpan cp /我的资源/*.png /我的图片
42 | `,
43 | Category: "阿里云盘",
44 | Before: ReloadConfigFunc,
45 | Action: func(c *cli.Context) error {
46 | if c.NArg() <= 1 {
47 | cli.ShowCommandHelp(c, c.Command.Name)
48 | return nil
49 | }
50 | if config.Config.ActiveUser() == nil {
51 | fmt.Println("未登录账号")
52 | return nil
53 | }
54 | RunCopy(parseDriveId(c), c.Args()...)
55 | return nil
56 | },
57 | Flags: []cli.Flag{
58 | cli.StringFlag{
59 | Name: "driveId",
60 | Usage: "网盘ID",
61 | Value: "",
62 | },
63 | },
64 | }
65 | }
66 |
67 | // RunCopy 执行复制文件/目录
68 | func RunCopy(driveId string, paths ...string) {
69 | activeUser := GetActiveUser()
70 | cacheCleanPaths := []string{}
71 | opFileList, targetFile, _, err := getFileInfo(driveId, paths...)
72 | if err != nil {
73 | fmt.Println(err)
74 | return
75 | }
76 | if targetFile == nil {
77 | fmt.Println("目标文件不存在")
78 | return
79 | }
80 | if opFileList == nil || len(opFileList) == 0 {
81 | fmt.Println("没有有效的文件可复制")
82 | return
83 | }
84 | cacheCleanPaths = append(cacheCleanPaths, targetFile.Path)
85 |
86 | failedCopyFiles := []*aliyunpan.FileEntity{}
87 | successCopyFiles := []*aliyunpan.FileEntity{}
88 | for _, mfi := range opFileList {
89 | _, er := activeUser.PanClient().OpenapiPanClient().FileCopy(&aliyunpan.FileCopyParam{
90 | DriveId: driveId,
91 | FileId: mfi.FileId,
92 | ToParentFileId: targetFile.FileId,
93 | })
94 | if er == nil {
95 | successCopyFiles = append(successCopyFiles, mfi)
96 | } else {
97 | failedCopyFiles = append(failedCopyFiles, mfi)
98 | }
99 | }
100 |
101 | if len(failedCopyFiles) > 0 {
102 | fmt.Println("以下文件复制失败:")
103 | for _, f := range failedCopyFiles {
104 | fmt.Println(f.Path)
105 | }
106 | fmt.Println("")
107 | }
108 | if len(successCopyFiles) > 0 {
109 | pnt := func() {
110 | tb := cmdtable.NewTable(os.Stdout)
111 | tb.SetHeader([]string{"#", "文件/目录"})
112 | for k, rs := range successCopyFiles {
113 | tb.Append([]string{strconv.Itoa(k + 1), rs.Path})
114 | }
115 | tb.Render()
116 | }
117 | fmt.Println("操作成功, 以下文件已复制到目标目录: ", targetFile.Path)
118 | pnt()
119 | activeUser.DeleteCache(cacheCleanPaths)
120 | } else {
121 | fmt.Println("无法复制文件,请稍后重试")
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/internal/command/drive_list.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package command
15 |
16 | import (
17 | "fmt"
18 | "github.com/olekukonko/tablewriter"
19 | "github.com/tickstep/aliyunpan-api/aliyunpan"
20 | "github.com/tickstep/aliyunpan/cmder/cmdtable"
21 | "github.com/tickstep/aliyunpan/internal/config"
22 | "github.com/urfave/cli"
23 | "strconv"
24 | "strings"
25 | )
26 |
27 | func CmdDrive() cli.Command {
28 | return cli.Command{
29 | Name: "drive",
30 | Usage: "切换网盘(备份盘/资源库)",
31 | Description: `
32 | 切换已登录账号的阿里云盘的工作网盘(备份盘/资源库)
33 | 如果运行该条命令没有提供参数, 程序将会列出所有的网盘列表, 供选择切换.
34 |
35 | 示例:
36 | aliyunpan drive
37 | aliyunpan drive
38 | `,
39 | Category: "阿里云盘账号",
40 | Before: ReloadConfigFunc,
41 | After: SaveConfigFunc,
42 | Action: func(c *cli.Context) error {
43 | inputData := c.Args().Get(0)
44 | targetDriveId := strings.TrimSpace(inputData)
45 | RunSwitchDriveList(targetDriveId)
46 | return nil
47 | },
48 | }
49 | }
50 |
51 | func RunSwitchDriveList(targetDriveId string) {
52 | currentDriveId := config.Config.ActiveUser().ActiveDriveId
53 | var activeDriveInfo *config.DriveInfo = nil
54 | driveList, renderStr := getDriveOptionList()
55 |
56 | if driveList == nil || len(driveList) == 0 {
57 | fmt.Println("切换网盘失败")
58 | return
59 | }
60 |
61 | if targetDriveId == "" {
62 | // show option list
63 | fmt.Println(renderStr)
64 |
65 | // 提示输入 index
66 | var index string
67 | fmt.Printf("输入要切换的网盘 # 值 > ")
68 | _, err := fmt.Scanln(&index)
69 | if err != nil {
70 | return
71 | }
72 |
73 | if n, err1 := strconv.Atoi(index); err1 == nil && (n-1) >= 0 && (n-1) < len(driveList) {
74 | activeDriveInfo = driveList[n-1]
75 | } else {
76 | fmt.Printf("切换网盘失败, 请检查 # 值是否正确\n")
77 | return
78 | }
79 | } else {
80 | // 直接切换
81 | for _, driveInfo := range driveList {
82 | if driveInfo.DriveId == targetDriveId {
83 | activeDriveInfo = driveInfo
84 | break
85 | }
86 | }
87 | }
88 |
89 | if activeDriveInfo == nil {
90 | fmt.Printf("切换网盘失败\n")
91 | return
92 | }
93 |
94 | config.Config.ActiveUser().ActiveDriveId = activeDriveInfo.DriveId
95 | activeUser := config.Config.ActiveUser()
96 | if currentDriveId != config.Config.ActiveUser().ActiveDriveId {
97 | // clear the drive work path
98 | if activeUser.IsFileDriveActive() {
99 | if activeUser.Workdir == "" {
100 | config.Config.ActiveUser().Workdir = "/"
101 | config.Config.ActiveUser().WorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
102 | }
103 | } else if activeUser.IsResourceDriveActive() {
104 | if activeUser.ResourceWorkdir == "" {
105 | config.Config.ActiveUser().ResourceWorkdir = "/"
106 | config.Config.ActiveUser().ResourceWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
107 | }
108 | } else if activeUser.IsAlbumDriveActive() {
109 | if activeUser.AlbumWorkdir == "" {
110 | config.Config.ActiveUser().AlbumWorkdir = "/"
111 | config.Config.ActiveUser().AlbumWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
112 | }
113 | }
114 | }
115 | fmt.Printf("切换到网盘:%s\n", activeDriveInfo.DriveName)
116 | }
117 |
118 | func getDriveOptionList() (config.DriveInfoList, string) {
119 | activeUser := config.Config.ActiveUser()
120 |
121 | driveList := activeUser.DriveList
122 | builder := &strings.Builder{}
123 | tb := cmdtable.NewTable(builder)
124 | tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_CENTER})
125 | tb.SetHeader([]string{"#", "drive_id", "网盘名称"})
126 |
127 | for k, info := range driveList {
128 | if info.IsAlbumDrive() {
129 | continue
130 | }
131 | tb.Append([]string{strconv.Itoa(k + 1), info.DriveId, info.DriveName})
132 | }
133 | tb.Render()
134 | return driveList, builder.String()
135 | }
136 |
--------------------------------------------------------------------------------
/internal/command/mkdir.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package command
15 |
16 | import (
17 | "fmt"
18 | "github.com/tickstep/aliyunpan-api/aliyunpan"
19 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
20 | "github.com/tickstep/aliyunpan/cmder"
21 | "github.com/tickstep/aliyunpan/internal/config"
22 | "github.com/urfave/cli"
23 | )
24 |
25 | func CmdMkdir() cli.Command {
26 | return cli.Command{
27 | Name: "mkdir",
28 | Usage: "创建目录",
29 | UsageText: cmder.App().Name + " mkdir <目录>",
30 | Category: "阿里云盘",
31 | Before: ReloadConfigFunc,
32 | Action: func(c *cli.Context) error {
33 | if c.NArg() == 0 {
34 | cli.ShowCommandHelp(c, c.Command.Name)
35 | return nil
36 | }
37 | if config.Config.ActiveUser() == nil {
38 | fmt.Println("未登录账号")
39 | return nil
40 | }
41 | RunMkdir(parseDriveId(c), c.Args().Get(0))
42 | return nil
43 | },
44 | Flags: []cli.Flag{
45 | cli.StringFlag{
46 | Name: "driveId",
47 | Usage: "网盘ID",
48 | Value: "",
49 | },
50 | },
51 | }
52 | }
53 |
54 | func RunMkdir(driveId, name string) {
55 | activeUser := GetActiveUser()
56 | fullpath := activeUser.PathJoin(driveId, name)
57 | rs := &aliyunpan.MkdirResult{}
58 | err := apierror.NewFailedApiError("")
59 | rs, err = activeUser.PanClient().OpenapiPanClient().MkdirByFullPath(driveId, fullpath)
60 |
61 | if err != nil {
62 | fmt.Println("创建文件夹失败:" + err.Error())
63 | return
64 | }
65 |
66 | if rs.FileId != "" {
67 | fmt.Println("创建文件夹成功: ", fullpath)
68 |
69 | // cache
70 | activeUser.DeleteCache(GetAllPathFolderByPath(fullpath))
71 | } else {
72 | fmt.Println("创建文件夹失败: ", fullpath)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/internal/command/mv.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package command
15 |
16 | import (
17 | "fmt"
18 | "github.com/tickstep/aliyunpan-api/aliyunpan"
19 | "github.com/tickstep/aliyunpan/cmder/cmdtable"
20 | "github.com/tickstep/aliyunpan/internal/config"
21 | "github.com/urfave/cli"
22 | "os"
23 | "path"
24 | "strconv"
25 | )
26 |
27 | func CmdMv() cli.Command {
28 | return cli.Command{
29 | Name: "mv",
30 | Usage: "移动文件/目录",
31 | UsageText: `
32 | aliyunpan mv <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>`,
33 | Description: `
34 | 注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败。支持通配符匹配移动文件,通配符当前只能匹配文件名,不能匹配文件路径。
35 |
36 | 示例:
37 |
38 | 将 /我的资源/1.mp4 移动到 根目录 /
39 | aliyunpan mv /我的资源/1.mp4 /
40 |
41 | 将 /我的资源 目录下所有的.png文件 移动到 /我的图片 目录下面,使用通配符匹配
42 | aliyunpan mv /我的资源/*.png /我的图片
43 | `,
44 | Category: "阿里云盘",
45 | Before: ReloadConfigFunc,
46 | Action: func(c *cli.Context) error {
47 | if c.NArg() <= 1 {
48 | cli.ShowCommandHelp(c, c.Command.Name)
49 | return nil
50 | }
51 | if config.Config.ActiveUser() == nil {
52 | fmt.Println("未登录账号")
53 | return nil
54 | }
55 | RunMove(parseDriveId(c), c.Args()...)
56 | return nil
57 | },
58 | Flags: []cli.Flag{
59 | cli.StringFlag{
60 | Name: "driveId",
61 | Usage: "网盘ID",
62 | Value: "",
63 | },
64 | },
65 | }
66 | }
67 |
68 | // RunMove 执行移动文件/目录
69 | func RunMove(driveId string, paths ...string) {
70 | activeUser := GetActiveUser()
71 | cacheCleanPaths := []string{}
72 | opFileList, targetFile, _, err := getFileInfo(driveId, paths...)
73 | if err != nil {
74 | fmt.Println(err)
75 | return
76 | }
77 | if targetFile == nil {
78 | fmt.Println("目标文件不存在")
79 | return
80 | }
81 | if opFileList == nil || len(opFileList) == 0 {
82 | fmt.Println("没有有效的文件可移动")
83 | return
84 | }
85 | cacheCleanPaths = append(cacheCleanPaths, targetFile.Path)
86 |
87 | failedMoveFiles := []*aliyunpan.FileEntity{}
88 | successMoveFiles := []*aliyunpan.FileEntity{}
89 | for _, mfi := range opFileList {
90 | fmr, er := activeUser.PanClient().OpenapiPanClient().FileMove(&aliyunpan.FileMoveParam{
91 | DriveId: driveId,
92 | FileId: mfi.FileId,
93 | ToDriveId: driveId,
94 | ToParentFileId: targetFile.FileId,
95 | })
96 | if er != nil || !fmr.Success {
97 | failedMoveFiles = append(failedMoveFiles, mfi)
98 | } else {
99 | successMoveFiles = append(successMoveFiles, mfi)
100 | }
101 | cacheCleanPaths = append(cacheCleanPaths, path.Dir(mfi.Path))
102 | }
103 |
104 | if len(failedMoveFiles) > 0 {
105 | fmt.Println("以下文件移动失败:")
106 | for _, f := range failedMoveFiles {
107 | fmt.Println(f.Path)
108 | }
109 | fmt.Println("")
110 | }
111 | if len(successMoveFiles) > 0 {
112 | pnt := func() {
113 | tb := cmdtable.NewTable(os.Stdout)
114 | tb.SetHeader([]string{"#", "文件/目录"})
115 | for k, rs := range successMoveFiles {
116 | tb.Append([]string{strconv.Itoa(k + 1), rs.Path})
117 | }
118 | tb.Render()
119 | }
120 | fmt.Println("操作成功, 以下文件已移动到目标目录: ", targetFile.Path)
121 | pnt()
122 | } else {
123 | fmt.Println("无法移动文件,请稍后重试")
124 | }
125 | activeUser.DeleteCache(cacheCleanPaths)
126 | }
127 |
128 | func getFileInfo(driveId string, paths ...string) (opFileList []*aliyunpan.FileEntity, targetFile *aliyunpan.FileEntity, failedPaths []string, error error) {
129 | if len(paths) <= 1 {
130 | return nil, nil, nil, fmt.Errorf("请指定目标文件夹路径")
131 | }
132 | activeUser := GetActiveUser()
133 | // the last one is the target file path
134 | targetFilePath := path.Clean(paths[len(paths)-1])
135 | absolutePath := activeUser.PathJoin(driveId, targetFilePath)
136 | targetFile, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, absolutePath)
137 | if err != nil || !targetFile.IsFolder() {
138 | return nil, nil, nil, fmt.Errorf("指定目标文件夹不存在")
139 | }
140 |
141 | for idx := 0; idx < (len(paths) - 1); idx++ {
142 | absolutePath = path.Clean(activeUser.PathJoin(driveId, paths[idx]))
143 | fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
144 | if err1 != nil {
145 | failedPaths = append(failedPaths, absolutePath)
146 | continue
147 | }
148 | if fileList == nil || len(fileList) == 0 {
149 | // 文件不存在
150 | failedPaths = append(failedPaths, absolutePath)
151 | continue
152 | }
153 | for _, f := range fileList {
154 | // 移动匹配的文件
155 | opFileList = append(opFileList, f)
156 | }
157 | }
158 | return
159 | }
160 |
--------------------------------------------------------------------------------
/internal/command/quota.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package command
15 |
16 | import (
17 | "fmt"
18 | "github.com/tickstep/aliyunpan/internal/config"
19 | "github.com/tickstep/library-go/converter"
20 | "github.com/urfave/cli"
21 | )
22 |
23 | type QuotaInfo struct {
24 | // 已使用个人空间大小
25 | UsedSize int64
26 | // 个人空间总大小
27 | Quota int64
28 | }
29 |
30 | func CmdQuota() cli.Command {
31 | return cli.Command{
32 | Name: "quota",
33 | Usage: "获取当前帐号空间配额",
34 | Description: "获取网盘的总储存空间, 和已使用的储存空间",
35 | Category: "阿里云盘账号",
36 | Before: ReloadConfigFunc,
37 | Action: func(c *cli.Context) error {
38 | if config.Config.ActiveUser() == nil {
39 | fmt.Println("未登录账号")
40 | return nil
41 | }
42 | q, err := RunGetQuotaInfo()
43 | if err == nil {
44 | fmt.Printf("账号: %s, uid: %s, 个人空间总额: %s, 个人空间已使用: %s, 比率: %.2f%%\n",
45 | config.Config.ActiveUser().Nickname, config.Config.ActiveUser().UserId,
46 | converter.ConvertFileSize(q.Quota, 2), converter.ConvertFileSize(q.UsedSize, 2),
47 | 100*float64(q.UsedSize)/float64(q.Quota))
48 | }
49 | return nil
50 | },
51 | }
52 | }
53 |
54 | func RunGetQuotaInfo() (quotaInfo *QuotaInfo, error error) {
55 | user, err := GetActivePanClient().OpenapiPanClient().GetUserInfo()
56 | if err != nil {
57 | return nil, err
58 | }
59 | return &QuotaInfo{
60 | UsedSize: int64(user.UsedSize),
61 | Quota: int64(user.TotalSize),
62 | }, nil
63 | }
64 |
--------------------------------------------------------------------------------
/internal/command/rename_test.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "fmt"
5 | "github.com/tickstep/aliyunpan-api/aliyunpan"
6 | "sort"
7 | "testing"
8 | )
9 |
10 | func TestRenameSort(t *testing.T) {
11 | files := fileArray{}
12 | files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "0我的文件01.txt"}))
13 | files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "4我的文件03.txt"}))
14 | files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "3我的文件02.txt"}))
15 | files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "1我的文件00.txt"}))
16 |
17 | sort.Sort(files)
18 |
19 | for _, f := range files {
20 | fmt.Println(f.file.FileName)
21 | }
22 | }
23 | func TestRenameNum0(t *testing.T) {
24 | fmt.Println(replaceNumStr("我的文件###.txt", 2))
25 | }
26 | func TestRenameNum1(t *testing.T) {
27 | fmt.Println(replaceNumStr("我的##文件###.txt", 123))
28 | }
29 | func TestRenameNum2(t *testing.T) {
30 | fmt.Println(replaceNumStr("我的文件###.txt", 1233))
31 | }
32 | func TestRenameNum3(t *testing.T) {
33 | fmt.Println(replaceNumStr("我的文件[###].txt", 1233))
34 | }
35 | func TestRenameNum4(t *testing.T) {
36 | fmt.Println(replaceNumStr("我的文件.txt", 1233))
37 | }
38 |
39 | func TestRenameNum5(t *testing.T) {
40 | fmt.Println(replaceNumStr("", 1233))
41 | }
42 |
--------------------------------------------------------------------------------
/internal/command/rm_test.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestWildcard(t *testing.T) {
9 | fmt.Println(isIncludeFile("ab0.txt", "ab0.txt"))
10 | fmt.Println(isIncludeFile("*.zip", "aliyunpan-v0.0.1-darwin-macos-amd64.zip"))
11 | fmt.Println(isIncludeFile(".*.swp", ".vd.txt.swp"))
12 | fmt.Println(isIncludeFile("*.swp", ".swp"))
13 | fmt.Println(isIncludeFile("*.swp", ".1swp"))
14 |
15 | fmt.Println(isIncludeFile("a*b", "ab"))
16 | fmt.Println(isIncludeFile("a*b", "aab"))
17 | fmt.Println(isIncludeFile("a*b", "accccccccdb"))
18 | fmt.Println(isIncludeFile("a?b", "acb"))
19 | fmt.Println(isIncludeFile("a?b", "accb"))
20 | fmt.Println(isIncludeFile("a[xyz]b", "axb"))
21 | fmt.Println(isIncludeFile("ab[0-9].txt", "ab0.txt"))
22 |
23 | fmt.Println(isIncludeFile("a*b/ab[0-9].txt", "acb/ab0.txt"))
24 | fmt.Println(isIncludeFile("aliyunpan*", "aliyunpan-v0.0.1-darwin-macos-amd64[TNT].zip"))
25 | }
26 |
--------------------------------------------------------------------------------
/internal/command/user_info.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package command
15 |
16 | import (
17 | "fmt"
18 | "github.com/tickstep/aliyunpan/cmder"
19 | "github.com/tickstep/aliyunpan/internal/config"
20 | "github.com/urfave/cli"
21 | "os"
22 | "strconv"
23 | )
24 |
25 | func CmdLoglist() cli.Command {
26 | return cli.Command{
27 | Name: "loglist",
28 | Usage: "列出帐号列表",
29 | Description: "列出所有已登录的阿里账号",
30 | Category: "阿里云盘账号",
31 | Before: ReloadConfigFunc,
32 | Action: func(c *cli.Context) error {
33 | fmt.Println(config.Config.UserList.String())
34 | return nil
35 | },
36 | }
37 | }
38 |
39 | func CmdSu() cli.Command {
40 | return cli.Command{
41 | Name: "su",
42 | Usage: "切换阿里账号",
43 | Description: `
44 | 切换已登录的阿里账号:
45 | 如果运行该条命令没有提供参数, 程序将会列出所有的帐号, 供选择切换.
46 |
47 | 示例:
48 | aliyunpan su
49 | aliyunpan su
50 | `,
51 | Category: "阿里云盘账号",
52 | Before: ReloadConfigFunc,
53 | After: SaveConfigFunc,
54 | Action: func(c *cli.Context) error {
55 | if c.NArg() >= 2 {
56 | cli.ShowCommandHelp(c, c.Command.Name)
57 | return nil
58 | }
59 |
60 | numLogins := config.Config.NumLogins()
61 |
62 | if numLogins == 0 {
63 | fmt.Printf("未设置任何帐号, 不能切换\n")
64 | return nil
65 | }
66 |
67 | var (
68 | inputData = c.Args().Get(0)
69 | uid string
70 | )
71 |
72 | if c.NArg() == 1 {
73 | // 直接切换
74 | uid = inputData
75 | } else if c.NArg() == 0 {
76 | // 输出所有帐号供选择切换
77 | cli.HandleAction(cmder.App().Command("loglist").Action, c)
78 |
79 | // 提示输入 index
80 | var index string
81 | fmt.Printf("输入要切换帐号的 # 值 > ")
82 | _, err := fmt.Scanln(&index)
83 | if err != nil {
84 | return nil
85 | }
86 |
87 | if n, err1 := strconv.Atoi(index); err1 == nil && (n-1) >= 0 && (n-1) < numLogins {
88 | uid = config.Config.UserList[n-1].UserId
89 | } else {
90 | fmt.Printf("切换用户失败, 请检查 # 值是否正确\n")
91 | return nil
92 | }
93 | } else {
94 | cli.ShowCommandHelp(c, c.Command.Name)
95 | }
96 |
97 | switchedUser, err := config.Config.SwitchUser(uid)
98 | if err != nil {
99 | fmt.Printf("切换用户失败, %s\n", err)
100 | return nil
101 | }
102 |
103 | if switchedUser == nil {
104 | switchedUser = TryLogin()
105 | }
106 |
107 | if switchedUser != nil {
108 | fmt.Printf("切换用户: %s\n", switchedUser.Nickname)
109 | } else {
110 | fmt.Printf("切换用户失败\n")
111 | }
112 |
113 | return nil
114 | },
115 | }
116 | }
117 |
118 | func CmdWho() cli.Command {
119 | return cli.Command{
120 | Name: "who",
121 | Usage: "获取当前帐号",
122 | Description: "获取当前帐号的信息",
123 | Category: "阿里云盘账号",
124 | Before: ReloadConfigFunc,
125 | Action: func(c *cli.Context) error {
126 | if config.Config.ActiveUser() == nil {
127 | fmt.Println("未登录账号")
128 | os.Exit(1)
129 | }
130 | activeUser := config.Config.ActiveUser()
131 | cloudName := activeUser.GetDriveById(activeUser.ActiveDriveId).DriveName
132 | user, _ := GetActivePanClient().OpenapiPanClient().GetUserInfo()
133 | thirdParty := "未开通"
134 | if user.ThirdPartyVip {
135 | thirdParty = "已开通(" + user.ThirdPartyVipExpire + ")"
136 | }
137 | fmt.Printf("当前帐号UID: %s, 昵称: %s, 三方权益包: %s, 当前使用网盘:%s\n", activeUser.UserId, activeUser.Nickname, thirdParty, cloudName)
138 | return nil
139 | },
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/internal/command_local/lcd.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package command_local
15 |
16 | import (
17 | "fmt"
18 | "github.com/tickstep/aliyunpan/internal/command"
19 | "github.com/tickstep/aliyunpan/internal/config"
20 | "github.com/urfave/cli"
21 | "os"
22 | "runtime"
23 | "strings"
24 | )
25 |
26 | // CmdLocalCd 切换本地工作目录
27 | func CmdLocalCd() cli.Command {
28 | return cli.Command{
29 | Name: "lcd",
30 | Category: "本地命令",
31 | Usage: "切换本地工作目录",
32 | Description: `
33 | aliyunpan lcd <本地目录, 绝对路径或相对路径>
34 |
35 | 示例:
36 |
37 | 切换 /我的资源 工作目录:
38 | aliyunpan lcd /我的资源
39 |
40 | 切换上级目录:
41 | aliyunpan lcd ..
42 |
43 | 切换根目录:
44 | aliyunpan lcd /
45 | `,
46 | Before: command.ReloadConfigFunc,
47 | After: command.SaveConfigFunc,
48 | Action: func(c *cli.Context) error {
49 | if c.NArg() == 0 {
50 | cli.ShowCommandHelp(c, c.Command.Name)
51 | return nil
52 | }
53 | RunChangeLocalDirectory(c.Args().Get(0))
54 | return nil
55 | },
56 | Flags: []cli.Flag{},
57 | }
58 | }
59 |
60 | // CmdLocalPwd 当前本地工作目录
61 | func CmdLocalPwd() cli.Command {
62 | return cli.Command{
63 | Name: "lpwd",
64 | Usage: "输出本地工作目录",
65 | Category: "本地命令",
66 | Before: command.ReloadConfigFunc,
67 | Action: func(c *cli.Context) error {
68 | lwd := config.Config.LocalWorkdir
69 | if lwd == "" {
70 | // 默认为用户主页目录
71 | lwd = GetLocalHomeDir()
72 | config.Config.LocalWorkdir = lwd
73 | }
74 | if runtime.GOOS == "windows" {
75 | lwd = strings.ReplaceAll(lwd, "/", "\\")
76 | } else {
77 | // Unix-like system, so just assume Unix
78 | lwd = strings.ReplaceAll(lwd, "\\", "/")
79 | }
80 | fmt.Println("本地工作目录: " + lwd)
81 | return nil
82 | },
83 | }
84 | }
85 |
86 | func RunChangeLocalDirectory(targetPath string) {
87 | targetPath = LocalPathJoin(targetPath)
88 |
89 | // 获取目标路径文件信息
90 | localFileInfo, er := os.Stat(targetPath)
91 | if er != nil {
92 | fmt.Println("目录路径不存在")
93 | return
94 | }
95 | if !localFileInfo.IsDir() {
96 | fmt.Printf("错误: %s 不是一个目录 (文件夹)\n", targetPath)
97 | return
98 | }
99 | config.Config.LocalWorkdir = strings.ReplaceAll(targetPath, "\\", "/")
100 | fmt.Printf("改变本地工作目录: %s\n", config.Config.LocalWorkdir)
101 | }
102 |
--------------------------------------------------------------------------------
/internal/command_local/utils.go:
--------------------------------------------------------------------------------
1 | package command_local
2 |
3 | import (
4 | "github.com/tickstep/aliyunpan/internal/config"
5 | "github.com/tickstep/aliyunpan/library/homedir"
6 | "os"
7 | "path"
8 | "path/filepath"
9 | "strings"
10 | )
11 |
12 | // GetLocalHomeDir 获取本地用户主目录
13 | func GetLocalHomeDir() string {
14 | // 默认为用户主页目录
15 | if hd, e := os.UserHomeDir(); e == nil {
16 | return hd
17 | }
18 | return ""
19 | }
20 |
21 | // LocalPathJoin 拼接本地路径
22 | func LocalPathJoin(p string) string {
23 | p = path.Clean(strings.ReplaceAll(p, "\\", "/"))
24 | if filepath.IsAbs(p) {
25 | return p
26 | } else if strings.HasPrefix(p, "~") {
27 | if d, e := homedir.Expand(p); e == nil {
28 | return d
29 | }
30 | }
31 | wd := config.Config.LocalWorkdir
32 | if wd == "" {
33 | wd = GetLocalHomeDir()
34 | }
35 | return path.Join(wd, p)
36 | }
37 |
38 | // LocalPathDir 获取当前文件夹的父文件夹路径
39 | func LocalPathDir(p string) string {
40 | p = strings.ReplaceAll(p, "\\", "/")
41 | if strings.HasSuffix(p, ":/") { // windows卷标路径,例如:C:\ 、D:\、E:\
42 | return p
43 | }
44 | parentDirPath := path.Dir(LocalPathClean(p))
45 | return parentDirPath
46 | }
47 |
48 | // LocalPathBase 获取当前路径的文件(文件夹)名
49 | func LocalPathBase(p string) string {
50 | p = strings.ReplaceAll(p, "\\", "/")
51 | if strings.HasSuffix(p, ":/") { // windows卷标路径,例如:C:\ 、D:\、E:\
52 | return ""
53 | }
54 | baseName := path.Base(LocalPathClean(p))
55 | return baseName
56 | }
57 |
58 | // LocalPathClean 规范化本地文件夹路径
59 | func LocalPathClean(p string) string {
60 | cleanPath := path.Clean(strings.ReplaceAll(p, "\\", "/"))
61 | return cleanPath
62 | }
63 |
--------------------------------------------------------------------------------
/internal/config/cache.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/tickstep/aliyunpan-api/aliyunpan"
5 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
6 | "github.com/tickstep/library-go/expires"
7 | "path"
8 | "time"
9 | )
10 |
11 | // DeleteCache 删除含有 dirs 的缓存
12 | func (pu *PanUser) DeleteCache(dirs []string) {
13 | cache := pu.cacheOpMap.LazyInitCachePoolOp(pu.ActiveDriveId)
14 | for _, v := range dirs {
15 | key := v + "_" + "OrderByName"
16 | _, ok := cache.Load(key)
17 | if ok {
18 | cache.Delete(key)
19 | }
20 | }
21 | }
22 |
23 | // DeleteOneCache 删除缓存
24 | func (pu *PanUser) DeleteOneCache(dirPath string) {
25 | ps := []string{dirPath}
26 | pu.DeleteCache(ps)
27 | }
28 |
29 | // CacheFilesDirectoriesList 缓存获取
30 | func (pu *PanUser) CacheFilesDirectoriesList(pathStr string) (fdl aliyunpan.FileList, apiError *apierror.ApiError) {
31 | data := pu.cacheOpMap.CacheOperation(pu.ActiveDriveId, pathStr+"_OrderByName", func() expires.DataExpires {
32 | var fi *aliyunpan.FileEntity
33 | fi, apiError = pu.panClient.OpenapiPanClient().FileInfoByPath(pu.ActiveDriveId, pathStr)
34 | if apiError != nil {
35 | return nil
36 | }
37 | fileListParam := &aliyunpan.FileListParam{
38 | DriveId: pu.ActiveDriveId,
39 | ParentFileId: fi.FileId,
40 | }
41 | fdl, apiError = pu.panClient.OpenapiPanClient().FileListGetAll(fileListParam, 200)
42 | if apiError != nil {
43 | return nil
44 | }
45 | // construct full path
46 | for _, f := range fdl {
47 | f.Path = path.Join(pathStr, f.FileName)
48 | }
49 | return expires.NewDataExpires(fdl, 10*time.Minute)
50 | })
51 | if apiError != nil {
52 | return
53 | }
54 | return data.Data().(aliyunpan.FileList), nil
55 | }
56 |
--------------------------------------------------------------------------------
/internal/config/errors.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package config
15 |
16 | import (
17 | "errors"
18 | )
19 |
20 | var (
21 | //ErrNotLogin 未登录帐号错误
22 | ErrNotLogin = errors.New("user not login")
23 | //ErrConfigFilePathNotSet 未设置配置文件
24 | ErrConfigFilePathNotSet = errors.New("config file not set")
25 | //ErrConfigFileNotExist 未设置Config, 未初始化
26 | ErrConfigFileNotExist = errors.New("config file not exist")
27 | //ErrConfigFileNoPermission Config文件无权限访问
28 | ErrConfigFileNoPermission = errors.New("config file permission denied")
29 | //ErrConfigContentsParseError 解析Config数据错误
30 | ErrConfigContentsParseError = errors.New("config contents parse error")
31 | )
32 |
--------------------------------------------------------------------------------
/internal/config/pan_client.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/tickstep/aliyunpan-api/aliyunpan_open"
5 | "github.com/tickstep/aliyunpan-api/aliyunpan_web"
6 | )
7 |
8 | type (
9 | // PanClient 云盘客户端
10 | PanClient struct {
11 | // 网页WEB接口客户端
12 | webapiPanClient *aliyunpan_web.WebPanClient
13 | // 阿里openapi接口客户端
14 | openapiPanClient *aliyunpan_open.OpenPanClient
15 | }
16 | )
17 |
18 | func NewPanClient(webClient *aliyunpan_web.WebPanClient, openClient *aliyunpan_open.OpenPanClient) *PanClient {
19 | return &PanClient{
20 | webapiPanClient: webClient,
21 | openapiPanClient: openClient,
22 | }
23 | }
24 |
25 | func (p *PanClient) WebapiPanClient() *aliyunpan_web.WebPanClient {
26 | return p.webapiPanClient
27 | }
28 |
29 | func (p *PanClient) OpenapiPanClient() *aliyunpan_open.OpenPanClient {
30 | return p.openapiPanClient
31 | }
32 |
--------------------------------------------------------------------------------
/internal/config/pan_config_export.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package config
15 |
16 | import (
17 | "os"
18 | "strconv"
19 | "strings"
20 |
21 | "github.com/olekukonko/tablewriter"
22 | "github.com/tickstep/aliyunpan/cmder/cmdtable"
23 | "github.com/tickstep/library-go/converter"
24 | "github.com/tickstep/library-go/requester"
25 | )
26 |
27 | // SetProxy 设置代理
28 | func (c *PanConfig) SetProxy(proxy string) {
29 | c.Proxy = proxy
30 | requester.SetGlobalProxy(proxy)
31 | }
32 |
33 | // SetLocalAddrs 设置localAddrs
34 | func (c *PanConfig) SetLocalAddrs(localAddrs string) {
35 | c.LocalAddrs = localAddrs
36 | ips := ParseLocalAddress(localAddrs, strings.ToLower(c.PreferIPType))
37 | if len(ips) > 0 {
38 | requester.SetLocalTCPAddrList(ips...)
39 | }
40 | }
41 |
42 | func (c *PanConfig) SetPreferIPType(ipType string) {
43 | c.PreferIPType = ipType
44 | t := requester.IPAny
45 | if strings.ToLower(ipType) == "ipv4" {
46 | t = requester.IPv4
47 | } else if strings.ToLower(ipType) == "ipv6" {
48 | t = requester.IPv6
49 | }
50 | requester.SetPreferIPType(t)
51 | }
52 |
53 | // SetCacheSizeByStr 设置cache_size
54 | func (c *PanConfig) SetCacheSizeByStr(sizeStr string) error {
55 | size, err := converter.ParseFileSizeStr(sizeStr)
56 | if err != nil {
57 | return err
58 | }
59 | c.CacheSize = int(size)
60 | return nil
61 | }
62 |
63 | // SetMaxDownloadRateByStr 设置 max_download_rate
64 | func (c *PanConfig) SetMaxDownloadRateByStr(sizeStr string) error {
65 | size, err := converter.ParseFileSizeStr(stripPerSecond(sizeStr))
66 | if err != nil {
67 | return err
68 | }
69 | c.MaxDownloadRate = size
70 | return nil
71 | }
72 |
73 | // SetMaxUploadRateByStr 设置 max_upload_rate
74 | func (c *PanConfig) SetMaxUploadRateByStr(sizeStr string) error {
75 | size, err := converter.ParseFileSizeStr(stripPerSecond(sizeStr))
76 | if err != nil {
77 | return err
78 | }
79 | c.MaxUploadRate = size
80 | return nil
81 | }
82 |
83 | // SetFileRecorderConfig 设置文件记录器
84 | func (c *PanConfig) SetFileRecorderConfig(config string) error {
85 | if config == "1" || config == "2" {
86 | c.FileRecordConfig = config
87 | }
88 | return nil
89 | }
90 |
91 | // SetDeviceId 设置客户端ID
92 | func (c *PanConfig) SetDeviceId(deviceId string) error {
93 | if deviceId == "" {
94 | return nil
95 | }
96 | c.DeviceId = deviceId
97 | return nil
98 | }
99 |
100 | // PrintTable 输出表格
101 | func (c *PanConfig) PrintTable() {
102 | fileRecorderLabel := "禁用"
103 | if c.FileRecordConfig == "1" {
104 | fileRecorderLabel = "开启"
105 | }
106 | tb := cmdtable.NewTable(os.Stdout)
107 | tb.SetHeader([]string{"名称", "值", "建议值", "描述"})
108 | tb.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
109 | tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
110 | tb.AppendBulk([][]string{
111 | []string{"cache_size", converter.ConvertFileSize(int64(c.CacheSize), 2), "1KB ~ 256KB", "下载缓存, 如果硬盘占用高或下载速度慢, 请尝试调大此值"},
112 | []string{"max_download_parallel", strconv.Itoa(c.MaxDownloadParallel), "1 ~ 20", "最大下载并发量,即同时下载文件最大数量"},
113 | []string{"max_upload_parallel", strconv.Itoa(c.MaxUploadParallel), "1 ~ 20", "最大上传并发量,即同时上传文件最大数量"},
114 | []string{"max_download_rate", showMaxRate(c.MaxDownloadRate), "", "限制单个文件最大下载速度, 0代表不限制"},
115 | []string{"max_upload_rate", showMaxRate(c.MaxUploadRate), "", "限制单个文件最大上传速度, 0代表不限制"},
116 | []string{"savedir", GetDownloadDir(), "", "下载文件的储存目录"},
117 | []string{"proxy", c.Proxy, "", "设置代理, 支持 http/socks5 代理,例如: http://127.0.0.1:8888 或者 socks5://127.0.0.1:8889"},
118 | []string{"local_addrs", c.LocalAddrs, "", "绑定本地网卡地址, 多个地址用逗号隔开,支持网口名称,例如: 127.0.0.1,192.168.100.126,en0,eth0"},
119 | []string{"ip_type", c.PreferIPType, "ipv4-优先IPv4,ipv6-优先IPv6", "设置域名解析IP优先类型。修改后需要重启应用生效"},
120 | []string{"file_record_config", fileRecorderLabel, "1-开启,2-禁用", "设置是否开启上传、下载、同步文件的结果记录,开启后会把结果记录到CSV文件方便后期查看"},
121 | []string{"device_id", c.DeviceId, "", "客户端ID,用于标识登录客户端,阿里单个账号最多允许10个客户端同时在线。修改后需要重启应用生效"},
122 | })
123 | tb.Render()
124 | }
125 |
--------------------------------------------------------------------------------
/internal/config/utils_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package config
15 |
16 | import (
17 | "fmt"
18 | "testing"
19 | )
20 |
21 | func TestEncryptString(t *testing.T) {
22 | fmt.Println(EncryptString("131687xxxxx@189.cn"))
23 | }
24 |
25 | func TestDecryptString(t *testing.T) {
26 | fmt.Println(DecryptString("75b3c8d21607440c0e8a70f4a4861c8669774cc69c70ce2a2c8acb815b6d5d3b"))
27 | }
28 |
29 | func TestRandomDeviceId(t *testing.T) {
30 | fmt.Println(RandomDeviceId())
31 | }
32 |
--------------------------------------------------------------------------------
/internal/file/downloader/config.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package downloader
15 |
16 | import (
17 | "github.com/tickstep/aliyunpan/library/requester/transfer"
18 | )
19 |
20 | const (
21 | //CacheSize 默认的下载缓存
22 | CacheSize = 8192
23 | )
24 |
25 | var (
26 | // MinParallelSize 单个线程最小的数据量
27 | MinParallelSize int64 = 10 * 1024 * 1024 // 10MB
28 |
29 | // MaxParallelWorkerCount 单个文件下载最大并发线程数量
30 | // 阿里云盘规定:并发下载线程数不要超过3,否则会有风控检测处罚的风险
31 | MaxParallelWorkerCount int = 3
32 | )
33 |
34 | // Config 下载配置
35 | type Config struct {
36 | Mode transfer.RangeGenMode // 下载Range分配模式
37 | MaxParallel int // 最大下载并发量
38 | SliceParallel int // 单文件下载线程数,为0代表程序自动调度
39 | CacheSize int // 下载缓冲
40 | BlockSize int64 // 每个Range区块的大小, RangeGenMode 为 RangeGenMode2 时才有效
41 | MaxRate int64 // 限制最大下载速度
42 | InstanceStateStorageFormat InstanceStateStorageFormat // 断点续传储存类型
43 | InstanceStatePath string // 断点续传信息路径
44 | TryHTTP bool // 是否尝试使用 http 连接
45 | ShowProgress bool // 是否展示下载进度条
46 | ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
47 | }
48 |
49 | // NewConfig 返回默认配置
50 | func NewConfig() *Config {
51 | return &Config{
52 | MaxParallel: 3,
53 | CacheSize: CacheSize,
54 | }
55 | }
56 |
57 | // Fix 修复配置信息, 使其合法
58 | func (cfg *Config) Fix() {
59 | fixCacheSize(&cfg.CacheSize)
60 | if cfg.MaxParallel < 1 {
61 | cfg.MaxParallel = 1
62 | }
63 | }
64 |
65 | // Copy 拷贝新的配置
66 | func (cfg *Config) Copy() *Config {
67 | newCfg := *cfg
68 | return &newCfg
69 | }
70 |
--------------------------------------------------------------------------------
/internal/file/downloader/instance_state.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package downloader
15 |
16 | import (
17 | "errors"
18 | "github.com/json-iterator/go"
19 | "github.com/tickstep/library-go/cachepool"
20 | "github.com/tickstep/library-go/crypto"
21 | "github.com/tickstep/library-go/logger"
22 | "github.com/tickstep/aliyunpan/library/requester/transfer"
23 | "os"
24 | "sync"
25 | )
26 |
27 | type (
28 | //InstanceState 状态, 断点续传信息
29 | InstanceState struct {
30 | saveFile *os.File
31 | format InstanceStateStorageFormat
32 | ii *transfer.DownloadInstanceInfoExport
33 | mu sync.Mutex
34 | }
35 |
36 | // InstanceStateStorageFormat 断点续传储存类型
37 | InstanceStateStorageFormat int
38 | )
39 |
40 | const (
41 | // InstanceStateStorageFormatJSON json 格式
42 | InstanceStateStorageFormatJSON = iota
43 | // InstanceStateStorageFormatProto3 protobuf 格式
44 | InstanceStateStorageFormatProto3
45 | )
46 |
47 | //NewInstanceState 初始化InstanceState
48 | func NewInstanceState(saveFile *os.File, format InstanceStateStorageFormat) *InstanceState {
49 | return &InstanceState{
50 | saveFile: saveFile,
51 | format: format,
52 | }
53 | }
54 |
55 | func (is *InstanceState) checkSaveFile() bool {
56 | return is.saveFile != nil
57 | }
58 |
59 | func (is *InstanceState) getSaveFileContents() []byte {
60 | if !is.checkSaveFile() {
61 | return nil
62 | }
63 |
64 | finfo, err := is.saveFile.Stat()
65 | if err != nil {
66 | panic(err)
67 | }
68 |
69 | size := finfo.Size()
70 | if size > 0xffffffff {
71 | panic("savePath too large")
72 | }
73 | intSize := int(size)
74 |
75 | buf := cachepool.RawMallocByteSlice(intSize)
76 |
77 | n, _ := is.saveFile.ReadAt(buf, 0)
78 | return crypto.Base64Decode(buf[:n])
79 | }
80 |
81 | //Get 获取断点续传信息
82 | func (is *InstanceState) Get() (eii *transfer.DownloadInstanceInfo) {
83 | if !is.checkSaveFile() {
84 | return nil
85 | }
86 |
87 | is.mu.Lock()
88 | defer is.mu.Unlock()
89 |
90 | contents := is.getSaveFileContents()
91 | if len(contents) <= 0 {
92 | return
93 | }
94 |
95 | is.ii = &transfer.DownloadInstanceInfoExport{}
96 | var err error
97 | err = jsoniter.Unmarshal(contents, is.ii)
98 |
99 | if err != nil {
100 | logger.Verbosef("DEBUG: InstanceInfo unmarshal error: %s\n", err)
101 | return
102 | }
103 |
104 | eii = is.ii.GetInstanceInfo()
105 | return
106 | }
107 |
108 | //Put 提交断点续传信息
109 | func (is *InstanceState) Put(eii *transfer.DownloadInstanceInfo) {
110 | if !is.checkSaveFile() {
111 | return
112 | }
113 |
114 | is.mu.Lock()
115 | defer is.mu.Unlock()
116 |
117 | if is.ii == nil {
118 | is.ii = &transfer.DownloadInstanceInfoExport{}
119 | }
120 | is.ii.SetInstanceInfo(eii)
121 | var (
122 | data []byte
123 | err error
124 | )
125 | data, err = jsoniter.Marshal(is.ii)
126 | if err != nil {
127 | panic(err)
128 | }
129 |
130 | err = is.saveFile.Truncate(int64(len(data)))
131 | if err != nil {
132 | logger.Verbosef("DEBUG: truncate file error: %s\n", err)
133 | }
134 |
135 | _, err = is.saveFile.WriteAt(crypto.Base64Encode(data), 0)
136 | if err != nil {
137 | logger.Verbosef("DEBUG: write instance state error: %s\n", err)
138 | }
139 | }
140 |
141 | //Close 关闭
142 | func (is *InstanceState) Close() error {
143 | if !is.checkSaveFile() {
144 | return nil
145 | }
146 |
147 | return is.saveFile.Close()
148 | }
149 |
150 | func (der *Downloader) initInstanceState(format InstanceStateStorageFormat) (err error) {
151 | if der.instanceState != nil {
152 | return errors.New("already initInstanceState")
153 | }
154 |
155 | var saveFile *os.File
156 | if der.config.InstanceStatePath != "" {
157 | saveFile, err = os.OpenFile(der.config.InstanceStatePath, os.O_RDWR|os.O_CREATE, 0777)
158 | if err != nil {
159 | return err
160 | }
161 | }
162 |
163 | der.instanceState = NewInstanceState(saveFile, format)
164 | return nil
165 | }
166 |
167 | func (der *Downloader) removeInstanceState() error {
168 | der.instanceState.Close()
169 | if der.config.InstanceStatePath != "" {
170 | return os.Remove(der.config.InstanceStatePath)
171 | }
172 | return nil
173 | }
174 |
--------------------------------------------------------------------------------
/internal/file/downloader/loadbalance.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package downloader
15 |
16 | import (
17 | "net/http"
18 | "sync/atomic"
19 | )
20 |
21 | type (
22 | // LoadBalancerResponse 负载均衡响应状态
23 | LoadBalancerResponse struct {
24 | URL string
25 | }
26 |
27 | // LoadBalancerResponseList 负载均衡列表
28 | LoadBalancerResponseList struct {
29 | lbr []*LoadBalancerResponse
30 | cursor int32
31 | }
32 |
33 | LoadBalancerCompareFunc func(info map[string]string, subResp *http.Response) bool
34 | )
35 |
36 | // NewLoadBalancerResponseList 初始化负载均衡列表
37 | func NewLoadBalancerResponseList(lbr []*LoadBalancerResponse) *LoadBalancerResponseList {
38 | return &LoadBalancerResponseList{
39 | lbr: lbr,
40 | }
41 | }
42 |
43 | // SequentialGet 顺序获取
44 | func (lbrl *LoadBalancerResponseList) SequentialGet() *LoadBalancerResponse {
45 | if len(lbrl.lbr) == 0 {
46 | return nil
47 | }
48 |
49 | if int(lbrl.cursor) >= len(lbrl.lbr) {
50 | lbrl.cursor = 0
51 | }
52 |
53 | lbr := lbrl.lbr[int(lbrl.cursor)]
54 | atomic.AddInt32(&lbrl.cursor, 1)
55 | return lbr
56 | }
57 |
58 | // RandomGet 随机获取
59 | func (lbrl *LoadBalancerResponseList) RandomGet() *LoadBalancerResponse {
60 | return lbrl.lbr[RandomNumber(0, len(lbrl.lbr))]
61 | }
62 |
63 | // AddLoadBalanceServer 增加负载均衡服务器
64 | func (der *Downloader) AddLoadBalanceServer(urls ...string) {
65 | der.loadBalansers = append(der.loadBalansers, urls...)
66 | }
67 |
68 | // DefaultLoadBalancerCompareFunc 检测负载均衡的服务器是否一致
69 | func DefaultLoadBalancerCompareFunc(info map[string]string, subResp *http.Response) bool {
70 | if info == nil || subResp == nil {
71 | return false
72 | }
73 |
74 | for headerKey, value := range info {
75 | if value != subResp.Header.Get(headerKey) {
76 | return false
77 | }
78 | }
79 |
80 | return true
81 | }
82 |
--------------------------------------------------------------------------------
/internal/file/downloader/resetcontroler.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package downloader
15 |
16 | import (
17 | "github.com/tickstep/library-go/expires"
18 | "sync"
19 | "time"
20 | )
21 |
22 | // ResetController 网络连接控制器
23 | type ResetController struct {
24 | mu sync.Mutex
25 | currentTime time.Time
26 | maxResetNum int
27 | resetEntity map[expires.Expires]struct{}
28 | }
29 |
30 | // NewResetController 初始化*ResetController
31 | func NewResetController(maxResetNum int) *ResetController {
32 | return &ResetController{
33 | currentTime: time.Now(),
34 | maxResetNum: maxResetNum,
35 | resetEntity: map[expires.Expires]struct{}{},
36 | }
37 | }
38 |
39 | func (rc *ResetController) update() {
40 | for k := range rc.resetEntity {
41 | if k.IsExpires() {
42 | delete(rc.resetEntity, k)
43 | }
44 | }
45 | }
46 |
47 | // AddResetNum 增加连接
48 | func (rc *ResetController) AddResetNum() {
49 | rc.mu.Lock()
50 | defer rc.mu.Unlock()
51 | rc.update()
52 | rc.resetEntity[expires.NewExpires(9*time.Second)] = struct{}{}
53 | }
54 |
55 | // CanReset 是否可以建立连接
56 | func (rc *ResetController) CanReset() bool {
57 | rc.mu.Lock()
58 | defer rc.mu.Unlock()
59 | rc.update()
60 | return len(rc.resetEntity) < rc.maxResetNum
61 | }
62 |
--------------------------------------------------------------------------------
/internal/file/downloader/sort.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package downloader
15 |
16 | type (
17 | // ByLeftDesc 根据剩余下载量倒序排序
18 | ByLeftDesc struct {
19 | WorkerList
20 | }
21 | )
22 |
23 | // Len 返回长度
24 | func (wl WorkerList) Len() int {
25 | return len(wl)
26 | }
27 |
28 | // Swap 交换
29 | func (wl WorkerList) Swap(i, j int) {
30 | wl[i], wl[j] = wl[j], wl[i]
31 | }
32 |
33 | // Less 实现倒序
34 | func (wl ByLeftDesc) Less(i, j int) bool {
35 | return wl.WorkerList[i].wrange.Len() > wl.WorkerList[j].wrange.Len()
36 | }
37 |
--------------------------------------------------------------------------------
/internal/file/downloader/status.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package downloader
15 |
16 | import (
17 | "github.com/tickstep/aliyunpan/library/requester/transfer"
18 | )
19 |
20 | type (
21 | //WorkerStatuser 状态
22 | WorkerStatuser interface {
23 | StatusCode() StatusCode //状态码
24 | StatusText() string
25 | }
26 |
27 | //StatusCode 状态码
28 | StatusCode int
29 |
30 | //WorkerStatus worker状态
31 | WorkerStatus struct {
32 | statusCode StatusCode
33 | }
34 |
35 | // DownloadStatusFunc 下载状态处理函数
36 | DownloadStatusFunc func(status transfer.DownloadStatuser, workersCallback func(RangeWorkerFunc))
37 | )
38 |
39 | const (
40 | //StatusCodeInit 初始化
41 | StatusCodeInit StatusCode = iota
42 | //StatusCodeSuccessed 成功
43 | StatusCodeSuccessed
44 | //StatusCodePending 等待响应
45 | StatusCodePending
46 | //StatusCodeDownloading 下载中
47 | StatusCodeDownloading
48 | //StatusCodeWaitToWrite 等待写入数据
49 | StatusCodeWaitToWrite
50 | //StatusCodeInternalError 内部错误
51 | StatusCodeInternalError
52 | //StatusCodeTooManyConnections 连接数太多
53 | StatusCodeTooManyConnections
54 | //StatusCodeNetError 网络错误
55 | StatusCodeNetError
56 | //StatusCodeFailed 下载失败
57 | StatusCodeFailed
58 | //StatusCodePaused 已暂停
59 | StatusCodePaused
60 | //StatusCodeReseted 已重设连接
61 | StatusCodeReseted
62 | //StatusCodeCanceled 已取消
63 | StatusCodeCanceled
64 | //StatusCodeDownloadUrlExpired 下载链接已过期
65 | StatusCodeDownloadUrlExpired
66 | // StatusCodeDownloadUrlExceedMaxConcurrency 限流报错,下载链接超过最大并发数
67 | StatusCodeDownloadUrlExceedMaxConcurrency
68 | //StatusCodeIllegalDownloadFile 文件非法,不允许下载
69 | StatusCodeIllegalDownloadFile
70 | )
71 |
72 | // GetStatusText 根据状态码获取状态信息
73 | func GetStatusText(sc StatusCode) string {
74 | switch sc {
75 | case StatusCodeInit:
76 | return "初始化"
77 | case StatusCodeSuccessed:
78 | return "成功"
79 | case StatusCodePending:
80 | return "等待响应"
81 | case StatusCodeDownloading:
82 | return "下载中"
83 | case StatusCodeWaitToWrite:
84 | return "等待写入数据"
85 | case StatusCodeInternalError:
86 | return "内部错误"
87 | case StatusCodeTooManyConnections:
88 | return "连接数太多"
89 | case StatusCodeNetError:
90 | return "网络错误"
91 | case StatusCodeFailed:
92 | return "下载失败"
93 | case StatusCodePaused:
94 | return "已暂停"
95 | case StatusCodeReseted:
96 | return "已重设连接"
97 | case StatusCodeCanceled:
98 | return "已取消"
99 | case StatusCodeDownloadUrlExpired:
100 | return "链接已过期"
101 | case StatusCodeDownloadUrlExceedMaxConcurrency:
102 | return "遇到限流报错"
103 | default:
104 | return "未知状态码"
105 | }
106 | }
107 |
108 | // NewWorkerStatus 初始化WorkerStatus
109 | func NewWorkerStatus() *WorkerStatus {
110 | return &WorkerStatus{
111 | statusCode: StatusCodeInit,
112 | }
113 | }
114 |
115 | // SetStatusCode 设置worker状态码
116 | func (ws *WorkerStatus) SetStatusCode(sc StatusCode) {
117 | ws.statusCode = sc
118 | }
119 |
120 | // StatusCode 返回状态码
121 | func (ws *WorkerStatus) StatusCode() StatusCode {
122 | return ws.statusCode
123 | }
124 |
125 | // StatusText 返回状态信息
126 | func (ws *WorkerStatus) StatusText() string {
127 | return GetStatusText(ws.statusCode)
128 | }
129 |
--------------------------------------------------------------------------------
/internal/file/downloader/utils.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package downloader
15 |
16 | import (
17 | "errors"
18 | "github.com/tickstep/library-go/logger"
19 | "github.com/tickstep/library-go/requester"
20 | mathrand "math/rand"
21 | "mime"
22 | "net/url"
23 | "path"
24 | "regexp"
25 | "strconv"
26 | "time"
27 | )
28 |
29 | var (
30 | // ContentRangeRE Content-Range 正则
31 | ContentRangeRE = regexp.MustCompile(`^.*? \d*?-\d*?/(\d*?)$`)
32 |
33 | // ranSource 随机数种子
34 | ranSource = mathrand.NewSource(time.Now().UnixNano())
35 |
36 | // ran 一个随机数实例
37 | ran = mathrand.New(ranSource)
38 |
39 | // 文件被禁止下载
40 | ErrFileDownloadForbidden = errors.New("文件被禁止下载")
41 | )
42 |
43 | // RandomNumber 生成指定区间随机数
44 | func RandomNumber(min, max int) int {
45 | if min > max {
46 | min, max = max, min
47 | }
48 | return ran.Intn(max-min) + min
49 | }
50 |
51 | // GetFileName 获取文件名
52 | func GetFileName(uri string, client *requester.HTTPClient) (filename string, err error) {
53 | if client == nil {
54 | client = requester.NewHTTPClient()
55 | }
56 |
57 | resp, err := client.Req("HEAD", uri, nil, nil)
58 | if resp != nil {
59 | defer resp.Body.Close()
60 | }
61 | if err != nil {
62 | return "", err
63 | }
64 |
65 | _, params, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition"))
66 | if err != nil {
67 | logger.Verbosef("DEBUG: GetFileName ParseMediaType error: %s\n", err)
68 | return path.Base(uri), nil
69 | }
70 |
71 | filename, err = url.QueryUnescape(params["filename"])
72 | if err != nil {
73 | return
74 | }
75 |
76 | if filename == "" {
77 | filename = path.Base(uri)
78 | }
79 |
80 | return
81 | }
82 |
83 | // ParseContentRange 解析Content-Range
84 | func ParseContentRange(contentRange string) (contentLength int64) {
85 | raw := ContentRangeRE.FindStringSubmatch(contentRange)
86 | if len(raw) < 2 {
87 | return -1
88 | }
89 |
90 | c, err := strconv.ParseInt(raw[1], 10, 64)
91 | if err != nil {
92 | return -1
93 | }
94 | return c
95 | }
96 |
97 | func fixCacheSize(size *int) {
98 | if *size < 1024 {
99 | *size = 1024
100 | }
101 | }
102 |
103 | // IsUrlExpired 下载链接是否已过期。过期返回True
104 | func IsUrlExpired(urlStr string) bool {
105 | u, err := url.Parse(urlStr)
106 | if err != nil {
107 | return true
108 | }
109 | expiredTimeSecStr := u.Query().Get("x-oss-expires")
110 | expiredTimeSec, _ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
111 | if (expiredTimeSec - time.Now().Unix()) <= 5 { // 小于5秒钟
112 | // expired
113 | return true
114 | }
115 | return false
116 | }
117 |
--------------------------------------------------------------------------------
/internal/file/downloader/writer.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package downloader
15 |
16 | import (
17 | "io"
18 | "os"
19 | "runtime"
20 | )
21 |
22 | type (
23 | // Fder 获取fd接口
24 | Fder interface {
25 | Fd() uintptr
26 | }
27 |
28 | // Writer 下载器数据输出接口
29 | Writer interface {
30 | io.WriterAt
31 | }
32 | )
33 |
34 | // NewDownloaderWriterByFilename 创建下载器数据输出接口, 类似于os.OpenFile
35 | func NewDownloaderWriterByFilename(name string, flag int, perm os.FileMode) (writer Writer, file *os.File, err error) {
36 | if runtime.GOOS == "windows" {
37 | // TODO: name进行特殊字符转换
38 | }
39 | file, err = os.OpenFile(name, flag, perm)
40 | if err != nil {
41 | return
42 | }
43 |
44 | writer = file
45 | return
46 | }
47 |
--------------------------------------------------------------------------------
/internal/file/uploader/block.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package uploader
15 |
16 | import (
17 | "bufio"
18 | "fmt"
19 | "github.com/tickstep/aliyunpan/library/requester/transfer"
20 | "github.com/tickstep/library-go/requester/rio/speeds"
21 | "io"
22 | "os"
23 | "sync"
24 | )
25 |
26 | type (
27 | // SplitUnit 将 io.ReaderAt 分割单元
28 | SplitUnit interface {
29 | Readed64
30 | io.Seeker
31 | Range() transfer.Range
32 | Left() int64
33 | ResetReader(readerAt io.ReaderAt)
34 | }
35 |
36 | fileBlock struct {
37 | readRange transfer.Range
38 | readed int64
39 | readerAt io.ReaderAt
40 | speedsStatRef *speeds.Speeds
41 | globalSpeedsStatRef *speeds.Speeds
42 | rateLimit *speeds.RateLimit
43 | mu sync.Mutex
44 | }
45 |
46 | bufioFileBlock struct {
47 | *fileBlock
48 | bufio *bufio.Reader
49 | }
50 | )
51 |
52 | // SplitBlock 文件分块
53 | func SplitBlock(fileSize, blockSize int64) (blockList []*BlockState) {
54 | gen := transfer.NewRangeListGenBlockSize(fileSize, 0, blockSize)
55 | rangeCount := gen.RangeCount()
56 | blockList = make([]*BlockState, 0, rangeCount)
57 | for i := 0; i < rangeCount; i++ {
58 | id, r := gen.GenRange()
59 | blockList = append(blockList, &BlockState{
60 | ID: id,
61 | Range: *r,
62 | })
63 | }
64 | return
65 | }
66 |
67 | // NewBufioSplitUnit io.ReaderAt实现SplitUnit接口, 有Buffer支持
68 | func NewBufioSplitUnit(readerAt io.ReaderAt, readRange transfer.Range, speedsStat *speeds.Speeds, rateLimit *speeds.RateLimit, globalSpeedsStat *speeds.Speeds) SplitUnit {
69 | su := &fileBlock{
70 | readerAt: readerAt,
71 | readRange: readRange,
72 | speedsStatRef: speedsStat,
73 | globalSpeedsStatRef: globalSpeedsStat,
74 | rateLimit: rateLimit,
75 | }
76 | return &bufioFileBlock{
77 | fileBlock: su,
78 | bufio: bufio.NewReaderSize(su, BufioReadSize),
79 | }
80 | }
81 |
82 | func (bfb *bufioFileBlock) Read(b []byte) (n int, err error) {
83 | return bfb.bufio.Read(b) // 间接调用fileBlock 的Read
84 | }
85 |
86 | // Read 只允许一个线程读同一个文件
87 | func (fb *fileBlock) Read(b []byte) (n int, err error) {
88 | fb.mu.Lock()
89 | defer fb.mu.Unlock()
90 |
91 | left := int(fb.Left())
92 | if left <= 0 {
93 | return 0, io.EOF
94 | }
95 |
96 | if len(b) > left {
97 | n, err = fb.readerAt.ReadAt(b[:left], fb.readed+fb.readRange.Begin)
98 | } else {
99 | n, err = fb.readerAt.ReadAt(b, fb.readed+fb.readRange.Begin)
100 | }
101 |
102 | n64 := int64(n)
103 | fb.readed += n64
104 | if fb.rateLimit != nil {
105 | fb.rateLimit.Add(n64) // 限速阻塞
106 | }
107 | if fb.speedsStatRef != nil {
108 | fb.speedsStatRef.Add(n64)
109 | }
110 | if fb.globalSpeedsStatRef != nil {
111 | fb.globalSpeedsStatRef.Add(n64)
112 | }
113 | return
114 | }
115 |
116 | func (fb *fileBlock) Seek(offset int64, whence int) (int64, error) {
117 | fb.mu.Lock()
118 | defer fb.mu.Unlock()
119 |
120 | switch whence {
121 | case os.SEEK_SET:
122 | fb.readed = offset
123 | case os.SEEK_CUR:
124 | fb.readed += offset
125 | case os.SEEK_END:
126 | fb.readed = fb.readRange.End - fb.readRange.Begin + offset
127 | default:
128 | return 0, fmt.Errorf("unsupport whence: %d", whence)
129 | }
130 | if fb.readed < 0 {
131 | fb.readed = 0
132 | }
133 | return fb.readed, nil
134 | }
135 |
136 | func (fb *fileBlock) Len() int64 {
137 | return fb.readRange.End - fb.readRange.Begin
138 | }
139 |
140 | func (fb *fileBlock) Left() int64 {
141 | return fb.readRange.End - fb.readRange.Begin - fb.readed
142 | }
143 |
144 | func (fb *fileBlock) Range() transfer.Range {
145 | return fb.readRange
146 | }
147 |
148 | func (fb *fileBlock) Readed() int64 {
149 | return fb.readed
150 | }
151 |
152 | func (fb *fileBlock) ResetReader(readerAt io.ReaderAt) {
153 | fb.readerAt = readerAt
154 | fb.readed = 0
155 | }
156 |
--------------------------------------------------------------------------------
/internal/file/uploader/block_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package uploader_test
15 |
16 | import (
17 | "fmt"
18 | "github.com/tickstep/library-go/cachepool"
19 | "github.com/tickstep/library-go/requester/rio"
20 | "github.com/tickstep/aliyunpan/library/requester/transfer"
21 | "github.com/tickstep/aliyunpan/internal/file/uploader"
22 | "io"
23 | "testing"
24 | )
25 |
26 | var (
27 | blockList = uploader.SplitBlock(10000, 999)
28 | )
29 |
30 | func TestSplitBlock(t *testing.T) {
31 | for k, e := range blockList {
32 | fmt.Printf("%d %#v\n", k, e)
33 | }
34 | }
35 |
36 | func TestSplitUnitRead(t *testing.T) {
37 | var size int64 = 65536*2+3432
38 | buffer := rio.NewBuffer(cachepool.RawMallocByteSlice(int(size)))
39 | unit := uploader.NewBufioSplitUnit(buffer, transfer.Range{Begin: 2, End: size}, nil, nil, nil)
40 |
41 | buf := cachepool.RawMallocByteSlice(1022)
42 | for {
43 | n, err := unit.Read(buf)
44 | if err != nil {
45 | if err == io.EOF {
46 | break
47 | }
48 | t.Fatalf("read error: %s\n", err)
49 | }
50 | fmt.Printf("n: %d, left: %d\n", n, unit.Left())
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/internal/file/uploader/error.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package uploader
15 |
16 | import "fmt"
17 |
18 | var (
19 | UploadUrlExpired = fmt.Errorf("UrlExpired")
20 | UploadPartNotSeq = fmt.Errorf("PartNotSequential")
21 | UploadNoSuchUpload = fmt.Errorf("NoSuchUpload")
22 | UploadTerminate = fmt.Errorf("UploadErrorTerminate")
23 | UploadPartAlreadyExist = fmt.Errorf("PartAlreadyExist")
24 | UploadHttpError = fmt.Errorf("HttpError")
25 | )
26 |
27 | type (
28 | // MultiError 多线程上传的错误
29 | MultiError struct {
30 | Err error
31 | // IsRetry 是否重试,
32 | Terminated bool
33 | NeedStartOver bool // 是否从头开始上传
34 | }
35 | )
36 |
37 | func (me *MultiError) Error() string {
38 | if me.Err != nil {
39 | return me.Err.Error()
40 | }
41 | return ""
42 | }
43 |
--------------------------------------------------------------------------------
/internal/file/uploader/instance_state.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package uploader
15 |
16 | import (
17 | "github.com/tickstep/aliyunpan/library/requester/transfer"
18 | )
19 |
20 | type (
21 | // BlockState 文件区块信息
22 | BlockState struct {
23 | ID int `json:"id"`
24 | Range transfer.Range `json:"range"`
25 | UploadDone bool `json:"upload_done"`
26 | }
27 |
28 | // InstanceState 上传断点续传信息
29 | InstanceState struct {
30 | BlockList []*BlockState `json:"block_list"`
31 | }
32 | )
33 |
34 | func (muer *MultiUploader) getWorkerListByInstanceState(is *InstanceState) workerList {
35 | workers := make(workerList, 0, len(is.BlockList))
36 | for _, blockState := range is.BlockList {
37 | if !blockState.UploadDone {
38 | workers = append(workers, &worker{
39 | id: blockState.ID,
40 | partOffset: blockState.Range.Begin,
41 | splitUnit: NewBufioSplitUnit(muer.file, blockState.Range, muer.speedsStat, muer.rateLimit, muer.globalSpeedsStat),
42 | uploadDone: false,
43 | })
44 | } else {
45 | // 已经完成的, 也要加入
46 | workers = append(workers, &worker{
47 | id: blockState.ID,
48 | partOffset: blockState.Range.Begin,
49 | splitUnit: NewBufioSplitUnit(muer.file, blockState.Range, muer.speedsStat, muer.rateLimit, muer.globalSpeedsStat),
50 | uploadDone: true,
51 | })
52 | }
53 | }
54 | return workers
55 | }
56 |
--------------------------------------------------------------------------------
/internal/file/uploader/readed.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package uploader
15 |
16 | import (
17 | "github.com/tickstep/library-go/requester/rio"
18 | "sync/atomic"
19 | )
20 |
21 | type (
22 | // Readed64 增加获取已读取数据量, 用于统计速度
23 | Readed64 interface {
24 | rio.ReaderLen64
25 | Readed() int64
26 | }
27 |
28 | readed64 struct {
29 | readed int64
30 | rio.ReaderLen64
31 | }
32 | )
33 |
34 | // NewReaded64 实现Readed64接口
35 | func NewReaded64(rl rio.ReaderLen64) Readed64 {
36 | return &readed64{
37 | readed: 0,
38 | ReaderLen64: rl,
39 | }
40 | }
41 |
42 | func (r64 *readed64) Read(p []byte) (n int, err error) {
43 | n, err = r64.ReaderLen64.Read(p)
44 | atomic.AddInt64(&r64.readed, int64(n))
45 | return n, err
46 | }
47 |
48 | func (r64 *readed64) Readed() int64 {
49 | return atomic.LoadInt64(&r64.readed)
50 | }
51 |
--------------------------------------------------------------------------------
/internal/file/uploader/status.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package uploader
15 |
16 | import (
17 | "sync/atomic"
18 | "time"
19 | )
20 |
21 | type (
22 | // Status 上传状态接口
23 | Status interface {
24 | TotalSize() int64 // 总大小
25 | Uploaded() int64 // 已上传数据
26 | SpeedsPerSecond() int64 // 每秒的上传速度
27 | TimeElapsed() time.Duration // 上传时间
28 | TimeLeft() time.Duration // 预计剩余时间, 负数代表未知
29 | }
30 |
31 | // UploadStatus 上传状态
32 | UploadStatus struct {
33 | totalSize int64 // 总大小
34 | uploaded int64 // 已上传数据
35 | speedsPerSecond int64 // 每秒的上传速度
36 | timeElapsed time.Duration // 上传时间
37 | }
38 |
39 | UploadStatusFunc func(status Status, updateChan <-chan struct{})
40 | )
41 |
42 | // TotalSize 返回总大小
43 | func (us *UploadStatus) TotalSize() int64 {
44 | return us.totalSize
45 | }
46 |
47 | // SetTotalSize 设置总大小
48 | func (us *UploadStatus) SetTotalSize(size int64) {
49 | us.totalSize = size
50 | }
51 |
52 | // Uploaded 返回已上传数据
53 | func (us *UploadStatus) Uploaded() int64 {
54 | return us.uploaded
55 | }
56 |
57 | // SetUploaded 设置已上传数据
58 | func (us *UploadStatus) SetUploaded(size int64) {
59 | us.uploaded = size
60 | }
61 |
62 | // SpeedsPerSecond 返回每秒的上传速度
63 | func (us *UploadStatus) SpeedsPerSecond() int64 {
64 | return us.speedsPerSecond
65 | }
66 |
67 | // TimeElapsed 返回上传时间
68 | func (us *UploadStatus) TimeElapsed() time.Duration {
69 | return us.timeElapsed
70 | }
71 |
72 | // TimeLeft 返回预计剩余时间, 负数代表未知
73 | func (us *UploadStatus) TimeLeft() time.Duration {
74 | var left time.Duration
75 | speeds := atomic.LoadInt64(&us.speedsPerSecond)
76 | if speeds <= 0 {
77 | left = -1
78 | } else {
79 | left = time.Duration((us.totalSize-us.uploaded)/(speeds)) * time.Second
80 | }
81 | return left
82 | }
83 |
84 | // GetStatusChan 获取上传状态
85 | func (u *Uploader) GetStatusChan() <-chan Status {
86 | c := make(chan Status)
87 |
88 | go func() {
89 | for {
90 | select {
91 | case <-u.finished:
92 | close(c)
93 | return
94 | default:
95 | if !u.executed {
96 | time.Sleep(1 * time.Second)
97 | continue
98 | }
99 |
100 | old := u.readed64.Readed()
101 | time.Sleep(1 * time.Second) // 每秒统计
102 |
103 | readed := u.readed64.Readed()
104 | c <- &UploadStatus{
105 | totalSize: u.readed64.Len(),
106 | uploaded: readed,
107 | speedsPerSecond: readed - old,
108 | timeElapsed: time.Since(u.executeTime) / 1e7 * 1e7,
109 | }
110 | }
111 | }
112 | }()
113 | return c
114 | }
115 |
116 | func (muer *MultiUploader) uploadStatusEvent() {
117 | if muer.onUploadStatusEvent == nil {
118 | return
119 | }
120 |
121 | go func() {
122 | ticker := time.NewTicker(1 * time.Second) // 每秒统计
123 | defer ticker.Stop()
124 | for {
125 | select {
126 | case <-muer.finished:
127 | return
128 | case <-ticker.C:
129 | readed := muer.workers.Readed()
130 | muer.onUploadStatusEvent(&UploadStatus{
131 | totalSize: muer.file.Len(),
132 | uploaded: readed,
133 | speedsPerSecond: muer.speedsStat.GetSpeeds(),
134 | timeElapsed: time.Since(muer.executeTime) / 1e8 * 1e8,
135 | }, muer.updateInstanceStateChan)
136 | }
137 | }
138 | }()
139 | }
140 |
--------------------------------------------------------------------------------
/internal/file/uploader/uploader.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package uploader
15 |
16 | import (
17 | "github.com/tickstep/aliyunpan/internal/utils"
18 | "github.com/tickstep/library-go/converter"
19 | "github.com/tickstep/library-go/requester"
20 | "github.com/tickstep/library-go/requester/rio"
21 | "net/http"
22 | "time"
23 | )
24 |
25 | const (
26 | // BufioReadSize bufio 缓冲区大小, 用于上传时读取文件
27 | BufioReadSize = int(64 * converter.KB) // 64KB
28 | )
29 |
30 | type (
31 | //CheckFunc 上传完成的检测函数
32 | CheckFunc func(resp *http.Response, uploadErr error)
33 |
34 | // Uploader 上传
35 | Uploader struct {
36 | url string // 上传地址
37 | readed64 Readed64 // 要上传的对象
38 | contentType string
39 |
40 | client *requester.HTTPClient
41 |
42 | executeTime time.Time
43 | executed bool
44 | finished chan struct{}
45 |
46 | checkFunc CheckFunc
47 | onExecute func()
48 | onFinish func()
49 | }
50 | )
51 |
52 | // NewUploader 返回 uploader 对象, url: 上传地址, readerlen64: 实现 rio.ReaderLen64 接口的对象, 例如文件
53 | func NewUploader(url string, readerlen64 rio.ReaderLen64) (uploader *Uploader) {
54 | uploader = &Uploader{
55 | url: url,
56 | readed64: NewReaded64(readerlen64),
57 | }
58 |
59 | return
60 | }
61 |
62 | func (u *Uploader) lazyInit() {
63 | if u.finished == nil {
64 | u.finished = make(chan struct{})
65 | }
66 | if u.client == nil {
67 | u.client = requester.NewHTTPClient()
68 | }
69 | u.client.SetTimeout(0)
70 | u.client.SetResponseHeaderTimeout(0)
71 | }
72 |
73 | // SetClient 设置http客户端
74 | func (u *Uploader) SetClient(c *requester.HTTPClient) {
75 | u.client = c
76 | }
77 |
78 | //SetContentType 设置Content-Type
79 | func (u *Uploader) SetContentType(contentType string) {
80 | u.contentType = contentType
81 | }
82 |
83 | //SetCheckFunc 设置上传完成的检测函数
84 | func (u *Uploader) SetCheckFunc(checkFunc CheckFunc) {
85 | u.checkFunc = checkFunc
86 | }
87 |
88 | // Execute 执行上传, 收到返回值信号则为上传结束
89 | func (u *Uploader) Execute() {
90 | utils.Trigger(u.onExecute)
91 |
92 | // 开始上传
93 | u.executeTime = time.Now()
94 | u.executed = true
95 | resp, _, err := u.execute()
96 |
97 | // 上传结束
98 | close(u.finished)
99 |
100 | if u.checkFunc != nil {
101 | u.checkFunc(resp, err)
102 | }
103 |
104 | utils.Trigger(u.onFinish) // 触发上传结束的事件
105 | }
106 |
107 | func (u *Uploader) execute() (resp *http.Response, code int, err error) {
108 | u.lazyInit()
109 | header := map[string]string{}
110 | if u.contentType != "" {
111 | header["Content-Type"] = u.contentType
112 | }
113 |
114 | resp, err = u.client.Req(http.MethodPost, u.url, u.readed64, header)
115 | if err != nil {
116 | return nil, 2, err
117 | }
118 |
119 | return resp, 0, nil
120 | }
121 |
122 | // OnExecute 任务开始时触发的事件
123 | func (u *Uploader) OnExecute(fn func()) {
124 | u.onExecute = fn
125 | }
126 |
127 | // OnFinish 任务完成时触发的事件
128 | func (u *Uploader) OnFinish(fn func()) {
129 | u.onFinish = fn
130 | }
131 |
--------------------------------------------------------------------------------
/internal/functions/common.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package functions
15 |
16 | import "time"
17 |
18 | // RetryWait 失败重试等待事件
19 | func RetryWait(retry int) time.Duration {
20 | if retry < 3 {
21 | return 2 * time.Duration(retry) * time.Second
22 | }
23 | return 6 * time.Second
24 | }
25 |
--------------------------------------------------------------------------------
/internal/functions/pandownload/download_statistic.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package pandownload
15 |
16 | import (
17 | "github.com/tickstep/aliyunpan/internal/functions"
18 | )
19 |
20 | type (
21 | DownloadStatistic struct {
22 | functions.Statistic
23 | }
24 | )
25 |
--------------------------------------------------------------------------------
/internal/functions/pandownload/errors.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package pandownload
15 |
16 | import "errors"
17 |
18 | var (
19 | // ErrDownloadNotSupportChecksum 文件不支持校验
20 | ErrDownloadNotSupportChecksum = errors.New("该文件不支持校验")
21 | // ErrDownloadChecksumFailed 文件校验失败
22 | ErrDownloadChecksumFailed = errors.New("该文件校验失败, 文件md5值与服务器记录的不匹配")
23 | // ErrDownloadFileBanned 违规文件
24 | ErrDownloadFileBanned = errors.New("该文件可能是违规文件, 不支持校验")
25 | // ErrDlinkNotFound 未取得下载链接
26 | ErrDlinkNotFound = errors.New("未取得下载链接")
27 | // ErrShareInfoNotFound 未在已分享列表中找到分享信息
28 | ErrShareInfoNotFound = errors.New("未在已分享列表中找到分享信息")
29 | )
30 |
--------------------------------------------------------------------------------
/internal/functions/pandownload/utils.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package pandownload
15 |
16 | import (
17 | "github.com/tickstep/aliyunpan-api/aliyunpan"
18 | "github.com/tickstep/aliyunpan/internal/localfile"
19 | "os"
20 | )
21 |
22 | // CheckFileValid 检测文件有效性
23 | func CheckFileValid(filePath string, fileInfo *aliyunpan.FileEntity) error {
24 | // 检查MD5
25 | // 检查文件大小
26 | // 检查digest签名
27 | return nil
28 | }
29 |
30 | // FileExist 检查文件是否存在
31 | //
32 | // 只有当文件存在, 文件大小不为0或断点续传文件不存在时, 才判断为存在
33 | func FileExist(path string) bool {
34 | if info, err := os.Stat(path); err == nil {
35 | if info.Size() == 0 {
36 | return false
37 | }
38 | if _, err = os.Stat(path + DownloadSuffix); err != nil {
39 | return true
40 | }
41 | }
42 |
43 | return false
44 | }
45 |
46 | // SymlinkFileExist 检查文件是否存在
47 | //
48 | // 逻辑和 FileExist 一致,增加符号链接文件的支持
49 | func SymlinkFileExist(fullPath, rootPath string) bool {
50 | originSaveRootSymlinkFile := localfile.NewSymlinkFile(rootPath)
51 | suffixPath := localfile.GetSuffixPath(fullPath, rootPath)
52 | savePathSymlinkFile, savePathFileInfo, err := localfile.RetrieveRealPathFromLogicSuffixPath(originSaveRootSymlinkFile, suffixPath)
53 | if err != nil || savePathFileInfo == nil {
54 | return false
55 | } else {
56 | if savePathFileInfo.IsDir() {
57 | return true
58 | } else {
59 | if savePathFileInfo.Size() == 0 {
60 | return false
61 | }
62 | if _, err = os.Stat(savePathSymlinkFile.RealPath + DownloadSuffix); err != nil {
63 | return true
64 | }
65 | }
66 | }
67 | return false
68 | }
69 |
--------------------------------------------------------------------------------
/internal/functions/panlogin/login_helper_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package panlogin
15 |
16 | import (
17 | "fmt"
18 | "github.com/tickstep/library-go/ids"
19 | "testing"
20 | "time"
21 | )
22 |
23 | func TestGetQRCodeLoginUrl(t *testing.T) {
24 | h := NewLoginHelper("http://localhost:8977")
25 | keyStr := ids.GetUniqueId("", 32)
26 | fmt.Println(keyStr)
27 | r, e := h.GetQRCodeLoginUrl(keyStr)
28 | fmt.Println(e)
29 | fmt.Println(r)
30 | }
31 |
32 | func TestGetQRCodeLoginResult(t *testing.T) {
33 | h := NewLoginHelper("http://localhost:8977")
34 | tokenId := "26e69f9978ba4574a1d66e58399fed4e"
35 | for {
36 | r, e := h.GetQRCodeLoginResult(tokenId)
37 | fmt.Println(e)
38 | fmt.Println(r)
39 | time.Sleep(1 * time.Second)
40 | if r.QrCodeStatus == "CONFIRMED" {
41 | break
42 | }
43 | }
44 | }
45 |
46 | func TestGetRefreshToken(t *testing.T) {
47 | h := NewLoginHelper("http://localhost:8977")
48 | tokenId := "26e69f9978ba4574a1d66e58399fed4e"
49 | r, e := h.GetRefreshToken(tokenId)
50 | fmt.Println(e)
51 | fmt.Println(r)
52 | }
53 |
54 | func TestParseRefreshToken(t *testing.T) {
55 | h := NewLoginHelper("http://localhost:8977")
56 | secureToken := "eff6054736d47b9c31f8839465555ebdff38c878ea0abbcd4b2336b30d33c71c7dac8824e40991654429ae521d8ef471"
57 | r, e := h.ParseSecureRefreshToken("", secureToken)
58 | fmt.Println(e)
59 | fmt.Println(r)
60 | }
61 |
--------------------------------------------------------------------------------
/internal/functions/panupload/upload_statistic.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package panupload
15 |
16 | import (
17 | "github.com/tickstep/aliyunpan/internal/functions"
18 | )
19 |
20 | type (
21 | UploadStatistic struct {
22 | functions.Statistic
23 | }
24 | )
25 |
--------------------------------------------------------------------------------
/internal/functions/panupload/utils.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package panupload
15 |
16 | import (
17 | "crypto/sha1"
18 | "encoding/hex"
19 | "github.com/tickstep/aliyunpan/internal/config"
20 | "github.com/tickstep/library-go/converter"
21 | "github.com/tickstep/library-go/logger"
22 | "net/url"
23 | "os"
24 | "path"
25 | "strconv"
26 | "strings"
27 | "time"
28 | )
29 |
30 | const (
31 | // MaxUploadBlockSize 最大上传的文件分片大小
32 | MaxUploadBlockSize = 2 * converter.GB
33 | // MinUploadBlockSize 最小的上传的文件分片大小
34 | MinUploadBlockSize = 4 * converter.MB
35 | // MaxRapidUploadSize 秒传文件支持的最大文件大小
36 | MaxRapidUploadSize = 20 * converter.GB
37 |
38 | // UploadingFileName 上传文件上传状态的文件名
39 | UploadingFileName = "aliyunpan_uploading.json"
40 | // UploadingBackupFileName 上传文件上传状态的副本
41 | UploadingBackupFileName = "aliyunpan_uploading.json.bak"
42 | )
43 |
44 | var (
45 | cmdUploadVerbose = logger.New("FILE_UPLOAD", config.EnvVerbose)
46 | )
47 |
48 | func getBlockSize(fileSize int64) int64 {
49 | blockNum := fileSize / MinUploadBlockSize
50 | if blockNum > 999 {
51 | return fileSize/999 + 1
52 | }
53 | return MinUploadBlockSize
54 | }
55 |
56 | // IsUrlExpired 上传链接是否已过期。过期返回True
57 | func IsUrlExpired(urlStr string) bool {
58 | u, err := url.Parse(urlStr)
59 | if err != nil {
60 | return true
61 | }
62 | expiredTimeSecStr := u.Query().Get("x-oss-expires")
63 | expiredTimeSec, _ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
64 | if (expiredTimeSec - time.Now().Unix()) <= 300 { // 小于5分钟
65 | // expired
66 | return true
67 | }
68 | return false
69 | }
70 |
71 | func IsVideoFile(fileName string) bool {
72 | if fileName == "" {
73 | return false
74 | }
75 | extName := strings.ToLower(path.Ext(fileName))
76 | if strings.Index(extName, ".") == 0 {
77 | extName = strings.TrimPrefix(extName, ".")
78 | }
79 | extList := config.Config.GetVideoExtensionList()
80 | for _, ext := range extList {
81 | if ext == extName {
82 | return true
83 | }
84 | }
85 | return false
86 | }
87 |
88 | // CalcFilePreHash 计算文件 PreHash
89 | func CalcFilePreHash(filePath string) string {
90 | localFile, _ := os.Open(filePath)
91 | defer localFile.Close()
92 | bytes := make([]byte, 1024)
93 | localFile.ReadAt(bytes, 0)
94 | sha1w := sha1.New()
95 | sha1w.Write(bytes)
96 | shaBytes := sha1w.Sum(nil)
97 | hashCode := hex.EncodeToString(shaBytes)
98 | return strings.ToUpper(hashCode)
99 | }
100 |
--------------------------------------------------------------------------------
/internal/functions/statistic.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package functions
15 |
16 | import (
17 | "sync/atomic"
18 | "time"
19 | )
20 |
21 | type (
22 | Statistic struct {
23 | totalSize int64
24 | startTime time.Time
25 | }
26 | )
27 |
28 | func (s *Statistic) AddTotalSize(size int64) int64 {
29 | return atomic.AddInt64(&s.totalSize, size)
30 | }
31 |
32 | func (s *Statistic) TotalSize() int64 {
33 | return s.totalSize
34 | }
35 |
36 | func (s *Statistic) StartTimer() {
37 | s.startTime = time.Now()
38 | }
39 |
40 | func (s *Statistic) Elapsed() time.Duration {
41 | return time.Now().Sub(s.startTime)
42 | }
43 |
--------------------------------------------------------------------------------
/internal/global/pan_file.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | type (
4 | FileSourceType string
5 | )
6 |
7 | const (
8 | // FileSource 文件,包括:资源盘文件、备份盘文件
9 | FileSource FileSourceType = "file"
10 | // AlbumSource 相册
11 | AlbumSource FileSourceType = "album"
12 | )
13 |
--------------------------------------------------------------------------------
/internal/global/vars.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | var (
4 | // AppVersion 应用版本
5 | AppVersion string
6 |
7 | // IsAppInCliMode 是否在交互模式
8 | IsAppInCliMode = false
9 |
10 | // IsSupportNoneOpenApiCommands 是否开启非OpenAPI的命令
11 | IsSupportNoneOpenApiCommands = false
12 | )
13 |
--------------------------------------------------------------------------------
/internal/localfile/checksum_write.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package localfile
15 |
16 | import (
17 | "hash"
18 | "io"
19 | )
20 |
21 | type (
22 | ChecksumWriter interface {
23 | io.Writer
24 | Sum() interface{}
25 | }
26 |
27 | ChecksumWriteUnit struct {
28 | SliceEnd int64
29 | End int64
30 | SliceSum interface{}
31 | Sum interface{}
32 | OnlySliceSum bool
33 | ChecksumWriter ChecksumWriter
34 |
35 | ptr int64
36 | }
37 |
38 | hashChecksumWriter struct {
39 | h hash.Hash
40 | }
41 |
42 | hash32ChecksumWriter struct {
43 | h hash.Hash32
44 | }
45 | )
46 |
47 | func (wi *ChecksumWriteUnit) handleEnd() error {
48 | if wi.ptr >= wi.End {
49 | // 已写完
50 | if !wi.OnlySliceSum {
51 | wi.Sum = wi.ChecksumWriter.Sum()
52 | }
53 | return ErrChecksumWriteStop
54 | }
55 | return nil
56 | }
57 |
58 | func (wi *ChecksumWriteUnit) write(p []byte) (n int, err error) {
59 | if wi.End <= 0 {
60 | // do nothing
61 | err = ErrChecksumWriteStop
62 | return
63 | }
64 | err = wi.handleEnd()
65 | if err != nil {
66 | return
67 | }
68 |
69 | var (
70 | i int
71 | left = wi.End - wi.ptr
72 | lenP = len(p)
73 | )
74 | if left < int64(lenP) {
75 | // 读取即将完毕
76 | i = int(left)
77 | } else {
78 | i = lenP
79 | }
80 | n, err = wi.ChecksumWriter.Write(p[:i])
81 | if err != nil {
82 | return
83 | }
84 | wi.ptr += int64(n)
85 | if left < int64(lenP) {
86 | err = wi.handleEnd()
87 | return
88 | }
89 | return
90 | }
91 |
92 | func (wi *ChecksumWriteUnit) Write(p []byte) (n int, err error) {
93 | if wi.SliceEnd <= 0 { // 忽略Slice
94 | // 读取全部
95 | n, err = wi.write(p)
96 | return
97 | }
98 |
99 | // 要计算Slice的情况
100 | // 调整slice
101 | if wi.SliceEnd > wi.End {
102 | wi.SliceEnd = wi.End
103 | }
104 |
105 | // 计算剩余Slice
106 | var (
107 | sliceLeft = wi.SliceEnd - wi.ptr
108 | )
109 | if sliceLeft <= 0 {
110 | // 已处理完Slice
111 | if wi.OnlySliceSum {
112 | err = ErrChecksumWriteStop
113 | return
114 | }
115 |
116 | // 继续处理
117 | n, err = wi.write(p)
118 | return
119 | }
120 |
121 | var (
122 | lenP = len(p)
123 | )
124 | if sliceLeft <= int64(lenP) {
125 | var n1, n2 int
126 | n1, err = wi.write(p[:sliceLeft])
127 | n += n1
128 | if err != nil {
129 | return
130 | }
131 | wi.SliceSum = wi.ChecksumWriter.Sum().([]byte)
132 | n2, err = wi.write(p[sliceLeft:])
133 | n += n2
134 | if err != nil {
135 | return
136 | }
137 | return
138 | }
139 | n, err = wi.write(p)
140 | return
141 | }
142 |
143 | func NewHashChecksumWriter(h hash.Hash) ChecksumWriter {
144 | return &hashChecksumWriter{
145 | h: h,
146 | }
147 | }
148 |
149 | func (hc *hashChecksumWriter) Write(p []byte) (n int, err error) {
150 | return hc.h.Write(p)
151 | }
152 |
153 | func (hc *hashChecksumWriter) Sum() interface{} {
154 | return hc.h.Sum(nil)
155 | }
156 |
157 | func NewHash32ChecksumWriter(h32 hash.Hash32) ChecksumWriter {
158 | return &hash32ChecksumWriter{
159 | h: h32,
160 | }
161 | }
162 |
163 | func (hc *hash32ChecksumWriter) Write(p []byte) (n int, err error) {
164 | return hc.h.Write(p)
165 | }
166 |
167 | func (hc *hash32ChecksumWriter) Sum() interface{} {
168 | return hc.h.Sum32()
169 | }
170 |
--------------------------------------------------------------------------------
/internal/localfile/errors.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package localfile
15 |
16 | import (
17 | "errors"
18 | )
19 |
20 | var (
21 | ErrFileIsNil = errors.New("file is nil")
22 | ErrChecksumWriteStop = errors.New("checksum write stop")
23 | ErrChecksumWriteAllStop = errors.New("checksum write all stop")
24 | )
25 |
--------------------------------------------------------------------------------
/internal/localfile/file.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package localfile
15 |
16 | import (
17 | "os"
18 | "path/filepath"
19 | "strings"
20 | )
21 |
22 | // EqualLengthMD5 检测md5和大小是否相同
23 | func (lfm *LocalFileMeta) EqualLengthMD5(m *LocalFileMeta) bool {
24 | if lfm.Length != m.Length {
25 | return false
26 | }
27 | if lfm.MD5 != m.MD5 {
28 | return false
29 | }
30 | return true
31 | }
32 |
33 | // EqualLengthSHA1 检测sha1和大小是否相同
34 | func (lfm *LocalFileMeta) EqualLengthSHA1(m *LocalFileMeta) bool {
35 | if lfm.Length != m.Length {
36 | return false
37 | }
38 | if lfm.SHA1 != m.SHA1 {
39 | return false
40 | }
41 | return true
42 | }
43 |
44 | // CompleteAbsPath 补齐绝对路径
45 | func (lfm *LocalFileMeta) CompleteAbsPath() {
46 | if filepath.IsAbs(lfm.Path.LogicPath) {
47 | return
48 | }
49 |
50 | absPath, err := filepath.Abs(lfm.Path.LogicPath)
51 | if err != nil {
52 | return
53 | }
54 | // windows
55 | if os.PathSeparator == '\\' {
56 | absPath = strings.ReplaceAll(absPath, "\\", "/")
57 | }
58 | lfm.Path.LogicPath = absPath
59 | }
60 |
61 | // GetFileSum 获取文件的大小, md5, crc32
62 | func GetFileSum(localPath string, flag int) (lfc *LocalFileEntity, err error) {
63 | lfc = NewLocalFileEntity(localPath)
64 | defer lfc.Close()
65 |
66 | err = lfc.OpenPath()
67 | if err != nil {
68 | return nil, err
69 | }
70 |
71 | err = lfc.Sum(flag)
72 | if err != nil {
73 | return nil, err
74 | }
75 | return lfc, nil
76 | }
77 |
--------------------------------------------------------------------------------
/internal/localfile/symlink_test.go:
--------------------------------------------------------------------------------
1 | package localfile
2 |
3 | import (
4 | "fmt"
5 | "github.com/tickstep/aliyunpan/internal/utils"
6 | "os"
7 | "path/filepath"
8 | "testing"
9 | )
10 |
11 | func TestMyWalkFile(t *testing.T) {
12 | count := 0
13 | walkFunc := func(file SymlinkFile, fi os.FileInfo, err error) error {
14 | if err != nil {
15 | fmt.Println(err)
16 | return err
17 | }
18 | count += 1
19 | fmt.Println("file: ", utils.ObjectToJsonStr(file, false))
20 | //fmt.Println("file: ", file)
21 | return nil
22 | }
23 |
24 | //curPath := "D:\\smb\\feny\\goprojects\\dev\\lks"
25 | curPath := "/Volumes/Downloads/dev/lks"
26 | file := NewSymlinkFile(curPath)
27 | if err := WalkAllFile(file, walkFunc); err != nil {
28 | if err != filepath.SkipDir {
29 | fmt.Printf("警告: 遍历错误: %s\n", err)
30 | }
31 | }
32 | fmt.Println("count: ", count)
33 | }
34 |
35 | func TestRetrieveRealPath(t *testing.T) {
36 | curPath := "/Volumes/Downloads/dev/lks/test"
37 | file := NewSymlinkFile(curPath)
38 | sf, _, e := RetrieveRealPath(file)
39 | if e != nil {
40 | fmt.Println(e)
41 | }
42 | fmt.Println(sf)
43 | }
44 |
45 | func TestRetrieveRealPathFromLogicPath(t *testing.T) {
46 | curPath := "/Volumes/Downloads/dev/lks/test/未命名文件夹cmd/sync_drive_config.json"
47 | sf, _, e := RetrieveRealPathFromLogicPath(curPath)
48 | if e != nil {
49 | fmt.Println(e)
50 | }
51 | fmt.Println(sf)
52 | }
53 |
54 | func TestRetrieveRealPathFromLogicSuffixPath(t *testing.T) {
55 | rootPath := NewSymlinkFile("/Volumes/Downloads/dev/测试同步盘/new_lks")
56 | rootPath, _, _ = RetrieveRealPath(rootPath)
57 | suffixPath := "test/未命名文件夹cmd/sync_drive_config.json"
58 | sf, _, e := RetrieveRealPathFromLogicSuffixPath(rootPath, suffixPath)
59 | if e != nil {
60 | fmt.Println(e)
61 | }
62 | fmt.Println(sf)
63 | }
64 |
--------------------------------------------------------------------------------
/internal/log/file_record.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "encoding/csv"
5 | "fmt"
6 | "github.com/tickstep/aliyunpan/internal/utils"
7 | "github.com/tickstep/library-go/converter"
8 | "github.com/tickstep/library-go/logger"
9 | "os"
10 | "path/filepath"
11 | "sync"
12 | )
13 |
14 | type (
15 | FileRecordItem struct {
16 | Status string `json:"status"`
17 | TimeStr string `json:"timeStr"`
18 | FileSize int64 `json:"fileSize"`
19 | FilePath string `json:"filePath"`
20 | }
21 |
22 | FileRecorder struct {
23 | Path string `json:"path"`
24 | locker *sync.Mutex
25 | }
26 | )
27 |
28 | // NewFileRecorder 创建文件记录器
29 | func NewFileRecorder(filePath string) *FileRecorder {
30 | return &FileRecorder{
31 | Path: filePath,
32 | locker: &sync.Mutex{},
33 | }
34 | }
35 |
36 | // Append 增加数据记录
37 | func (f *FileRecorder) Append(item *FileRecordItem) error {
38 | f.locker.Lock()
39 | defer f.locker.Unlock()
40 | savePath := f.Path
41 | folder := filepath.Dir(savePath)
42 | if b, err := utils.PathExists(folder); err == nil && !b {
43 | os.MkdirAll(folder, 0755)
44 | }
45 |
46 | var fp *os.File
47 | var write *csv.Writer
48 | if b, err := utils.PathExists(savePath); err == nil && b {
49 | file, err1 := os.OpenFile(savePath, os.O_APPEND, 0755)
50 | if err1 != nil {
51 | logger.Verbosef("打开文件["+savePath+"]失败,%v", err1)
52 | return err1
53 | }
54 | fp = file
55 | write = csv.NewWriter(fp) //创建一个新的写入文件流
56 | } else {
57 | file, err1 := os.OpenFile(savePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) // 创建文件句柄
58 | if err1 != nil {
59 | logger.Verbosef("创建文件["+savePath+"]失败,%v", err1)
60 | return err1
61 | }
62 | fp = file
63 | fp.WriteString("\xEF\xBB\xBF") // 写入UTF-8 BOM
64 | write = csv.NewWriter(fp) //创建一个新的写入文件流
65 | write.Write([]string{"状态", "时间", "文件大小", "文件路径"})
66 | }
67 | if fp == nil || write == nil {
68 | return fmt.Errorf("open recorder file error")
69 | }
70 | defer fp.Close()
71 |
72 | data := []string{item.Status, item.TimeStr, converter.ConvertFileSize(item.FileSize, 2), item.FilePath}
73 | write.Write(data)
74 | write.Flush()
75 | return nil
76 | }
77 |
--------------------------------------------------------------------------------
/internal/log/file_record_test.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestCsvFile(t *testing.T) {
8 | savePath := "D:\\smb\\feny\\goprojects\\dev\\logs\\file_upload_records.csv"
9 | recorder := NewFileRecorder(savePath)
10 | recorder.Append(&FileRecordItem{
11 | Status: "成功",
12 | TimeStr: "2022-12-19 16:46:36",
13 | FileSize: 453450,
14 | FilePath: "D:\\smb\\feny\\goprojects\\dev\\myfile.mp4",
15 | })
16 | }
17 |
--------------------------------------------------------------------------------
/internal/panupdate/github.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package panupdate
15 |
16 | type (
17 | // AssetInfo asset 信息
18 | AssetInfo struct {
19 | Name string `json:"name"`
20 | ContentType string `json:"content_type"`
21 | State string `json:"state"`
22 | Size int64 `json:"size"`
23 | BrowserDownloadURL string `json:"browser_download_url"`
24 | }
25 |
26 | // ReleaseInfo 发布信息
27 | ReleaseInfo struct {
28 | TagName string `json:"tag_name"`
29 | Assets []*AssetInfo `json:"assets"`
30 | }
31 | )
32 |
--------------------------------------------------------------------------------
/internal/panupdate/updatefile.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package panupdate
15 |
16 | import (
17 | "fmt"
18 | "io"
19 | "os"
20 | "path/filepath"
21 | )
22 |
23 | func update(targetPath string, src io.Reader) error {
24 | info, err := os.Stat(targetPath)
25 | if err != nil {
26 | if os.IsNotExist(err) {
27 | fmt.Printf("警告: 本地文件不存在 %s\n", targetPath)
28 | return nil
29 | }
30 | fmt.Printf("Warning: %s\n", err)
31 | return nil
32 | }
33 |
34 | privMode := info.Mode()
35 |
36 | oldPath := filepath.Join(filepath.Dir(targetPath), "old-"+filepath.Base(targetPath))
37 |
38 | err = os.Rename(targetPath, oldPath)
39 | if err != nil {
40 | return err
41 | }
42 |
43 | newFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY, privMode)
44 | if err != nil {
45 | return err
46 | }
47 |
48 | _, err = io.Copy(newFile, src)
49 | if err != nil {
50 | return err
51 | }
52 |
53 | err = newFile.Close()
54 | if err != nil {
55 | fmt.Printf("Warning: 关闭文件发生错误: %s\n", err)
56 | }
57 |
58 | err = os.Remove(oldPath)
59 | if err != nil {
60 | fmt.Printf("Warning: 移除旧文件发生错误: %s\n", err)
61 | }
62 | return nil
63 | }
64 |
--------------------------------------------------------------------------------
/internal/plugins/idle_plugin.go:
--------------------------------------------------------------------------------
1 | package plugins
2 |
3 | type (
4 | IdlePlugin struct {
5 | Name string
6 | }
7 | )
8 |
9 | func NewIdlePlugin() *IdlePlugin {
10 | return &IdlePlugin{
11 | Name: "IdlePlugin",
12 | }
13 | }
14 |
15 | func (p *IdlePlugin) Start() error {
16 | return nil
17 | }
18 |
19 | func (p *IdlePlugin) UploadFilePrepareCallback(context *Context, params *UploadFilePrepareParams) (*UploadFilePrepareResult, error) {
20 | return nil, nil
21 | }
22 |
23 | func (p *IdlePlugin) UploadFileFinishCallback(context *Context, params *UploadFileFinishParams) error {
24 | return nil
25 | }
26 |
27 | func (p *IdlePlugin) DownloadFilePrepareCallback(context *Context, params *DownloadFilePrepareParams) (*DownloadFilePrepareResult, error) {
28 | return nil, nil
29 | }
30 |
31 | func (p *IdlePlugin) DownloadFileFinishCallback(context *Context, params *DownloadFileFinishParams) error {
32 | return nil
33 | }
34 |
35 | func (p *IdlePlugin) SyncScanLocalFilePrepareCallback(context *Context, params *SyncScanLocalFilePrepareParams) (*SyncScanLocalFilePrepareResult, error) {
36 | return nil, nil
37 | }
38 |
39 | func (p *IdlePlugin) SyncScanPanFilePrepareCallback(context *Context, params *SyncScanPanFilePrepareParams) (*SyncScanPanFilePrepareResult, error) {
40 | return nil, nil
41 | }
42 |
43 | func (p *IdlePlugin) SyncFileFinishCallback(context *Context, params *SyncFileFinishParams) error {
44 | return nil
45 | }
46 |
47 | func (p *IdlePlugin) SyncAllFileFinishCallback(context *Context, params *SyncAllFileFinishParams) error {
48 | return nil
49 | }
50 | func (p *IdlePlugin) UserTokenRefreshFinishCallback(context *Context, params *UserTokenRefreshFinishParams) error {
51 | return nil
52 | }
53 |
54 | func (p *IdlePlugin) RemoveFilePrepareCallback(context *Context, params *RemoveFilePrepareParams) (*RemoveFilePrepareResult, error) {
55 | return nil, nil
56 | }
57 |
58 | func (p *IdlePlugin) Stop() error {
59 | return nil
60 | }
61 |
--------------------------------------------------------------------------------
/internal/plugins/plugin_manager.go:
--------------------------------------------------------------------------------
1 | package plugins
2 |
3 | import (
4 | "fmt"
5 | "github.com/tickstep/aliyunpan/internal/config"
6 | "github.com/tickstep/aliyunpan/internal/global"
7 | "github.com/tickstep/library-go/logger"
8 | "io/ioutil"
9 | "os"
10 | "path"
11 | "path/filepath"
12 | "strings"
13 | )
14 |
15 | type (
16 | PluginManager struct {
17 | PluginPath string
18 | }
19 | )
20 |
21 | func GetContext(user *config.PanUser) *Context {
22 | if user == nil {
23 | return &Context{
24 | AppName: "aliyunpan",
25 | Version: global.AppVersion,
26 | UserId: "",
27 | Nickname: "",
28 | FileDriveId: "",
29 | AlbumDriveId: "",
30 | }
31 | }
32 | return &Context{
33 | AppName: "aliyunpan",
34 | Version: global.AppVersion,
35 | UserId: user.UserId,
36 | Nickname: user.Nickname,
37 | FileDriveId: user.DriveList.GetFileDriveId(), // 备份盘
38 | AlbumDriveId: user.DriveList.GetAlbumDriveId(), // 相册盘,在OpenAPI接口中,这个相册盘ID是获取不到的
39 | ResourceDriveId: user.DriveList.GetResourceDriveId(), // 资源盘
40 | }
41 | }
42 |
43 | func NewPluginManager(pluginDir string) *PluginManager {
44 | return &PluginManager{
45 | PluginPath: pluginDir,
46 | }
47 | }
48 |
49 | func (p *PluginManager) SetPluginPath(pluginPath string) error {
50 | if fi, err := os.Stat(pluginPath); err == nil && fi.IsDir() {
51 | p.PluginPath = filepath.Clean(pluginPath)
52 | } else {
53 | return fmt.Errorf("path must be a folder")
54 | }
55 | return nil
56 | }
57 |
58 | func (p *PluginManager) GetPlugin() (Plugin, error) {
59 | // js plugins folder
60 | // only support js plugins right now
61 | jsPluginPath := path.Clean(p.PluginPath + string(os.PathSeparator) + "js")
62 | if fi, err := os.Stat(jsPluginPath); err == nil && fi.IsDir() {
63 | jsPlugin := NewJsPlugin()
64 | if jsPlugin.Start() != nil {
65 | logger.Verbosef("初始化JS脚本错误\n")
66 | return interface{}(NewIdlePlugin()).(Plugin), nil
67 | }
68 |
69 | jsPluginValid := false
70 | if files, e := ioutil.ReadDir(jsPluginPath); e == nil {
71 | for _, f := range files {
72 | if !f.IsDir() {
73 | if strings.HasPrefix(strings.ToLower(f.Name()), ".") || strings.HasPrefix(strings.ToLower(f.Name()), "~") {
74 | continue
75 | }
76 | if strings.HasSuffix(strings.ToLower(f.Name()), ".js") {
77 | // this is a js file
78 | bytes, re := ioutil.ReadFile(path.Clean(jsPluginPath + string(os.PathSeparator) + f.Name()))
79 | if re != nil {
80 | logger.Verbosef("读取JS脚本错误: %s\n", re)
81 | continue
82 | }
83 | var script = string(bytes)
84 | if jsPlugin.LoadScript(script) == nil {
85 | jsPluginValid = true
86 | logger.Verbosef("加载JS脚本成功: %s\n", f.Name())
87 | }
88 | }
89 | }
90 | }
91 | }
92 | if jsPluginValid {
93 | return interface{}(jsPlugin).(Plugin), nil
94 | }
95 | }
96 |
97 | // default idle plugins
98 | return interface{}(NewIdlePlugin()).(Plugin), nil
99 | }
100 |
--------------------------------------------------------------------------------
/internal/plugins/plugin_manager_test.go:
--------------------------------------------------------------------------------
1 | package plugins
2 |
3 | import (
4 | "fmt"
5 | jsoniter "github.com/json-iterator/go"
6 | "testing"
7 | )
8 |
9 | func TestPluginUpload(t *testing.T) {
10 | pluginManager := NewPluginManager("D:\\smb\\feny\\goprojects\\dev")
11 | plugin, err := pluginManager.GetPlugin()
12 | if err != nil {
13 | fmt.Println(err)
14 | }
15 | ctx := &Context{
16 | AppName: "aliyunpan",
17 | Version: "v0.1.3",
18 | UserId: "11001d48564f43b3bc5662874f04bb11",
19 | Nickname: "tickstep",
20 | FileDriveId: "19519121",
21 | AlbumDriveId: "29519122",
22 | }
23 | params := &UploadFilePrepareParams{
24 | LocalFilePath: "D:\\Program Files\\aliyunpan\\Downloads\\11001d48564f43b3bc5662874f04bb11\\token.bat",
25 | LocalFileName: "token.bat",
26 | LocalFileSize: 125330,
27 | LocalFileType: "file",
28 | LocalFileUpdatedAt: "2022-04-14 07:05:12",
29 | DriveId: "19519221",
30 | DriveFilePath: "aliyunpan/Downloads/11001d48564f43b3bc5662874f04bb11/token.bat",
31 | }
32 | b, _ := jsoniter.Marshal(ctx)
33 | fmt.Println(string(b))
34 | b, _ = jsoniter.Marshal(params)
35 | fmt.Println(string(b))
36 | r, e := plugin.UploadFilePrepareCallback(ctx, params)
37 | if e != nil {
38 | fmt.Println(e)
39 | }
40 | fmt.Println(r)
41 | }
42 |
43 | func TestPluginDownload(t *testing.T) {
44 | pluginManager := NewPluginManager("/Volumes/Downloads/dev/config/plugin")
45 | plugin, err := pluginManager.GetPlugin()
46 | if err != nil {
47 | fmt.Println(err)
48 | }
49 | ctx := &Context{
50 | AppName: "aliyunpan",
51 | Version: "v0.1.3",
52 | UserId: "11001d48564f43b3bc5662874f04bb11",
53 | Nickname: "tickstep",
54 | FileDriveId: "19519121",
55 | AlbumDriveId: "29519122",
56 | }
57 | params := &DownloadFilePrepareParams{
58 | DriveId: "19519221",
59 | DriveFilePath: "/test/aliyunpan/Downloads/token.bat",
60 | DriveFileName: "token.bat",
61 | DriveFileSize: 125330,
62 | DriveFileType: "file",
63 | DriveFileSha1: "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
64 | DriveFileUpdatedAt: "2022-04-14 07:05:12",
65 | LocalFilePath: "aliyunpan\\Downloads\\token.bat",
66 | }
67 | b, _ := jsoniter.Marshal(ctx)
68 | fmt.Println(string(b))
69 | b, _ = jsoniter.Marshal(params)
70 | fmt.Println(string(b))
71 | r, e := plugin.DownloadFilePrepareCallback(ctx, params)
72 | if e != nil {
73 | fmt.Println(e)
74 | }
75 | fmt.Println(r)
76 | }
77 |
--------------------------------------------------------------------------------
/internal/plugins/plugin_util_test.go:
--------------------------------------------------------------------------------
1 | package plugins
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestDeleteLocalFile(t *testing.T) {
9 | fmt.Println(DeleteLocalFile("/Volumes/Downloads/dev/upload/2"))
10 | }
11 |
12 | func TestSendEmail(t *testing.T) {
13 | fmt.Println(sendEmail("smtp.qq.com:465", "111xxx@qq.com", "xxxxxx", "12545xxx@qq.com", "title", "hello", "text", true))
14 | }
15 |
16 | func TestPutString(t *testing.T) {
17 | PersistenceFilePath = "/Volumes/Downloads/kv.bolt"
18 | PutString("test1", "ok1234-new")
19 | }
20 |
21 | func TestGetString(t *testing.T) {
22 | PersistenceFilePath = "/Volumes/Downloads/kv.bolt"
23 | v := GetString("test1")
24 | fmt.Println(v)
25 | }
26 |
--------------------------------------------------------------------------------
/internal/syncdrive/bolt_db_test.go:
--------------------------------------------------------------------------------
1 | package syncdrive
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/tickstep/bolt"
7 | "io/fs"
8 | "io/ioutil"
9 | "os"
10 | "strings"
11 | "testing"
12 | "time"
13 | )
14 |
15 | func TestPath(t *testing.T) {
16 | 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})
17 | if err != nil {
18 | return
19 | }
20 | defer db.Close()
21 |
22 | // add item
23 | // Start a writable transaction.
24 | tx, err := db.Begin(true)
25 | if err != nil {
26 | return
27 | }
28 | defer tx.Rollback()
29 |
30 | rootBucket, _ := tx.CreateBucketIfNotExists([]byte("/"))
31 | if e := rootBucket.Put([]byte("test"), []byte("test value")); e != nil {
32 | return
33 | }
34 | _, e := rootBucket.CreateBucketIfNotExists([]byte("test"))
35 | println(e)
36 |
37 | // Commit the transaction and check for error.
38 | if err := tx.Commit(); err != nil {
39 | return
40 | }
41 | }
42 |
43 | func dosomething(ctx context.Context) {
44 | for {
45 | select {
46 | case <-ctx.Done():
47 | fmt.Println("playing")
48 | return
49 | default:
50 | fmt.Println("I am working!")
51 | time.Sleep(time.Second)
52 | }
53 | }
54 | }
55 |
56 | func TestContext(t *testing.T) {
57 | ctx, cancelFunc := context.WithCancel(context.Background())
58 | go func() {
59 | time.Sleep(5 * time.Second)
60 | cancelFunc()
61 | }()
62 | dosomething(ctx)
63 | }
64 |
65 | func TestBolt(t *testing.T) {
66 | localFileDb := NewLocalSyncDb("D:\\smb\\feny\\goprojects\\dev\\sync_drive\\local.db")
67 | localFileDb.Open()
68 | defer localFileDb.Close()
69 | localFileDb.Add(&LocalFileItem{
70 | FileName: "dev",
71 | FileSize: 0,
72 | FileType: "folder",
73 | CreatedAt: "2022-05-12 10:21:14",
74 | UpdatedAt: "2022-05-12 10:21:14",
75 | FileExtension: ".db",
76 | Sha1Hash: "",
77 | Path: "D:\\smb\\feny\\goprojects\\dev",
78 | })
79 | localFileDb.Add(&LocalFileItem{
80 | FileName: "file1.db",
81 | FileSize: 0,
82 | FileType: "file",
83 | CreatedAt: "2022-05-12 10:21:14",
84 | UpdatedAt: "2022-05-12 10:21:14",
85 | FileExtension: ".db",
86 | Sha1Hash: "",
87 | Path: "D:\\smb\\feny\\goprojects\\dev\\file1.db",
88 | })
89 | go func(db LocalSyncDb) {
90 | for i := 1; i <= 10; i++ {
91 | sb := &strings.Builder{}
92 | fmt.Fprintf(sb, "D:\\smb\\feny\\goprojects\\dev\\go\\file%d.db", i)
93 | db.Add(&LocalFileItem{
94 | FileName: "file1.db",
95 | FileSize: 0,
96 | FileType: "file",
97 | CreatedAt: "2022-05-12 10:21:14",
98 | UpdatedAt: "2022-05-12 10:21:14",
99 | FileExtension: ".db",
100 | Sha1Hash: "",
101 | Path: sb.String(),
102 | })
103 | }
104 | }(localFileDb)
105 | time.Sleep(1 * time.Second)
106 | localFileDb.Add(&LocalFileItem{
107 | FileName: "file1.db",
108 | FileSize: 0,
109 | FileType: "file",
110 | CreatedAt: "2022-05-12 10:21:14",
111 | UpdatedAt: "2022-05-12 10:21:14",
112 | FileExtension: ".db",
113 | Sha1Hash: "",
114 | Path: "D:\\smb\\feny\\goprojects\\dev\\file3.db",
115 | })
116 | time.Sleep(5 * time.Second)
117 | }
118 |
119 | func TestBoltUltraFiles(t *testing.T) {
120 | localFileDb := NewLocalSyncDb("/Volumes/DataDisk3T/test/local.bolt")
121 | localFileDb.Open()
122 | defer localFileDb.Close()
123 |
124 | count := int64(0)
125 | walkFunc := func(parentDirPath string, infos []fs.FileInfo, err error) error {
126 | files := LocalFileList{}
127 | for _, info := range infos {
128 | if strings.Index(info.Name(), ".") == 0 {
129 | continue
130 | }
131 | count += 1
132 | file := parentDirPath + "/" + info.Name()
133 | fmt.Println(count, " - ", file)
134 | files = append(files, newLocalFileItem(info, file))
135 | }
136 | localFileDb.AddFileList(files)
137 | return nil
138 | }
139 | WalkAllFileFunc("/Volumes/Downloads", walkFunc)
140 | println("\ntotal: ", count)
141 | }
142 |
143 | type MyWalkFunc func(parentDirPath string, infos []fs.FileInfo, err error) error
144 |
145 | func WalkAllFileFunc(dirPath string, walkFn MyWalkFunc) error {
146 | info, err := os.Lstat(dirPath)
147 | if err != nil {
148 | infos := []os.FileInfo{}
149 | infos = append(infos, info)
150 | err = walkFn(dirPath, infos, err)
151 | } else {
152 | err = walkAllFileFunc(dirPath, info, walkFn)
153 | }
154 | return err
155 | }
156 |
157 | func walkAllFileFunc(dirPath string, info os.FileInfo, walkFn MyWalkFunc) error {
158 | if !info.IsDir() {
159 | infos := []os.FileInfo{}
160 | infos = append(infos, info)
161 | return walkFn(dirPath, infos, nil)
162 | }
163 |
164 | files, err := ioutil.ReadDir(dirPath)
165 | if err != nil {
166 | return walkFn(dirPath, nil, err)
167 | }
168 | if len(files) == 0 {
169 | return nil
170 | }
171 | err = walkFn(dirPath, files, err)
172 | if err != nil {
173 | return err
174 | }
175 | for _, fi := range files {
176 | if fi.IsDir() {
177 | err = walkAllFileFunc(dirPath+"/"+fi.Name(), fi, walkFn)
178 | if err != nil {
179 | return err
180 | }
181 | }
182 | }
183 | return nil
184 | }
185 |
--------------------------------------------------------------------------------
/internal/syncdrive/sync_constants.go:
--------------------------------------------------------------------------------
1 | package syncdrive
2 |
3 | const (
4 | // DownloadingFileSuffix 下载中文件后缀
5 | DownloadingFileSuffix string = ".aliyunpan"
6 |
7 | // TimeSecondsOf30Seconds 30秒
8 | TimeSecondsOf30Seconds int64 = 30
9 |
10 | // TimeSecondsOfOneMinute 一分钟秒数
11 | TimeSecondsOfOneMinute int64 = 60
12 |
13 | // TimeSecondsOf2Minute 2分钟秒数
14 | TimeSecondsOf2Minute int64 = 2 * TimeSecondsOfOneMinute
15 |
16 | // TimeSecondsOf5Minute 5分钟秒数
17 | TimeSecondsOf5Minute int64 = 5 * TimeSecondsOfOneMinute
18 |
19 | // TimeSecondsOf10Minute 10分钟秒数
20 | TimeSecondsOf10Minute int64 = 10 * TimeSecondsOfOneMinute
21 |
22 | // TimeSecondsOf30Minute 30分钟秒数
23 | TimeSecondsOf30Minute int64 = 30 * TimeSecondsOfOneMinute
24 |
25 | // TimeSecondsOf60Minute 60分钟秒数
26 | TimeSecondsOf60Minute int64 = 60 * TimeSecondsOfOneMinute
27 | )
28 |
29 | var (
30 | LogPrompt = false
31 | )
32 |
--------------------------------------------------------------------------------
/internal/syncdrive/sync_db_util.go:
--------------------------------------------------------------------------------
1 | package syncdrive
2 |
3 | import (
4 | "path"
5 | "regexp"
6 | "strings"
7 | )
8 |
9 | // FormatFilePath 格式化文件路径
10 | func FormatFilePath(filePath string) string {
11 | if filePath == "" {
12 | return ""
13 | }
14 |
15 | // 是否是windows路径
16 | matched, _ := regexp.MatchString("^([a-zA-Z]:)", filePath)
17 | if matched {
18 | // 去掉卷标签,例如:D:
19 | filePath = string([]rune(filePath)[2:])
20 | }
21 | filePath = strings.ReplaceAll(filePath, "\\", "/")
22 | return path.Clean(filePath)
23 | }
24 |
--------------------------------------------------------------------------------
/internal/syncdrive/sync_db_util_test.go:
--------------------------------------------------------------------------------
1 | package syncdrive
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestFormatFilePath(t *testing.T) {
9 | fmt.Println(FormatFilePath("D:\\-beyond\\p\\9168473.html"))
10 | }
11 |
12 | func TestFormatFilePath2(t *testing.T) {
13 | fmt.Println(FormatFilePath("/my/folder/test.txt"))
14 | }
15 |
--------------------------------------------------------------------------------
/internal/syncdrive/utils.go:
--------------------------------------------------------------------------------
1 | package syncdrive
2 |
3 | import (
4 | "fmt"
5 | "io/fs"
6 | "os"
7 | "path"
8 | "strings"
9 | )
10 |
11 | // GetPanFileFullPathFromLocalPath 获取网盘文件的路径
12 | func GetPanFileFullPathFromLocalPath(localFilePath, localRootPath, panRootPath string) string {
13 | localFilePath = strings.ReplaceAll(localFilePath, "\\", "/")
14 | localRootPath = strings.ReplaceAll(localRootPath, "\\", "/")
15 |
16 | relativePath := strings.TrimPrefix(localFilePath, localRootPath)
17 | panPath := path.Join(path.Clean(panRootPath), relativePath)
18 | return strings.ReplaceAll(panPath, "\\", "/")
19 | }
20 |
21 | // GetLocalFileFullPathFromPanPath 获取本地文件的路径
22 | func GetLocalFileFullPathFromPanPath(panFilePath, localRootPath, panRootPath string) string {
23 | panFilePath = strings.ReplaceAll(panFilePath, "\\", "/")
24 | panRootPath = strings.ReplaceAll(panRootPath, "\\", "/")
25 |
26 | relativePath := strings.TrimPrefix(panFilePath, panRootPath)
27 | return path.Join(path.Clean(localRootPath), relativePath)
28 | }
29 |
30 | // IsSymlinkFile 是否是软链接文件
31 | func IsSymlinkFile(file fs.FileInfo) bool {
32 | if file.Mode()&os.ModeSymlink != 0 {
33 | return true
34 | }
35 | return false
36 | }
37 |
38 | // PromptPrintln 输出提示消息到控制台
39 | func PromptPrintln(msg string) {
40 | if LogPrompt {
41 | //fmt.Println("[" + utils.NowTimeStr() + "] " + msg)
42 | fmt.Println(msg)
43 | }
44 | }
45 |
46 | func PromptPrint(msg string) {
47 | if LogPrompt {
48 | fmt.Print(msg)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/internal/taskframework/executor.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package taskframework
15 |
16 | import (
17 | "github.com/GeertJohan/go.incremental"
18 | "github.com/oleiade/lane"
19 | "github.com/tickstep/aliyunpan/internal/waitgroup"
20 | "strconv"
21 | "time"
22 | )
23 |
24 | type (
25 | TaskExecutor struct {
26 | incr *incremental.Int // 任务id生成
27 | deque *lane.Deque // 队列
28 | parallel int // 任务的最大并发量
29 |
30 | // 是否统计失败队列
31 | IsFailedDeque bool
32 | failedDeque *lane.Deque
33 | }
34 | )
35 |
36 | func NewTaskExecutor() *TaskExecutor {
37 | return &TaskExecutor{}
38 | }
39 |
40 | func (te *TaskExecutor) lazyInit() {
41 | if te.deque == nil {
42 | te.deque = lane.NewDeque()
43 | }
44 | if te.incr == nil {
45 | te.incr = &incremental.Int{}
46 | }
47 | if te.parallel < 1 {
48 | te.parallel = 1
49 | }
50 | if te.IsFailedDeque {
51 | te.failedDeque = lane.NewDeque()
52 | }
53 | }
54 |
55 | // 设置任务的最大并发量
56 | func (te *TaskExecutor) SetParallel(parallel int) {
57 | te.parallel = parallel
58 | }
59 |
60 | //Append 将任务加到任务队列末尾
61 | func (te *TaskExecutor) Append(unit TaskUnit, maxRetry int) *TaskInfo {
62 | te.lazyInit()
63 | taskInfo := &TaskInfo{
64 | id: strconv.Itoa(te.incr.Next()),
65 | maxRetry: maxRetry,
66 | }
67 | unit.SetTaskInfo(taskInfo)
68 | te.deque.Append(&TaskInfoItem{
69 | Info: taskInfo,
70 | Unit: unit,
71 | })
72 | return taskInfo
73 | }
74 |
75 | //AppendNoRetry 将任务加到任务队列末尾, 不重试
76 | func (te *TaskExecutor) AppendNoRetry(unit TaskUnit) {
77 | te.Append(unit, 0)
78 | }
79 |
80 | //Count 返回任务数量
81 | func (te *TaskExecutor) Count() int {
82 | if te.deque == nil {
83 | return 0
84 | }
85 | return te.deque.Size()
86 | }
87 |
88 | // Execute 执行任务
89 | // 一个任务对应一个文件上传
90 | func (te *TaskExecutor) Execute() {
91 | te.lazyInit()
92 |
93 | for {
94 | wg := waitgroup.NewWaitGroup(te.parallel)
95 | for {
96 | e := te.deque.Shift()
97 | if e == nil { // 任务为空
98 | break
99 | }
100 |
101 | // 获取任务
102 | task, ok := e.(*TaskInfoItem)
103 | if !ok {
104 | // type cast failed
105 | }
106 | wg.AddDelta()
107 |
108 | go func(task *TaskInfoItem) {
109 | defer wg.Done()
110 |
111 | result := task.Unit.Run()
112 |
113 | // 返回结果为空
114 | if result == nil {
115 | task.Unit.OnComplete(result)
116 | return
117 | }
118 |
119 | // 取消下载
120 | if result.Cancel {
121 | task.Unit.OnCancel(result)
122 | return
123 | }
124 |
125 | if result.Succeed {
126 | task.Unit.OnSuccess(result)
127 | task.Unit.OnComplete(result)
128 | return
129 | }
130 |
131 | // 需要进行重试
132 | if result.NeedRetry {
133 | // 重试次数超出限制
134 | // 执行失败
135 | if task.Info.IsExceedRetry() {
136 | task.Unit.OnFailed(result)
137 | if te.IsFailedDeque {
138 | // 加入失败队列
139 | te.failedDeque.Append(task)
140 | }
141 | task.Unit.OnComplete(result)
142 | return
143 | }
144 |
145 | task.Info.retry++ // 增加重试次数
146 | task.Unit.OnRetry(result) // 调用重试
147 | task.Unit.OnComplete(result)
148 |
149 | time.Sleep(task.Unit.RetryWait()) // 等待
150 | te.deque.Append(task) // 重新加入队列末尾
151 | return
152 | }
153 |
154 | // 执行失败
155 | task.Unit.OnFailed(result)
156 | if te.IsFailedDeque {
157 | // 加入失败队列
158 | te.failedDeque.Append(task)
159 | }
160 | task.Unit.OnComplete(result)
161 | }(task)
162 | }
163 |
164 | wg.Wait()
165 |
166 | // 没有任务了
167 | if te.deque.Size() == 0 {
168 | break
169 | }
170 | }
171 | }
172 |
173 | //FailedDeque 获取失败队列
174 | func (te *TaskExecutor) FailedDeque() *lane.Deque {
175 | return te.failedDeque
176 | }
177 |
178 | //Stop 停止执行
179 | func (te *TaskExecutor) Stop() {
180 |
181 | }
182 |
183 | //Pause 暂停执行
184 | func (te *TaskExecutor) Pause() {
185 |
186 | }
187 |
188 | //Resume 恢复执行
189 | func (te *TaskExecutor) Resume() {
190 | }
191 |
--------------------------------------------------------------------------------
/internal/taskframework/task_unit.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package taskframework
15 |
16 | import "time"
17 |
18 | type (
19 | TaskUnit interface {
20 | SetTaskInfo(info *TaskInfo)
21 | // Run 执行任务
22 | Run() (result *TaskUnitRunResult)
23 | // OnRetry 重试任务执行的方法
24 | // 当达到最大重试次数, 执行失败
25 | OnRetry(lastRunResult *TaskUnitRunResult)
26 | // OnSuccess 每次执行成功执行的方法
27 | OnSuccess(lastRunResult *TaskUnitRunResult)
28 | // OnFailed 每次执行失败执行的方法
29 | OnFailed(lastRunResult *TaskUnitRunResult)
30 | // OnComplete 每次执行结束执行的方法, 不管成功失败
31 | OnComplete(lastRunResult *TaskUnitRunResult)
32 | // OnCancel 取消下载
33 | OnCancel(lastRunResult *TaskUnitRunResult)
34 | // RetryWait 重试等待的时间
35 | RetryWait() time.Duration
36 | }
37 |
38 | // TaskUnitRunResult 任务单元执行结果
39 | TaskUnitRunResult struct {
40 | Succeed bool // 是否执行成功
41 | NeedRetry bool // 是否需要重试
42 | Cancel bool // 是否取消了任务
43 |
44 | // 以下是额外的信息
45 | Err error // 错误信息
46 | ResultCode int // 结果代码
47 | ResultMessage string // 结果描述
48 | Extra interface{} // 额外的信息
49 | }
50 | )
51 |
52 | var (
53 | // TaskUnitRunResultSuccess 任务执行成功
54 | TaskUnitRunResultSuccess = &TaskUnitRunResult{}
55 | )
56 |
--------------------------------------------------------------------------------
/internal/taskframework/taskframework_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package taskframework_test
15 |
16 | import (
17 | "fmt"
18 | "github.com/tickstep/aliyunpan/internal/taskframework"
19 | "testing"
20 | "time"
21 | )
22 |
23 | type (
24 | TestUnit struct {
25 | retry bool
26 | taskInfo *taskframework.TaskInfo
27 | }
28 | )
29 |
30 | func (tu *TestUnit) SetTaskInfo(taskInfo *taskframework.TaskInfo) {
31 | tu.taskInfo = taskInfo
32 | }
33 |
34 | func (tu *TestUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunResult) {
35 | fmt.Printf("[%s] error: %s, failed\n", tu.taskInfo.Id(), lastRunResult.Err)
36 | }
37 |
38 | func (tu *TestUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunResult) {
39 | fmt.Printf("[%s] success\n", tu.taskInfo.Id())
40 | }
41 |
42 | func (tu *TestUnit) OnComplete(lastRunResult *taskframework.TaskUnitRunResult) {
43 | fmt.Printf("[%s] complete\n", tu.taskInfo.Id())
44 | }
45 |
46 | func (tu *TestUnit) Run() (result *taskframework.TaskUnitRunResult) {
47 | fmt.Printf("[%s] running...\n", tu.taskInfo.Id())
48 | return &taskframework.TaskUnitRunResult{
49 | //Succeed: true,
50 | NeedRetry: true,
51 | }
52 | }
53 |
54 | func (tu *TestUnit) OnCancel(lastRunResult *taskframework.TaskUnitRunResult) {
55 |
56 | }
57 |
58 | func (tu *TestUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunResult) {
59 | fmt.Printf("[%s] prepare retry, times [%d/%d]...\n", tu.taskInfo.Id(), tu.taskInfo.Retry(), tu.taskInfo.MaxRetry())
60 | }
61 |
62 | func (tu *TestUnit) RetryWait() time.Duration {
63 | return 1 * time.Second
64 | }
65 |
66 | func TestTaskExecutor(t *testing.T) {
67 | te := taskframework.NewTaskExecutor()
68 | te.SetParallel(2)
69 | for i := 0; i < 3; i++ {
70 | tu := TestUnit{
71 | retry: false,
72 | }
73 | te.Append(&tu, 2)
74 | }
75 | te.Execute()
76 | }
77 |
--------------------------------------------------------------------------------
/internal/taskframework/taskinfo.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package taskframework
15 |
16 | type (
17 | TaskInfo struct {
18 | id string
19 | maxRetry int
20 | retry int
21 | }
22 |
23 | TaskInfoItem struct {
24 | Info *TaskInfo
25 | Unit TaskUnit
26 | }
27 | )
28 |
29 | // IsExceedRetry 重试次数达到限制
30 | func (t *TaskInfo) IsExceedRetry() bool {
31 | return t.retry >= t.maxRetry
32 | }
33 |
34 | func (t *TaskInfo) Id() string {
35 | return t.id
36 | }
37 |
38 | func (t *TaskInfo) MaxRetry() int {
39 | return t.maxRetry
40 | }
41 |
42 | func (t *TaskInfo) SetMaxRetry(maxRetry int) {
43 | t.maxRetry = maxRetry
44 | }
45 |
46 | func (t *TaskInfo) Retry() int {
47 | return t.retry
48 | }
49 |
--------------------------------------------------------------------------------
/internal/utils/utils_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func TestConvertTime(t *testing.T) {
10 | seconds := time.Duration(50) * time.Second
11 | fmt.Println(ConvertTime(seconds))
12 |
13 | seconds = time.Duration(150) * time.Second
14 | fmt.Println(ConvertTime(seconds))
15 |
16 | seconds = time.Duration(3600) * time.Second
17 | fmt.Println(ConvertTime(seconds))
18 |
19 | seconds = time.Duration(1246852) * time.Second
20 | fmt.Println(ConvertTime(seconds))
21 | }
22 |
23 | func TestUuidStr(t *testing.T) {
24 | fmt.Println(UuidStr())
25 | }
26 |
27 | func TestMd5Str(t *testing.T) {
28 | fmt.Println(Md5Str("123456"))
29 | }
30 |
31 | func TestParseTimeStr(t *testing.T) {
32 | fmt.Println(ParseTimeStr(""))
33 | }
34 |
35 | func TestIsAbsPath_ReturnTrue(t *testing.T) {
36 | fmt.Println(IsLocalAbsPath("D:\\my\\folder\\test"))
37 | }
38 |
39 | func TestIsAbsPath_ReturnFalse(t *testing.T) {
40 | fmt.Println(IsLocalAbsPath("my\\folder\\test"))
41 | }
42 |
43 | func TestResizeUploadBlockSize_ReturnDefaultBlockSize(t *testing.T) {
44 | MB := int64(1024 * 1024) // 1048576
45 | fileSize := int64(1073741824) // 90GB
46 | fmt.Println(ResizeUploadBlockSize(fileSize, 10*MB)) // 10485760 = 10240KB
47 | }
48 |
49 | func TestResizeUploadBlockSize_ReturnNewBlockSize(t *testing.T) {
50 | MB := int64(1024 * 1024) // 1048576
51 | fileSize := int64(107374182400) // 100GB
52 | fmt.Println(ResizeUploadBlockSize(fileSize, 10*MB)) // 10737664 = 10486KB
53 | }
54 | func TestParseVersionNum(t *testing.T) {
55 | fmt.Println(ParseVersionNum("v1.3.55"))
56 | }
57 |
58 | func TestParseVersionNum2(t *testing.T) {
59 | fmt.Println(ParseVersionNum("v0.3.10-dev"))
60 | }
61 |
62 | func TestParseVersionNum3(t *testing.T) {
63 | fmt.Println(ParseVersionNum("v0.3.5-1"))
64 | }
65 |
66 | func TestParseVersionNum4(t *testing.T) {
67 | fmt.Println(ParseVersionNum("v"))
68 | }
69 |
--------------------------------------------------------------------------------
/internal/waitgroup/wait_group.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package waitgroup
15 |
16 | import "sync"
17 |
18 | // WaitGroup 在 sync.WaitGroup 的基础上, 新增线程控制功能
19 | type WaitGroup struct {
20 | wg sync.WaitGroup
21 | p chan struct{}
22 |
23 | sync.RWMutex
24 | }
25 |
26 | // NewWaitGroup returns a pointer to a new `WaitGroup` object.
27 | // parallel 为最大并发数, 0 代表无限制
28 | func NewWaitGroup(parallel int) (w *WaitGroup) {
29 | w = &WaitGroup{
30 | wg: sync.WaitGroup{},
31 | }
32 |
33 | if parallel <= 0 {
34 | return
35 | }
36 |
37 | w.p = make(chan struct{}, parallel)
38 | return
39 | }
40 |
41 | // AddDelta sync.WaitGroup.Add(1)
42 | func (w *WaitGroup) AddDelta() {
43 | if w.p != nil {
44 | w.p <- struct{}{}
45 | }
46 |
47 | w.wg.Add(1)
48 | }
49 |
50 | // Done sync.WaitGroup.Done()
51 | func (w *WaitGroup) Done() {
52 | w.wg.Done()
53 |
54 | if w.p != nil {
55 | <-w.p
56 | }
57 | }
58 |
59 | // Wait 参照 sync.WaitGroup 的 Wait 方法
60 | func (w *WaitGroup) Wait() {
61 | w.wg.Wait()
62 | if w.p != nil {
63 | close(w.p)
64 | }
65 | }
66 |
67 | // Parallel 返回当前正在进行的任务数量
68 | func (w *WaitGroup) Parallel() int {
69 | return len(w.p)
70 | }
71 |
--------------------------------------------------------------------------------
/internal/waitgroup/wait_group_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package waitgroup
15 |
16 | import (
17 | "fmt"
18 | "testing"
19 | "time"
20 | )
21 |
22 | func TestWg(t *testing.T) {
23 | wg := NewWaitGroup(2)
24 | for i := 0; i < 60; i++ {
25 | wg.AddDelta()
26 | go func(i int) {
27 | fmt.Println(i, wg.Parallel())
28 | time.Sleep(1e9)
29 | wg.Done()
30 | }(i)
31 | }
32 | wg.Wait()
33 | }
34 |
--------------------------------------------------------------------------------
/library/collection/queue.go:
--------------------------------------------------------------------------------
1 | package collection
2 |
3 | import (
4 | "strings"
5 | "sync"
6 | )
7 |
8 | type QueueItem interface {
9 | HashCode() string
10 | }
11 |
12 | type Queue struct {
13 | queueList []interface{}
14 |
15 | mutex sync.Mutex
16 | }
17 |
18 | func NewFifoQueue() *Queue {
19 | return &Queue{}
20 | }
21 |
22 | func (q *Queue) Push(item interface{}) {
23 | q.mutex.Lock()
24 | defer q.mutex.Unlock()
25 | if q.queueList == nil {
26 | q.queueList = []interface{}{}
27 | }
28 | q.queueList = append(q.queueList, item)
29 | }
30 |
31 | func (q *Queue) PushUnique(item interface{}) {
32 | q.mutex.Lock()
33 | defer q.mutex.Unlock()
34 | if q.queueList == nil {
35 | q.queueList = []interface{}{}
36 | } else {
37 | for _, qItem := range q.queueList {
38 | if strings.Compare(item.(QueueItem).HashCode(), qItem.(QueueItem).HashCode()) == 0 {
39 | return
40 | }
41 | }
42 | }
43 | q.queueList = append(q.queueList, item)
44 | }
45 |
46 | func (q *Queue) Pop() interface{} {
47 | q.mutex.Lock()
48 | defer q.mutex.Unlock()
49 | if q.queueList == nil {
50 | q.queueList = []interface{}{}
51 | }
52 | if len(q.queueList) == 0 {
53 | return nil
54 | }
55 | item := q.queueList[0]
56 | q.queueList = q.queueList[1:]
57 | return item
58 | }
59 |
60 | func (q *Queue) Length() int {
61 | q.mutex.Lock()
62 | defer q.mutex.Unlock()
63 | return len(q.queueList)
64 | }
65 |
66 | func (q *Queue) Remove(item interface{}) {
67 | q.mutex.Lock()
68 | defer q.mutex.Unlock()
69 | if q.queueList == nil {
70 | q.queueList = []interface{}{}
71 | }
72 | if len(q.queueList) == 0 {
73 | return
74 | }
75 | j := 0
76 | for _, qItem := range q.queueList {
77 | if strings.Compare(item.(QueueItem).HashCode(), qItem.(QueueItem).HashCode()) != 0 {
78 | q.queueList[j] = qItem
79 | j++
80 | }
81 | }
82 | q.queueList = q.queueList[:j]
83 | return
84 | }
85 |
86 | func (q *Queue) Contains(item interface{}) bool {
87 | q.mutex.Lock()
88 | defer q.mutex.Unlock()
89 | if q.queueList == nil {
90 | q.queueList = []interface{}{}
91 | }
92 | if len(q.queueList) == 0 {
93 | return false
94 | }
95 | for _, qItem := range q.queueList {
96 | if strings.Compare(item.(QueueItem).HashCode(), qItem.(QueueItem).HashCode()) == 0 {
97 | return true
98 | }
99 | }
100 | return false
101 | }
102 |
--------------------------------------------------------------------------------
/library/collection/queue_test.go:
--------------------------------------------------------------------------------
1 | package collection
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | type item struct {
9 | Name string
10 | }
11 |
12 | func (i *item) HashCode() string {
13 | return i.Name
14 | }
15 |
16 | func TestRemove(t *testing.T) {
17 | q := NewFifoQueue()
18 | q.Push(&item{Name: "1"})
19 | q.Push(&item{Name: "2"})
20 | q.Push(&item{Name: "3"})
21 | q.Push(&item{Name: "4"})
22 | q.Remove(&item{Name: "3"})
23 | fmt.Println(q)
24 | }
25 |
--------------------------------------------------------------------------------
/library/filelocker/errors.go:
--------------------------------------------------------------------------------
1 | package filelocker
2 |
3 | import "errors"
4 |
5 | var (
6 | ErrTimeout = errors.New("timeout")
7 | )
8 |
--------------------------------------------------------------------------------
/library/filelocker/file_locker.go:
--------------------------------------------------------------------------------
1 | package filelocker
2 |
3 | import (
4 | "os"
5 | )
6 |
7 | const (
8 | lockExt = ".lock"
9 | )
10 |
11 | type (
12 | FileLocker struct {
13 | FilePath string
14 | LockFilePath string
15 | lockFile *os.File
16 | }
17 | )
18 |
19 | func NewFileLocker(path string) *FileLocker {
20 | return &FileLocker{
21 | FilePath: path,
22 | LockFilePath: path + lockExt,
23 | lockFile: nil,
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/library/filelocker/file_locker_test.go:
--------------------------------------------------------------------------------
1 | package filelocker
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "testing"
7 | "time"
8 | )
9 |
10 | func TestFlocker(t *testing.T) {
11 | // lock file first time - success
12 | locker := NewFileLocker("D:\\smb\\feny\\goprojects\\dev\\aliyunpan")
13 | e := LockFile(locker, 0755, true, 5*time.Second)
14 | fmt.Println(e)
15 |
16 | // lock file again - fail
17 | //time.Sleep(5 * time.Second)
18 | //e = flock(locker, 0755, true, 5*time.Second)
19 | //fmt.Println(e)
20 |
21 | // Unlock the file.
22 | if err := UnlockFile(locker); err != nil {
23 | log.Printf("funlock error: %s", err)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/library/filelocker/locker_solaris.go:
--------------------------------------------------------------------------------
1 | package filelocker
2 |
3 | import (
4 | "os"
5 | "syscall"
6 | "time"
7 | )
8 |
9 | // LockFile acquires an advisory lock on a file descriptor.
10 | func LockFile(locker *FileLocker, mode os.FileMode, exclusive bool, timeout time.Duration) error {
11 | f, err := os.OpenFile(locker.LockFilePath, os.O_CREATE, mode)
12 | if err != nil {
13 | return err
14 | }
15 | locker.lockFile = f
16 |
17 | var t time.Time
18 | for {
19 | // If we're beyond our timeout then return an error.
20 | // This can only occur after we've attempted a flock once.
21 | if t.IsZero() {
22 | t = time.Now()
23 | } else if timeout > 0 && time.Since(t) > timeout {
24 | return ErrTimeout
25 | }
26 | var lock syscall.Flock_t
27 | lock.Start = 0
28 | lock.Len = 0
29 | lock.Pid = 0
30 | lock.Whence = 0
31 | lock.Pid = 0
32 | if exclusive {
33 | lock.Type = syscall.F_WRLCK
34 | } else {
35 | lock.Type = syscall.F_RDLCK
36 | }
37 | err := syscall.FcntlFlock(locker.lockFile.Fd(), syscall.F_SETLK, &lock)
38 | if err == nil {
39 | return nil
40 | } else if err != syscall.EAGAIN {
41 | return err
42 | }
43 |
44 | // Wait for a bit and try again.
45 | time.Sleep(50 * time.Millisecond)
46 | }
47 | }
48 |
49 | // UnlockFile releases an advisory lock on a file descriptor.
50 | func UnlockFile(locker *FileLocker) error {
51 | var lock syscall.Flock_t
52 | lock.Start = 0
53 | lock.Len = 0
54 | lock.Type = syscall.F_UNLCK
55 | lock.Whence = 0
56 | return syscall.FcntlFlock(uintptr(locker.lockFile.Fd()), syscall.F_SETLK, &lock)
57 | }
58 |
--------------------------------------------------------------------------------
/library/filelocker/locker_unix.go:
--------------------------------------------------------------------------------
1 | //go:build !windows && !plan9 && !solaris
2 | // +build !windows,!plan9,!solaris
3 |
4 | package filelocker
5 |
6 | import (
7 | "os"
8 | "syscall"
9 | "time"
10 | )
11 |
12 | // LockFile acquires an advisory lock on a file descriptor.
13 | func LockFile(locker *FileLocker, mode os.FileMode, exclusive bool, timeout time.Duration) error {
14 | f, err := os.OpenFile(locker.LockFilePath, os.O_CREATE, mode)
15 | if err != nil {
16 | return err
17 | }
18 | locker.lockFile = f
19 |
20 | var t time.Time
21 | for {
22 | // If we're beyond our timeout then return an error.
23 | // This can only occur after we've attempted a flock once.
24 | if t.IsZero() {
25 | t = time.Now()
26 | } else if timeout > 0 && time.Since(t) > timeout {
27 | return ErrTimeout
28 | }
29 | flag := syscall.LOCK_SH
30 | if exclusive {
31 | flag = syscall.LOCK_EX
32 | }
33 |
34 | // Otherwise attempt to obtain an exclusive lock.
35 | err := syscall.Flock(int(locker.lockFile.Fd()), flag|syscall.LOCK_NB)
36 | if err == nil {
37 | return nil
38 | } else if err != syscall.EWOULDBLOCK {
39 | return err
40 | }
41 |
42 | // Wait for a bit and try again.
43 | time.Sleep(50 * time.Millisecond)
44 | }
45 | }
46 |
47 | // UnlockFile releases an advisory lock on a file descriptor.
48 | func UnlockFile(locker *FileLocker) error {
49 | return syscall.Flock(int(locker.lockFile.Fd()), syscall.LOCK_UN)
50 | }
51 |
--------------------------------------------------------------------------------
/library/filelocker/locker_windows.go:
--------------------------------------------------------------------------------
1 | package filelocker
2 |
3 | import (
4 | "os"
5 | "syscall"
6 | "time"
7 | "unsafe"
8 | )
9 |
10 | // LockFileEx code derived from golang build filemutex_windows.go @ v1.5.1
11 | var (
12 | modkernel32 = syscall.NewLazyDLL("kernel32.dll")
13 | procLockFileEx = modkernel32.NewProc("LockFileEx")
14 | procUnlockFileEx = modkernel32.NewProc("UnlockFileEx")
15 | )
16 |
17 | const (
18 | // see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx
19 | flagLockExclusive = 2
20 | flagLockFailImmediately = 1
21 |
22 | // see https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx
23 | errLockViolation syscall.Errno = 0x21
24 | )
25 |
26 | func lockFileEx(h syscall.Handle, flags, reserved, locklow, lockhigh uint32, ol *syscall.Overlapped) (err error) {
27 | r, _, err := procLockFileEx.Call(uintptr(h), uintptr(flags), uintptr(reserved), uintptr(locklow), uintptr(lockhigh), uintptr(unsafe.Pointer(ol)))
28 | if r == 0 {
29 | return err
30 | }
31 | return nil
32 | }
33 |
34 | func unlockFileEx(h syscall.Handle, reserved, locklow, lockhigh uint32, ol *syscall.Overlapped) (err error) {
35 | r, _, err := procUnlockFileEx.Call(uintptr(h), uintptr(reserved), uintptr(locklow), uintptr(lockhigh), uintptr(unsafe.Pointer(ol)), 0)
36 | if r == 0 {
37 | return err
38 | }
39 | return nil
40 | }
41 |
42 | // LockFile acquires an advisory lock on a file descriptor.
43 | func LockFile(locker *FileLocker, mode os.FileMode, exclusive bool, timeout time.Duration) error {
44 | // Create a separate lock file on windows because a process
45 | // cannot share an exclusive lock on the same file. This is
46 | // needed during Tx.WriteTo().
47 | f, err := os.OpenFile(locker.LockFilePath, os.O_CREATE, mode)
48 | if err != nil {
49 | return err
50 | }
51 | locker.lockFile = f
52 |
53 | var t time.Time
54 | for {
55 | // If we're beyond our timeout then return an error.
56 | // This can only occur after we've attempted a flock once.
57 | if t.IsZero() {
58 | t = time.Now()
59 | } else if timeout > 0 && time.Since(t) > timeout {
60 | return ErrTimeout
61 | }
62 |
63 | var flag uint32 = flagLockFailImmediately
64 | if exclusive {
65 | flag |= flagLockExclusive
66 | }
67 |
68 | err := lockFileEx(syscall.Handle(locker.lockFile.Fd()), flag, 0, 1, 0, &syscall.Overlapped{})
69 | if err == nil {
70 | return nil
71 | } else if err != errLockViolation {
72 | return err
73 | }
74 |
75 | // Wait for a bit and try again.
76 | time.Sleep(50 * time.Millisecond)
77 | }
78 | }
79 |
80 | // UnlockFile releases an advisory lock on a file descriptor.
81 | func UnlockFile(locker *FileLocker) error {
82 | err := unlockFileEx(syscall.Handle(locker.lockFile.Fd()), 0, 1, 0, &syscall.Overlapped{})
83 | locker.lockFile.Close()
84 | os.Remove(locker.LockFilePath)
85 | return err
86 | }
87 |
--------------------------------------------------------------------------------
/library/homedir/homedir.go:
--------------------------------------------------------------------------------
1 | package homedir
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "os"
7 | "os/exec"
8 | "path/filepath"
9 | "runtime"
10 | "strconv"
11 | "strings"
12 | "sync"
13 | )
14 |
15 | // DisableCache will disable caching of the home directory. Caching is enabled
16 | // by default.
17 | var DisableCache bool
18 |
19 | var homedirCache string
20 | var cacheLock sync.RWMutex
21 |
22 | // Dir returns the home directory for the executing user.
23 | //
24 | // This uses an OS-specific method for discovering the home directory.
25 | // An error is returned if a home directory cannot be detected.
26 | func Dir() (string, error) {
27 | if !DisableCache {
28 | cacheLock.RLock()
29 | cached := homedirCache
30 | cacheLock.RUnlock()
31 | if cached != "" {
32 | return cached, nil
33 | }
34 | }
35 |
36 | cacheLock.Lock()
37 | defer cacheLock.Unlock()
38 |
39 | var result string
40 | var err error
41 | if runtime.GOOS == "windows" {
42 | result, err = dirWindows()
43 | } else {
44 | // Unix-like system, so just assume Unix
45 | result, err = dirUnix()
46 | }
47 |
48 | if err != nil {
49 | return "", err
50 | }
51 | homedirCache = result
52 | return result, nil
53 | }
54 |
55 | // Expand expands the path to include the home directory if the path
56 | // is prefixed with `~`. If it isn't prefixed with `~`, the path is
57 | // returned as-is.
58 | func Expand(path string) (string, error) {
59 | if len(path) == 0 {
60 | return path, nil
61 | }
62 |
63 | if path[0] != '~' {
64 | return path, nil
65 | }
66 |
67 | if len(path) > 1 && path[1] != '/' && path[1] != '\\' {
68 | return "", errors.New("cannot expand user-specific home dir")
69 | }
70 |
71 | dir, err := Dir()
72 | if err != nil {
73 | return "", err
74 | }
75 |
76 | return filepath.Join(dir, path[1:]), nil
77 | }
78 |
79 | // Reset clears the cache, forcing the next call to Dir to re-detect
80 | // the home directory. This generally never has to be called, but can be
81 | // useful in tests if you're modifying the home directory via the HOME
82 | // env var or something.
83 | func Reset() {
84 | cacheLock.Lock()
85 | defer cacheLock.Unlock()
86 | homedirCache = ""
87 | }
88 |
89 | func dirUnix() (string, error) {
90 | homeEnv := "HOME"
91 | if runtime.GOOS == "plan9" {
92 | // On plan9, env vars are lowercase.
93 | homeEnv = "home"
94 | }
95 |
96 | // First prefer the HOME environmental variable
97 | if home := os.Getenv(homeEnv); home != "" {
98 | return home, nil
99 | }
100 |
101 | var stdout bytes.Buffer
102 |
103 | // If that fails, try OS specific commands
104 | if runtime.GOOS == "darwin" {
105 | cmd := exec.Command("sh", "-c", `dscl -q . -read /Users/"$(whoami)" NFSHomeDirectory | sed 's/^[^ ]*: //'`)
106 | cmd.Stdout = &stdout
107 | if err := cmd.Run(); err == nil {
108 | result := strings.TrimSpace(stdout.String())
109 | if result != "" {
110 | return result, nil
111 | }
112 | }
113 | } else {
114 | cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid()))
115 | cmd.Stdout = &stdout
116 | if err := cmd.Run(); err != nil {
117 | // If the error is ErrNotFound, we ignore it. Otherwise, return it.
118 | if err != exec.ErrNotFound {
119 | return "", err
120 | }
121 | } else {
122 | if passwd := strings.TrimSpace(stdout.String()); passwd != "" {
123 | // username:password:uid:gid:gecos:home:shell
124 | passwdParts := strings.SplitN(passwd, ":", 7)
125 | if len(passwdParts) > 5 {
126 | return passwdParts[5], nil
127 | }
128 | }
129 | }
130 | }
131 |
132 | // If all else fails, try the shell
133 | stdout.Reset()
134 | cmd := exec.Command("sh", "-c", "cd && pwd")
135 | cmd.Stdout = &stdout
136 | if err := cmd.Run(); err != nil {
137 | return "", err
138 | }
139 |
140 | result := strings.TrimSpace(stdout.String())
141 | if result == "" {
142 | return "", errors.New("blank output when reading home directory")
143 | }
144 |
145 | return result, nil
146 | }
147 |
148 | func dirWindows() (string, error) {
149 | // First prefer the HOME environmental variable
150 | if home := os.Getenv("HOME"); home != "" {
151 | return home, nil
152 | }
153 |
154 | // Prefer standard environment variable USERPROFILE
155 | if home := os.Getenv("USERPROFILE"); home != "" {
156 | return home, nil
157 | }
158 |
159 | drive := os.Getenv("HOMEDRIVE")
160 | path := os.Getenv("HOMEPATH")
161 | home := drive + path
162 | if drive == "" || path == "" {
163 | return "", errors.New("HOMEDRIVE, HOMEPATH, or USERPROFILE are blank")
164 | }
165 |
166 | return home, nil
167 | }
--------------------------------------------------------------------------------
/library/nets/util.go:
--------------------------------------------------------------------------------
1 | package nets
2 |
3 | import (
4 | "net"
5 | "strings"
6 | )
7 |
8 | type NetInterfaceInfoList []*NetInterfaceInfo
9 | type NetInterfaceInfo struct {
10 | Name string `json:"name"`
11 | Mac string `json:"mac"`
12 | IPv4 string `json:"ipv4"`
13 | IPv6 string `json:"ipv6"`
14 | }
15 |
16 | func (n *NetInterfaceInfoList) GetByName(name string) *NetInterfaceInfo {
17 | for _, v := range *n {
18 | if v.Name == name {
19 | return v
20 | }
21 | }
22 | return nil
23 | }
24 |
25 | // GetLocalNetInterfaceAddress 获取本地接口地址信息
26 | func GetLocalNetInterfaceAddress() (NetInterfaceInfoList, error) {
27 | interfaces, err := net.Interfaces()
28 | if err != nil {
29 | return nil, err
30 | }
31 | netList := NetInterfaceInfoList{}
32 | for _, inter := range interfaces {
33 | netInfo := &NetInterfaceInfo{
34 | Name: inter.Name,
35 | Mac: inter.HardwareAddr.String(),
36 | IPv4: "",
37 | IPv6: "",
38 | }
39 | addrs, err2 := inter.Addrs()
40 | if err2 != nil {
41 | continue
42 | }
43 | for _, address := range addrs {
44 | if ipnet, ok := address.(*net.IPNet); ok {
45 | if ipnet.IP.To4() != nil { // ipv4
46 | if ipnet.IP.String() != "" {
47 | netInfo.IPv4 = ipnet.IP.String()
48 | }
49 | } else if ipnet.IP.To16() != nil && !ipnet.IP.IsLoopback() { // ipv6
50 | if ipnet.IP.String() != "" && !strings.HasPrefix(ipnet.IP.String(), "fe80") { // 去掉本地IPv6地址
51 | netInfo.IPv6 = ipnet.IP.String()
52 | }
53 | }
54 | }
55 | }
56 | netList = append(netList, netInfo)
57 | }
58 | return netList, nil
59 | }
60 |
--------------------------------------------------------------------------------
/library/requester/transfer/download_instanceinfo.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package transfer
15 |
16 | import (
17 | "time"
18 | )
19 |
20 | type (
21 | //DownloadInstanceInfo 状态详细信息, 用于导出状态文件
22 | DownloadInstanceInfo struct {
23 | DownloadStatus *DownloadStatus
24 | Ranges RangeList
25 | }
26 |
27 | // DownloadInstanceInfoExport 断点续传
28 | DownloadInstanceInfoExport struct {
29 | RangeGenMode RangeGenMode `json:"rangeGenMode,omitempty"`
30 | TotalSize int64 `json:"totalSize,omitempty"`
31 | GenBegin int64 `json:"genBegin,omitempty"`
32 | BlockSize int64 `json:"blockSize,omitempty"`
33 | Ranges []*Range `json:"ranges,omitempty"`
34 | }
35 | )
36 |
37 | // GetInstanceInfo 从断点信息获取下载状态
38 | func (m *DownloadInstanceInfoExport) GetInstanceInfo() (eii *DownloadInstanceInfo) {
39 | eii = &DownloadInstanceInfo{
40 | Ranges: m.Ranges,
41 | }
42 |
43 | var downloaded int64
44 | switch m.RangeGenMode {
45 | case RangeGenMode_BlockSize:
46 | downloaded = m.GenBegin - eii.Ranges.Len()
47 | default:
48 | downloaded = m.TotalSize - eii.Ranges.Len()
49 | }
50 | eii.DownloadStatus = &DownloadStatus{
51 | startTime: time.Now(),
52 | totalSize: m.TotalSize,
53 | downloaded: downloaded,
54 | gen: NewRangeListGenBlockSize(m.TotalSize, m.GenBegin, m.BlockSize),
55 | }
56 | switch m.RangeGenMode {
57 | case RangeGenMode_BlockSize:
58 | eii.DownloadStatus.gen = NewRangeListGenBlockSize(m.TotalSize, m.GenBegin, m.BlockSize)
59 | default:
60 | eii.DownloadStatus.gen = NewRangeListGenDefault(m.TotalSize, m.TotalSize, len(m.Ranges), len(m.Ranges))
61 | }
62 | return eii
63 | }
64 |
65 | // SetInstanceInfo 从下载状态导出断点信息
66 | func (m *DownloadInstanceInfoExport) SetInstanceInfo(eii *DownloadInstanceInfo) {
67 | if eii == nil {
68 | return
69 | }
70 |
71 | if eii.DownloadStatus != nil {
72 | m.TotalSize = eii.DownloadStatus.TotalSize()
73 | if eii.DownloadStatus.gen != nil {
74 | m.GenBegin = eii.DownloadStatus.gen.LoadBegin()
75 | m.BlockSize = eii.DownloadStatus.gen.LoadBlockSize()
76 | m.RangeGenMode = eii.DownloadStatus.gen.RangeGenMode()
77 | } else {
78 | m.RangeGenMode = RangeGenMode_Default
79 | }
80 | }
81 | m.Ranges = eii.Ranges
82 | }
83 |
--------------------------------------------------------------------------------
/library/requester/transfer/download_status.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 tickstep.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package transfer
15 |
16 | import (
17 | "github.com/tickstep/library-go/requester/rio/speeds"
18 | "sync"
19 | "sync/atomic"
20 | "time"
21 | )
22 |
23 | type (
24 | //DownloadStatuser 下载状态接口
25 | DownloadStatuser interface {
26 | TotalSize() int64
27 | Downloaded() int64
28 | SpeedsPerSecond() int64
29 | TimeElapsed() time.Duration // 已开始时间
30 | TimeLeft() time.Duration // 预计剩余时间, 负数代表未知
31 | }
32 |
33 | //DownloadStatus 下载状态及统计信息
34 | DownloadStatus struct {
35 | totalSize int64 // 总大小
36 | downloaded int64 // 已下载的数据量
37 | speedsDownloaded int64 // 用于统计速度的downloaded
38 | maxSpeeds int64 // 最大下载速度
39 | tmpSpeeds int64 // 缓存的速度
40 | speedsStat speeds.Speeds // 速度统计 (注意对齐)
41 |
42 | startTime time.Time // 开始下载的时间
43 |
44 | rateLimit *speeds.RateLimit // 限速控制
45 |
46 | gen *RangeListGen // Range生成状态
47 | mu sync.Mutex
48 | }
49 | )
50 |
51 | // NewDownloadStatus 初始化DownloadStatus
52 | func NewDownloadStatus() *DownloadStatus {
53 | return &DownloadStatus{
54 | startTime: time.Now(),
55 | }
56 | }
57 |
58 | // SetRateLimit 设置限速
59 | func (ds *DownloadStatus) SetRateLimit(rl *speeds.RateLimit) {
60 | ds.rateLimit = rl
61 | }
62 |
63 | // SetTotalSize 设置总大小
64 | func (ds *DownloadStatus) SetTotalSize(size int64) {
65 | ds.totalSize = size
66 | }
67 |
68 | // AddDownloaded 增加已下载数据量
69 | func (ds *DownloadStatus) AddDownloaded(d int64) {
70 | atomic.AddInt64(&ds.downloaded, d)
71 | }
72 |
73 | // AddTotalSize 增加总大小 (不支持多线程)
74 | func (ds *DownloadStatus) AddTotalSize(size int64) {
75 | ds.totalSize += size
76 | }
77 |
78 | // AddSpeedsDownloaded 增加已下载数据量, 用于统计速度
79 | func (ds *DownloadStatus) AddSpeedsDownloaded(d int64) {
80 | if ds.rateLimit != nil {
81 | ds.rateLimit.Add(d)
82 | }
83 | ds.speedsStat.Add(d)
84 | }
85 |
86 | // SetMaxSpeeds 设置最大速度, 原子操作
87 | func (ds *DownloadStatus) SetMaxSpeeds(speeds int64) {
88 | if speeds > atomic.LoadInt64(&ds.maxSpeeds) {
89 | atomic.StoreInt64(&ds.maxSpeeds, speeds)
90 | }
91 | }
92 |
93 | // ClearMaxSpeeds 清空统计最大速度, 原子操作
94 | func (ds *DownloadStatus) ClearMaxSpeeds() {
95 | atomic.StoreInt64(&ds.maxSpeeds, 0)
96 | }
97 |
98 | // TotalSize 返回总大小
99 | func (ds *DownloadStatus) TotalSize() int64 {
100 | return ds.totalSize
101 | }
102 |
103 | // Downloaded 返回已下载数据量
104 | func (ds *DownloadStatus) Downloaded() int64 {
105 | return atomic.LoadInt64(&ds.downloaded)
106 | }
107 |
108 | // UpdateSpeeds 更新speeds
109 | func (ds *DownloadStatus) UpdateSpeeds() {
110 | atomic.StoreInt64(&ds.tmpSpeeds, ds.speedsStat.GetSpeeds())
111 | }
112 |
113 | // SpeedsPerSecond 返回每秒速度
114 | func (ds *DownloadStatus) SpeedsPerSecond() int64 {
115 | return atomic.LoadInt64(&ds.tmpSpeeds)
116 | }
117 |
118 | // MaxSpeeds 返回最大速度
119 | func (ds *DownloadStatus) MaxSpeeds() int64 {
120 | return atomic.LoadInt64(&ds.maxSpeeds)
121 | }
122 |
123 | // TimeElapsed 返回花费的时间
124 | func (ds *DownloadStatus) TimeElapsed() (elapsed time.Duration) {
125 | return time.Since(ds.startTime)
126 | }
127 |
128 | // TimeLeft 返回预计剩余时间
129 | func (ds *DownloadStatus) TimeLeft() (left time.Duration) {
130 | speeds := atomic.LoadInt64(&ds.tmpSpeeds)
131 | if speeds <= 0 {
132 | left = -1
133 | } else {
134 | left = time.Duration((ds.totalSize-ds.downloaded)/(speeds)) * time.Second
135 | }
136 | return
137 | }
138 |
139 | // RangeListGen 返回RangeListGen
140 | func (ds *DownloadStatus) RangeListGen() *RangeListGen {
141 | return ds.gen
142 | }
143 |
144 | // SetRangeListGen 设置RangeListGen
145 | func (ds *DownloadStatus) SetRangeListGen(gen *RangeListGen) {
146 | ds.gen = gen
147 | }
148 |
--------------------------------------------------------------------------------
/resource_windows_386.syso:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/resource_windows_386.syso
--------------------------------------------------------------------------------
/resource_windows_amd64.syso:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tickstep/aliyunpan/e1ac6a95b96137e1f34ae16842f206d68e636925/resource_windows_amd64.syso
--------------------------------------------------------------------------------
/versioninfo.json:
--------------------------------------------------------------------------------
1 | {
2 | "FixedFileInfo": {
3 | "FileVersion": {
4 | "Major": 0,
5 | "Minor": 3,
6 | "Patch": 7,
7 | "Build": 0
8 | },
9 | "ProductVersion": {
10 | "Major": 0,
11 | "Minor": 3,
12 | "Patch": 7,
13 | "Build": 0
14 | },
15 | "FileFlagsMask": "3f",
16 | "FileFlags ": "00",
17 | "FileOS": "040004",
18 | "FileType": "01",
19 | "FileSubType": "00"
20 | },
21 | "StringFileInfo": {
22 | "Comments": "",
23 | "CompanyName": "tickstep",
24 | "FileDescription": "阿里云盘客户端",
25 | "FileVersion": "v0.3.7",
26 | "InternalName": "",
27 | "LegalCopyright": "© 2021-2025 tickstep.",
28 | "LegalTrademarks": "",
29 | "OriginalFilename": "",
30 | "PrivateBuild": "",
31 | "ProductName": "aliyunpan",
32 | "ProductVersion": "v0.3.7",
33 | "SpecialBuild": ""
34 | },
35 | "VarFileInfo": {
36 | "Translation": {
37 | "LangID": "0409",
38 | "CharsetID": "04B0"
39 | }
40 | },
41 | "IconPath": "assets/aliyunpan.ico",
42 | "ManifestPath": "aliyunpan.exe.manifest"
43 | }
--------------------------------------------------------------------------------
/win_build.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | REM ============= build script for windows ================
4 | REM how to use
5 | REM win_build.bat v0.0.1
6 | REM =======================================================
7 |
8 | REM ============= variable definitions ================
9 | set currentDir=%CD%
10 | set output=out
11 | set name=aliyunpan
12 | set version=%1
13 |
14 | REM ============= build action ================
15 | call :build_task %name%-%version%-windows-x86 windows 386
16 | call :build_task %name%-%version%-windows-x64 windows amd64
17 | call :build_task %name%-%version%-linux-386 linux 386
18 | call :build_task %name%-%version%-linux-amd64 linux amd64
19 | call :build_task %name%-%version%-darwin-macos-amd64 darwin amd64
20 |
21 | goto:EOF
22 |
23 | REM ============= build function ================
24 | :build_task
25 | setlocal
26 |
27 | set targetName=%1
28 | set GOOS=%2
29 | set GOARCH=%3
30 | set goarm=%4
31 | set GO386=sse2
32 | set CGO_ENABLED=0
33 | set GOARM=%goarm%
34 |
35 | echo "Building %targetName% ..."
36 | if %GOOS% == windows (
37 | goversioninfo -o=resource_windows_386.syso
38 | goversioninfo -64 -o=resource_windows_amd64.syso
39 | go build -ldflags "-linkmode internal -X main.Version=%version% -s -w" -o "%output%/%1/%name%.exe"
40 | ) ^
41 | else (
42 | go build -ldflags "-X main.Version=%version% -s -w" -o "%output%/%1/%name%"
43 | )
44 |
45 | copy README.md %output%\%1
46 | copy docs\manual.md %output%\%1
47 |
48 | mkdir %output%\%1\plugin
49 | xcopy /e assets\plugin %output%\%1\plugin
50 |
51 | mkdir %output%\%1\sync_drive
52 | xcopy /e assets\sync_drive %output%\%1\sync_drive
53 |
54 | xcopy /e assets\scripts %output%\%1
55 | endlocal
56 |
57 |
--------------------------------------------------------------------------------