├── .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 | --------------------------------------------------------------------------------