├── .gitignore ├── LICENSE ├── README.md ├── assets ├── caution.png ├── cloudpan189-go.ico ├── cloudpan189-go.png └── images │ ├── debug-log-screenshot.png │ └── win10-env-debug-config.png ├── bin ├── macOS │ └── goversioninfo └── windows │ └── goversioninfo.exe ├── build.sh ├── cloudpan189-go.exe.manifest ├── 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 ├── docs ├── complie_project.md └── manual.md ├── entitlements.xml ├── go.mod ├── go.sum ├── internal ├── command │ ├── backup.go │ ├── cd.go │ ├── command.go │ ├── cp_mv.go │ ├── download.go │ ├── export_file.go │ ├── family_list.go │ ├── import_file.go │ ├── login.go │ ├── ls_search.go │ ├── mkdir.go │ ├── quota.go │ ├── recycle.go │ ├── rename.go │ ├── rm.go │ ├── share.go │ ├── upload.go │ ├── user_info.go │ ├── user_sign.go │ ├── utils.go │ └── xcp.go ├── config │ ├── cache.go │ ├── errors.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 │ ├── panupload │ │ ├── sync_database.go │ │ ├── sync_database_bolt.go │ │ ├── upload.go │ │ ├── upload_database.go │ │ ├── upload_statistic.go │ │ ├── upload_task_unit.go │ │ └── utils.go │ └── statistic.go ├── localfile │ ├── checksum_write.go │ ├── errors.go │ ├── file.go │ └── localfile.go ├── panupdate │ ├── github.go │ ├── panupdate.go │ └── updatefile.go ├── taskframework │ ├── executor.go │ ├── task_unit.go │ ├── taskframework_test.go │ └── taskinfo.go ├── utils │ └── utils.go └── waitgroup │ ├── wait_group.go │ └── wait_group_test.go ├── library ├── crypto │ └── crypto.go ├── homedir │ └── homedir.go └── requester │ └── transfer │ ├── download_instanceinfo.go │ ├── download_status.go │ └── rangelist.go ├── main.go ├── package ├── debian │ ├── Packages.sh │ ├── copyright │ └── linux-amd64 │ │ └── control └── rpm │ └── cloudpan189-go-rpm-src.tar ├── resource_windows_386.syso ├── resource_windows_amd64.syso ├── versioninfo.json └── win_build.bat /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | cloudpan189-go 7 | cloudpan189-go.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 | cloud189_config.json 29 | cloud189_command_history.txt 30 | cloud189_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 | main_test.go 44 | git_push.sh -------------------------------------------------------------------------------- /assets/caution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tickstep/cloudpan189-go/58c99009978e9dd4761cff5632df31db601562f9/assets/caution.png -------------------------------------------------------------------------------- /assets/cloudpan189-go.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tickstep/cloudpan189-go/58c99009978e9dd4761cff5632df31db601562f9/assets/cloudpan189-go.ico -------------------------------------------------------------------------------- /assets/cloudpan189-go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tickstep/cloudpan189-go/58c99009978e9dd4761cff5632df31db601562f9/assets/cloudpan189-go.png -------------------------------------------------------------------------------- /assets/images/debug-log-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tickstep/cloudpan189-go/58c99009978e9dd4761cff5632df31db601562f9/assets/images/debug-log-screenshot.png -------------------------------------------------------------------------------- /assets/images/win10-env-debug-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tickstep/cloudpan189-go/58c99009978e9dd4761cff5632df31db601562f9/assets/images/win10-env-debug-config.png -------------------------------------------------------------------------------- /bin/macOS/goversioninfo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tickstep/cloudpan189-go/58c99009978e9dd4761cff5632df31db601562f9/bin/macOS/goversioninfo -------------------------------------------------------------------------------- /bin/windows/goversioninfo.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tickstep/cloudpan189-go/58c99009978e9dd4761cff5632df31db601562f9/bin/windows/goversioninfo.exe -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # how to use 4 | # for macOS & linux, run this command in shell 5 | # ./build.sh v0.1.0 6 | 7 | name="cloudpan189-go" 8 | version=$1 9 | 10 | if [ "$1" = "" ]; then 11 | version=v1.0.0 12 | fi 13 | 14 | output="out" 15 | 16 | default_golang() { 17 | export GOROOT=/usr/local/go 18 | go=$GOROOT/bin/go 19 | } 20 | 21 | Build() { 22 | default_golang 23 | goarm=$4 24 | if [ "$4" = "" ]; then 25 | goarm=7 26 | fi 27 | 28 | echo "Building $1..." 29 | export GOOS=$2 GOARCH=$3 GO386=sse2 CGO_ENABLED=0 GOARM=$4 30 | if [ $2 = "windows" ]; then 31 | goversioninfo -o=resource_windows_386.syso 32 | goversioninfo -64 -o=resource_windows_amd64.syso 33 | $go build -ldflags "-X main.Version=$version -s -w" -o "$output/$1/$name.exe" 34 | RicePack $1 $name.exe 35 | else 36 | $go build -ldflags "-X main.Version=$version -s -w" -o "$output/$1/$name" 37 | RicePack $1 $name 38 | fi 39 | 40 | Pack $1 $2 41 | } 42 | 43 | AndroidBuild() { 44 | default_golang 45 | echo "Building $1..." 46 | export GOOS=$2 GOARCH=$3 GOARM=$4 CGO_ENABLED=1 47 | $go build -ldflags "-X main.Version=$version -s -w -linkmode=external -extldflags=-pie" -o "$output/$1/$name" 48 | 49 | RicePack $1 $name 50 | Pack $1 $2 51 | } 52 | 53 | IOSBuild() { 54 | default_golang 55 | echo "Building $1..." 56 | mkdir -p "$output/$1" 57 | cd "$output/$1" 58 | export CC=/usr/local/go/misc/ios/clangwrap.sh GOOS=ios GOARCH=arm64 GOARM=7 CGO_ENABLED=1 59 | $go build -ldflags "-X main.Version=$version -s -w" -o $name github.com/tickstep/cloudpan189-go 60 | jtool --sign --inplace --ent ../../entitlements.xml $name 61 | cd ../.. 62 | RicePack $1 $name 63 | Pack $1 "ios" 64 | } 65 | 66 | # zip 打包 67 | Pack() { 68 | if [ $2 != "windows" ]; then 69 | chmod +x "$output/$1/$name" 70 | fi 71 | 72 | cp README.md "$output/$1" 73 | 74 | cd $output 75 | zip -q -r "$1.zip" "$1" 76 | 77 | # 删除 78 | rm -rf "$1" 79 | 80 | cd .. 81 | } 82 | 83 | # rice 打包静态资源 84 | RicePack() { 85 | return # 已取消web功能 86 | } 87 | 88 | # Android 89 | export ANDROID_NDK_ROOT=/Users/tickstep/Applications/android_ndk/android-ndk-r23-darwin 90 | CC=$ANDROID_NDK_ROOT/bin/arm-linux-androideabi/bin/clang AndroidBuild $name-$version"-android-api16-armv7" android arm 7 91 | CC=$ANDROID_NDK_ROOT/bin/aarch64-linux-android/bin/clang AndroidBuild $name-$version"-android-api21-arm64" android arm64 7 92 | CC=$ANDROID_NDK_ROOT/bin/i686-linux-android/bin/clang AndroidBuild $name-$version"-android-api16-386" android 386 7 93 | CC=$ANDROID_NDK_ROOT/bin/x86_64-linux-android/bin/clang AndroidBuild $name-$version"-android-api21-amd64" android amd64 7 94 | 95 | # iOS 96 | IOSBuild $name-$version"-ios-arm64" 97 | 98 | # OS X / macOS 99 | Build $name-$version"-darwin-macos-amd64" darwin amd64 100 | # Build $name-$version"-darwin-macos-386" darwin 386 101 | Build $name-$version"-darwin-macos-arm64" darwin arm64 102 | 103 | # Windows 104 | Build $name-$version"-windows-x86" windows 386 105 | Build $name-$version"-windows-x64" windows amd64 106 | Build $name-$version"-windows-arm" windows arm 107 | 108 | # Linux 109 | Build $name-$version"-linux-386" linux 386 110 | Build $name-$version"-linux-amd64" linux amd64 111 | Build $name-$version"-linux-armv5" linux arm 5 112 | Build $name-$version"-linux-armv7" linux arm 7 113 | Build $name-$version"-linux-arm64" linux arm64 114 | GOMIPS=softfloat Build $name-$version"-linux-mips" linux mips 115 | Build $name-$version"-linux-mips64" linux mips64 116 | GOMIPS=softfloat Build $name-$version"-linux-mipsle" linux mipsle 117 | Build $name-$version"-linux-mips64le" linux mips64le 118 | # Build $name-$version"-linux-ppc64" linux ppc64 119 | # Build $name-$version"-linux-ppc64le" linux ppc64le 120 | # Build $name-$version"-linux-s390x" linux s390x 121 | 122 | # Others 123 | # Build $name-$version"-solaris-amd64" solaris amd64 124 | Build $name-$version"-freebsd-386" freebsd 386 125 | Build $name-$version"-freebsd-amd64" freebsd amd64 126 | # Build $name-$version"-freebsd-arm" freebsd arm 127 | # Build $name-$version"-netbsd-386" netbsd 386 128 | # Build $name-$version"-netbsd-amd64" netbsd amd64 129 | # Build $name-$version"-netbsd-arm" netbsd arm 130 | # Build $name-$version"-openbsd-386" openbsd 386 131 | # Build $name-$version"-openbsd-amd64" openbsd amd64 132 | # Build $name-$version"-openbsd-arm" openbsd arm 133 | # Build $name-$version"-plan9-386" plan9 386 134 | # Build $name-$version"-plan9-amd64" plan9 amd64 135 | # Build $name-$version"-plan9-arm" plan9 arm 136 | # Build $name-$version"-nacl-386" nacl 386 137 | # Build $name-$version"-nacl-amd64p32" nacl amd64p32 138 | # Build $name-$version"-nacl-arm" nacl arm 139 | # Build $name-$version"-dragonflybsd-amd64" dragonfly amd64 140 | 141 | # 龙芯 LoongArch 142 | Build $name-$version"-linux-loong64" linux loong64 -------------------------------------------------------------------------------- /cloudpan189-go.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | true 12 | 13 | 14 | -------------------------------------------------------------------------------- /cmder/cmder_helper.go: -------------------------------------------------------------------------------- 1 | package cmder 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tickstep/cloudpan189-api/cloudpan" 6 | "github.com/tickstep/cloudpan189-api/cloudpan/apierror" 7 | "github.com/tickstep/cloudpan189-go/cmder/cmdliner" 8 | "github.com/tickstep/cloudpan189-go/internal/config" 9 | "github.com/tickstep/library-go/logger" 10 | "github.com/urfave/cli" 11 | "sync" 12 | ) 13 | 14 | var ( 15 | appInstance *cli.App 16 | 17 | saveConfigMutex *sync.Mutex = new(sync.Mutex) 18 | 19 | ReloadConfigFunc = func(c *cli.Context) error { 20 | err := config.Config.Reload() 21 | if err != nil { 22 | fmt.Printf("重载配置错误: %s\n", err) 23 | } 24 | return nil 25 | } 26 | 27 | SaveConfigFunc = func(c *cli.Context) error { 28 | saveConfigMutex.Lock() 29 | defer saveConfigMutex.Unlock() 30 | err := config.Config.Save() 31 | if err != nil { 32 | fmt.Printf("保存配置错误: %s\n", err) 33 | } 34 | return nil 35 | } 36 | ) 37 | 38 | func SetApp(app *cli.App) { 39 | appInstance = app 40 | } 41 | 42 | func App() *cli.App { 43 | return appInstance 44 | } 45 | 46 | func DoLoginHelper(username, password string) (usernameStr, passwordStr string, webToken cloudpan.WebLoginToken, appToken cloudpan.AppLoginToken, error error) { 47 | line := cmdliner.NewLiner() 48 | defer line.Close() 49 | 50 | if username == "" { 51 | username, error = line.State.Prompt("请输入用户名(手机号/邮箱/别名), 回车键提交 > ") 52 | if error != nil { 53 | return 54 | } 55 | } 56 | 57 | if password == "" { 58 | // liner 的 PasswordPrompt 不安全, 拆行之后密码就会显示出来了 59 | fmt.Printf("请输入密码(输入的密码无回显, 确认输入完成, 回车提交即可) > ") 60 | password, error = line.State.PasswordPrompt("") 61 | if error != nil { 62 | return 63 | } 64 | } 65 | 66 | // app login 67 | atoken, apperr := cloudpan.AppLogin(username, password) 68 | if apperr != nil { 69 | fmt.Println("APP登录失败:", apperr) 70 | return "", "", webToken, appToken, fmt.Errorf("登录失败") 71 | } 72 | 73 | // web cookie 74 | wtoken := &cloudpan.WebLoginToken{} 75 | cookieLoginUser := cloudpan.RefreshCookieToken(atoken.SessionKey) 76 | if cookieLoginUser != "" { 77 | logger.Verboseln("get COOKIE_LOGIN_USER by session key") 78 | wtoken.CookieLoginUser = cookieLoginUser 79 | } else { 80 | // try login directly 81 | wtoken, apperr = cloudpan.Login(username, password) 82 | if apperr != nil { 83 | if apperr.Code == apierror.ApiCodeNeedCaptchaCode { 84 | for i := 0; i < 10; i++ { 85 | // 需要认证码 86 | savePath, apiErr := cloudpan.GetCaptchaImage() 87 | if apiErr != nil { 88 | fmt.Errorf("获取认证码错误") 89 | return "", "", webToken, appToken, apiErr 90 | } 91 | fmt.Printf("打开以下路径, 以查看验证码\n%s\n\n", savePath) 92 | vcode, err := line.State.Prompt("请输入验证码 > ") 93 | if err != nil { 94 | return "", "", webToken, appToken, err 95 | } 96 | wtoken, apiErr = cloudpan.LoginWithCaptcha(username, password, vcode) 97 | if apiErr != nil { 98 | return "", "", webToken, appToken, apiErr 99 | } else { 100 | return 101 | } 102 | } 103 | 104 | } else { 105 | return "", "", webToken, appToken, fmt.Errorf("登录失败") 106 | } 107 | } 108 | } 109 | 110 | webToken = *wtoken 111 | appToken = *atoken 112 | usernameStr = username 113 | passwordStr = password 114 | return 115 | } 116 | 117 | func TryLogin() *config.PanUser { 118 | // can do automatically login? 119 | for _, u := range config.Config.UserList { 120 | if u.UID == config.Config.ActiveUID { 121 | // login 122 | _, _, webToken, appToken, err := DoLoginHelper(config.DecryptString(u.LoginUserName), config.DecryptString(u.LoginUserPassword)) 123 | if err != nil { 124 | logger.Verboseln("automatically login error") 125 | break 126 | } 127 | // success 128 | u.WebToken = webToken 129 | u.AppToken = appToken 130 | 131 | // save 132 | SaveConfigFunc(nil) 133 | // reload 134 | ReloadConfigFunc(nil) 135 | return config.Config.ActiveUser() 136 | } 137 | } 138 | return nil 139 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ``` 8 | 9 | versioninfo.json - 里面有exe程序信息以及ico的配置 10 | 使用 goversioninfo 工具运行以下命令 11 | ``` 12 | goversioninfo -o=resource_windows_386.syso 13 | goversioninfo -64 -o=resource_windows_amd64.syso 14 | ``` 15 | 即可编译出.syso资源库,再使用 go build 编译之后,exe文件就会拥有应用程序信息和ico图标 -------------------------------------------------------------------------------- /entitlements.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | application-identifier 6 | com.tickstep.cloudpan189go 7 | get-task-allow 8 | 9 | platform-application 10 | 11 | keychain-access-groups 12 | 13 | com.tickstep.cloudpan189go 14 | 15 | 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tickstep/cloudpan189-go 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/GeertJohan/go.incremental v1.0.0 7 | github.com/json-iterator/go v1.1.10 8 | github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 9 | github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb 10 | github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce 11 | github.com/peterh/liner v1.1.1-0.20190305032635-6f820f8f90ce 12 | github.com/tickstep/bolt v1.3.4 13 | github.com/tickstep/cloudpan189-api v0.1.0 14 | github.com/tickstep/library-go v0.1.1 15 | github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f 16 | ) 17 | 18 | require ( 19 | github.com/boltdb/bolt v1.3.1 // indirect 20 | github.com/cpuguy83/go-md2man v1.0.10 // indirect 21 | github.com/denisbrodbeck/machineid v1.0.1 // indirect 22 | github.com/mattn/go-runewidth v0.0.9 // indirect 23 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 24 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect 25 | github.com/russross/blackfriday v1.5.2 // indirect 26 | github.com/satori/go.uuid v1.2.0 // indirect 27 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 // indirect 28 | ) 29 | 30 | //replace github.com/tickstep/bolt => /Users/tickstep/Documents/Workspace/go/projects/bolt 31 | //replace github.com/tickstep/library-go => /Users/tickstep/Documents/Workspace/go/projects/library-go 32 | //replace github.com/tickstep/cloudpan189-api => /Users/tickstep/Documents/Workspace/go/projects/cloudpan189-api 33 | -------------------------------------------------------------------------------- /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/cloudpan189-go/cmder" 19 | "github.com/tickstep/cloudpan189-go/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 | cloudpan189-go cd <目录, 绝对路径或相对路径> 30 | 31 | 示例: 32 | 33 | 切换 /我的资源 工作目录: 34 | cloudpan189-go cd /我的资源 35 | 36 | 切换上级目录: 37 | cloudpan189-go cd .. 38 | 39 | 切换根目录: 40 | cloudpan189-go cd / 41 | `, 42 | Before: cmder.ReloadConfigFunc, 43 | After: cmder.SaveConfigFunc, 44 | Action: func(c *cli.Context) error { 45 | if c.NArg() == 0 { 46 | cli.ShowCommandHelp(c, c.Command.Name) 47 | return nil 48 | } 49 | if config.Config.ActiveUser() == nil { 50 | fmt.Println("未登录账号") 51 | return nil 52 | } 53 | RunChangeDirectory(parseFamilyId(c), c.Args().Get(0)) 54 | return nil 55 | }, 56 | Flags: []cli.Flag{ 57 | cli.StringFlag{ 58 | Name: "familyId", 59 | Usage: "家庭云ID", 60 | Value: "", 61 | }, 62 | }, 63 | } 64 | } 65 | 66 | func CmdPwd() cli.Command { 67 | return cli.Command{ 68 | Name: "pwd", 69 | Usage: "输出工作目录", 70 | UsageText: cmder.App().Name + " pwd", 71 | Category: "天翼云盘", 72 | Before: cmder.ReloadConfigFunc, 73 | Action: func(c *cli.Context) error { 74 | if config.Config.ActiveUser() == nil { 75 | fmt.Println("未登录账号") 76 | return nil 77 | } 78 | if IsFamilyCloud(config.Config.ActiveUser().ActiveFamilyId) { 79 | fmt.Println(config.Config.ActiveUser().FamilyWorkdir) 80 | } else { 81 | fmt.Println(config.Config.ActiveUser().Workdir) 82 | } 83 | return nil 84 | }, 85 | } 86 | } 87 | 88 | func RunChangeDirectory(familyId int64, targetPath string) { 89 | user := config.Config.ActiveUser() 90 | targetPath = user.PathJoin(familyId, targetPath) 91 | 92 | targetPathInfo, err := user.PanClient().AppFileInfoByPath(familyId, targetPath) 93 | if err != nil { 94 | fmt.Println(err) 95 | return 96 | } 97 | 98 | if !targetPathInfo.IsFolder { 99 | fmt.Printf("错误: %s 不是一个目录 (文件夹)\n", targetPath) 100 | return 101 | } 102 | 103 | if IsFamilyCloud(familyId) { 104 | user.FamilyWorkdir = targetPath 105 | user.FamilyWorkdirFileEntity = *targetPathInfo 106 | } else { 107 | user.Workdir = targetPath 108 | user.WorkdirFileEntity = *targetPathInfo 109 | } 110 | 111 | fmt.Printf("改变工作目录: %s\n", targetPath) 112 | } 113 | -------------------------------------------------------------------------------- /internal/command/export_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 command 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "github.com/tickstep/cloudpan189-api/cloudpan" 20 | "github.com/tickstep/cloudpan189-api/cloudpan/apierror" 21 | "github.com/tickstep/cloudpan189-go/cmder" 22 | "github.com/tickstep/cloudpan189-go/internal/config" 23 | "github.com/tickstep/library-go/logger" 24 | "github.com/urfave/cli" 25 | "log" 26 | "os" 27 | "path" 28 | "strconv" 29 | "time" 30 | ) 31 | 32 | type ( 33 | ImportExportFileItem struct { 34 | FileMd5 string `json:"md5"` 35 | FileSize int64 `json:"size"` 36 | Path string `json:"path"` 37 | LastOpTime string `json:"lastOpTime"` 38 | } 39 | ) 40 | 41 | func CmdExport() cli.Command { 42 | return cli.Command{ 43 | Name: "export", 44 | Usage: "导出文件/目录元数据", 45 | UsageText: cmder.App().Name + " export <网盘文件/目录的路径1> <文件/目录2> <文件/目录3> ... <本地保存文件路径>", 46 | Description: ` 47 | 导出指定文件/目录下面的所有文件的元数据信息,并保存到指定的本地文件里面。导出的文件元信息可以使用 import 命令(秒传文件功能)导入到网盘中。 48 | 支持多个文件或目录的导出. 49 | 50 | 示例: 51 | 52 | 导出 /我的资源/1.mp4 元数据到文件 /Users/tickstep/Downloads/export_files.txt 53 | cloudpan189-go export /我的资源/1.mp4 /Users/tickstep/Downloads/export_files.txt 54 | 55 | 导出 /我的资源 整个目录 元数据到文件 /Users/tickstep/Downloads/export_files.txt 56 | cloudpan189-go export /我的资源 /Users/tickstep/Downloads/export_files.txt 57 | 58 | 导出 网盘 整个目录 元数据到文件 /Users/tickstep/Downloads/export_files.txt 59 | cloudpan189-go export / /Users/tickstep/Downloads/export_files.txt 60 | `, 61 | Category: "天翼云盘", 62 | Before: cmder.ReloadConfigFunc, 63 | Action: func(c *cli.Context) error { 64 | if c.NArg() < 2 { 65 | cli.ShowCommandHelp(c, c.Command.Name) 66 | return nil 67 | } 68 | 69 | subArgs := c.Args() 70 | RunExportFiles(parseFamilyId(c), c.Bool("ow"), subArgs[:len(subArgs)-1], subArgs[len(subArgs)-1]) 71 | return nil 72 | }, 73 | Flags: []cli.Flag{ 74 | cli.BoolFlag{ 75 | Name: "ow", 76 | Usage: "overwrite, 覆盖已存在的导出文件", 77 | }, 78 | cli.StringFlag{ 79 | Name: "familyId", 80 | Usage: "家庭云ID", 81 | Value: "", 82 | }, 83 | }, 84 | } 85 | } 86 | 87 | 88 | func RunExportFiles(familyId int64, overwrite bool, panPaths []string, saveLocalFilePath string) { 89 | activeUser := config.Config.ActiveUser() 90 | panClient := activeUser.PanClient() 91 | 92 | lfi,_ := os.Stat(saveLocalFilePath) 93 | realSaveFilePath := saveLocalFilePath 94 | if lfi != nil { 95 | if lfi.IsDir() { 96 | realSaveFilePath = path.Join(saveLocalFilePath, "export_file_") + strconv.FormatInt(time.Now().Unix(), 10) + ".txt" 97 | } else { 98 | if !overwrite { 99 | fmt.Println("导出文件已存在") 100 | return 101 | } 102 | } 103 | } else { 104 | // create file 105 | localDir := path.Dir(saveLocalFilePath) 106 | dirFs,_ := os.Stat(localDir) 107 | if dirFs != nil { 108 | if !dirFs.IsDir() { 109 | fmt.Println("指定的保存文件路径不合法") 110 | return 111 | } 112 | } else { 113 | er := os.MkdirAll(localDir, 0755) 114 | if er != nil { 115 | fmt.Println("创建文件夹出错") 116 | return 117 | } 118 | } 119 | realSaveFilePath = saveLocalFilePath 120 | } 121 | 122 | totalCount := 0 123 | saveFile, err := os.OpenFile(realSaveFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 124 | if err != nil { 125 | log.Fatal(err) 126 | return 127 | } 128 | 129 | for _,panPath := range panPaths { 130 | panPath = activeUser.PathJoin(familyId, panPath) 131 | panClient.AppFilesDirectoriesRecurseList(familyId, panPath, func(depth int, _ string, fd *cloudpan.AppFileEntity, apiError *apierror.ApiError) bool { 132 | if apiError != nil { 133 | logger.Verbosef("%s\n", apiError) 134 | return true 135 | } 136 | 137 | // 只需要存储文件即可 138 | if !fd.IsFolder { 139 | item := ImportExportFileItem{ 140 | FileMd5: fd.FileMd5, 141 | FileSize: fd.FileSize, 142 | Path: fd.Path, 143 | LastOpTime: fd.LastOpTime, 144 | } 145 | jstr,e := json.Marshal(&item) 146 | if e != nil { 147 | logger.Verboseln("to json string err") 148 | return false 149 | } 150 | saveFile.WriteString(string(jstr) + "\n") 151 | totalCount += 1 152 | time.Sleep(time.Duration(100) * time.Millisecond) 153 | fmt.Printf("\r导出文件数量: %d", totalCount) 154 | } 155 | return true 156 | }) 157 | } 158 | 159 | // close and save 160 | if err := saveFile.Close(); err != nil { 161 | log.Fatal(err) 162 | } 163 | 164 | fmt.Printf("\r导出文件总数量: %d\n", totalCount) 165 | fmt.Printf("导出文件保存路径: %s\n", realSaveFilePath) 166 | } 167 | -------------------------------------------------------------------------------- /internal/command/family_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/cloudpan189-api/cloudpan" 20 | "github.com/tickstep/cloudpan189-go/cmder" 21 | "github.com/tickstep/cloudpan189-go/cmder/cmdtable" 22 | "github.com/tickstep/cloudpan189-go/internal/config" 23 | "github.com/urfave/cli" 24 | "strconv" 25 | "strings" 26 | ) 27 | 28 | func CmdFamily() cli.Command { 29 | return cli.Command{ 30 | Name: "family", 31 | Usage: "切换云工作模式(家庭云/个人云)", 32 | Description: ` 33 | 切换已登录的天翼帐号的云工作模式(家庭云/个人云) 34 | 如果运行该条命令没有提供参数, 程序将会列出所有的家庭云, 供选择切换. 35 | 36 | 示例: 37 | cloudpan189-go family 38 | cloudpan189-go family 39 | `, 40 | Category: "天翼云盘账号", 41 | Before: cmder.ReloadConfigFunc, 42 | After: cmder.SaveConfigFunc, 43 | Action: func(c *cli.Context) error { 44 | inputData := c.Args().Get(0) 45 | targetFamilyId := int64(-1) 46 | if inputData != "" && len(inputData) > 0 { 47 | targetFamilyId, _ = strconv.ParseInt(inputData, 10, 0) 48 | } 49 | RunSwitchFamilyList(targetFamilyId) 50 | return nil 51 | }, 52 | } 53 | } 54 | 55 | func RunSwitchFamilyList(targetFamilyId int64) { 56 | currentFamilyId := config.Config.ActiveUser().ActiveFamilyId 57 | var activeFamilyInfo *cloudpan.AppFamilyInfo = nil 58 | familyList,renderStr := getFamilyOptionList() 59 | 60 | if familyList == nil || len(familyList) == 0 { 61 | fmt.Println("切换云工作模式失败") 62 | return 63 | } 64 | 65 | if targetFamilyId < 0 { 66 | // show option list 67 | fmt.Println(renderStr) 68 | 69 | // 提示输入 index 70 | var index string 71 | fmt.Printf("输入要切换的家庭云 # 值 > ") 72 | _, err := fmt.Scanln(&index) 73 | if err != nil { 74 | return 75 | } 76 | 77 | if n, err := strconv.Atoi(index); err == nil && n >= 0 && n < len(familyList) { 78 | activeFamilyInfo = familyList[n] 79 | } else { 80 | fmt.Printf("切换云工作模式失败, 请检查 # 值是否正确\n") 81 | return 82 | } 83 | } else { 84 | // 直接切换 85 | for _,familyInfo := range familyList { 86 | if familyInfo.FamilyId == targetFamilyId { 87 | activeFamilyInfo = familyInfo 88 | break 89 | } 90 | } 91 | } 92 | 93 | if activeFamilyInfo == nil { 94 | fmt.Printf("切换云工作模式失败\n") 95 | return 96 | } 97 | 98 | config.Config.ActiveUser().ActiveFamilyId = activeFamilyInfo.FamilyId 99 | config.Config.ActiveUser().ActiveFamilyInfo = *activeFamilyInfo 100 | if currentFamilyId != config.Config.ActiveUser().ActiveFamilyId { 101 | // clear the family work path 102 | config.Config.ActiveUser().FamilyWorkdir = "/" 103 | config.Config.ActiveUser().FamilyWorkdirFileEntity = *cloudpan.NewAppFileEntityForRootDir() 104 | } 105 | if activeFamilyInfo.FamilyId > 0 { 106 | fmt.Printf("切换云工作模式:家庭云 %s\n", activeFamilyInfo.RemarkName) 107 | } else { 108 | fmt.Printf("切换云工作模式:%s\n", activeFamilyInfo.RemarkName) 109 | } 110 | 111 | } 112 | 113 | func getFamilyOptionList() ([]*cloudpan.AppFamilyInfo, string) { 114 | activeUser := config.Config.ActiveUser() 115 | 116 | familyResult,err := activeUser.PanClient().AppFamilyGetFamilyList() 117 | if err != nil { 118 | fmt.Println("获取家庭列表失败") 119 | return nil, "" 120 | } 121 | t := []*cloudpan.AppFamilyInfo{} 122 | personCloud := &cloudpan.AppFamilyInfo{ 123 | FamilyId: 0, 124 | RemarkName: "个人云", 125 | CreateTime: "-", 126 | } 127 | t = append(t, personCloud) 128 | t = append(t, familyResult.FamilyInfoList...) 129 | familyList := t 130 | builder := &strings.Builder{} 131 | tb := cmdtable.NewTable(builder) 132 | tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER}) 133 | tb.SetHeader([]string{"#", "family_id", "家庭云名", "创建日期"}) 134 | 135 | for k, familyInfo := range familyList { 136 | tb.Append([]string{strconv.Itoa(k), strconv.FormatInt(familyInfo.FamilyId, 10), familyInfo.RemarkName, familyInfo.CreateTime}) 137 | } 138 | tb.Render() 139 | return familyList, builder.String() 140 | } 141 | -------------------------------------------------------------------------------- /internal/command/login.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/cloudpan189-api/cloudpan" 19 | "github.com/tickstep/cloudpan189-go/cmder" 20 | "github.com/tickstep/cloudpan189-go/internal/config" 21 | _ "github.com/tickstep/library-go/requester" 22 | "github.com/urfave/cli" 23 | ) 24 | 25 | 26 | func CmdLogin() cli.Command { 27 | return cli.Command{ 28 | Name: "login", 29 | Usage: "登录天翼云盘账号", 30 | Description: ` 31 | 示例: 32 | cloudpan189-go login 33 | cloudpan189-go login -username=tickstep -password=123xxx 34 | 35 | 常规登录: 36 | 按提示一步一步来即可. 37 | `, 38 | Category: "天翼云盘账号", 39 | Before: cmder.ReloadConfigFunc, // 每次进行登录动作的时候需要调用刷新配置 40 | After: cmder.SaveConfigFunc, // 登录完成需要调用保存配置 41 | Action: func(c *cli.Context) error { 42 | appToken := cloudpan.AppLoginToken{} 43 | webToken := cloudpan.WebLoginToken{} 44 | username := "" 45 | passowrd := "" 46 | if c.IsSet("COOKIE_LOGIN_USER") { 47 | webToken.CookieLoginUser = c.String("COOKIE_LOGIN_USER") 48 | } else if c.NArg() == 0 { 49 | var err error 50 | username, passowrd, webToken, appToken, err = RunLogin(c.String("username"), c.String("password")) 51 | if err != nil { 52 | fmt.Println(err) 53 | return err 54 | } 55 | } else { 56 | cli.ShowCommandHelp(c, c.Command.Name) 57 | return nil 58 | } 59 | cloudUser, _ := config.SetupUserByCookie(&webToken, &appToken) 60 | // save username / password 61 | cloudUser.LoginUserName = config.EncryptString(username) 62 | cloudUser.LoginUserPassword = config.EncryptString(passowrd) 63 | config.Config.SetActiveUser(cloudUser) 64 | fmt.Println("天翼帐号登录成功: ", cloudUser.Nickname) 65 | return nil 66 | }, 67 | // 命令的附加options参数说明,使用 help login 命令即可查看 68 | Flags: []cli.Flag{ 69 | cli.StringFlag{ 70 | Name: "username", 71 | Usage: "登录天翼帐号的用户名(手机号/邮箱/别名)", 72 | }, 73 | cli.StringFlag{ 74 | Name: "password", 75 | Usage: "登录天翼帐号的用户密码", 76 | }, 77 | // 暂不支持 78 | // cloudpan189-go login -COOKIE_LOGIN_USER=8B12CBBCE89CA8DFC3445985B63B511B5E7EC7... 79 | //cli.StringFlag{ 80 | // Name: "COOKIE_LOGIN_USER", 81 | // Usage: "使用 COOKIE_LOGIN_USER cookie来登录帐号", 82 | //}, 83 | }, 84 | } 85 | } 86 | 87 | func CmdLogout() cli.Command { 88 | return cli.Command{ 89 | Name: "logout", 90 | Usage: "退出天翼帐号", 91 | Description: "退出当前登录的帐号", 92 | Category: "天翼云盘账号", 93 | Before: cmder.ReloadConfigFunc, 94 | After: cmder.SaveConfigFunc, 95 | Action: func(c *cli.Context) error { 96 | if config.Config.NumLogins() == 0 { 97 | fmt.Println("未设置任何帐号, 不能退出") 98 | return nil 99 | } 100 | 101 | var ( 102 | confirm string 103 | activeUser = config.Config.ActiveUser() 104 | ) 105 | 106 | if !c.Bool("y") { 107 | fmt.Printf("确认退出当前帐号: %s ? (y/n) > ", activeUser.Nickname) 108 | _, err := fmt.Scanln(&confirm) 109 | if err != nil || (confirm != "y" && confirm != "Y") { 110 | return err 111 | } 112 | } 113 | 114 | deletedUser, err := config.Config.DeleteUser(activeUser.UID) 115 | if err != nil { 116 | fmt.Printf("退出用户 %s, 失败, 错误: %s\n", activeUser.Nickname, err) 117 | } 118 | 119 | fmt.Printf("退出用户成功: %s\n", deletedUser.Nickname) 120 | return nil 121 | }, 122 | Flags: []cli.Flag{ 123 | cli.BoolFlag{ 124 | Name: "y", 125 | Usage: "确认退出帐号", 126 | }, 127 | }, 128 | } 129 | } 130 | 131 | func RunLogin(username, password string) (usernameStr, passwordStr string, webToken cloudpan.WebLoginToken, appToken cloudpan.AppLoginToken, error error) { 132 | return cmder.DoLoginHelper(username, password) 133 | } 134 | -------------------------------------------------------------------------------- /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/cloudpan189-api/cloudpan" 19 | "github.com/tickstep/cloudpan189-api/cloudpan/apierror" 20 | "github.com/tickstep/cloudpan189-go/cmder" 21 | "github.com/tickstep/cloudpan189-go/internal/config" 22 | "github.com/urfave/cli" 23 | "path" 24 | "strings" 25 | ) 26 | 27 | func CmdMkdir() cli.Command { 28 | return cli.Command{ 29 | Name: "mkdir", 30 | Usage: "创建目录", 31 | UsageText: cmder.App().Name + " mkdir <目录>", 32 | Category: "天翼云盘", 33 | Before: cmder.ReloadConfigFunc, 34 | Action: func(c *cli.Context) error { 35 | if c.NArg() == 0 { 36 | cli.ShowCommandHelp(c, c.Command.Name) 37 | return nil 38 | } 39 | if config.Config.ActiveUser() == nil { 40 | fmt.Println("未登录账号") 41 | return nil 42 | } 43 | RunMkdir(parseFamilyId(c), c.Args().Get(0)) 44 | return nil 45 | }, 46 | Flags: []cli.Flag{ 47 | cli.StringFlag{ 48 | Name: "familyId", 49 | Usage: "家庭云ID", 50 | Value: "", 51 | }, 52 | }, 53 | } 54 | } 55 | 56 | func RunMkdir(familyId int64, name string) { 57 | activeUser := GetActiveUser() 58 | fullpath := activeUser.PathJoin(familyId, name) 59 | pathSlice := strings.Split(fullpath, "/") 60 | rs := &cloudpan.AppMkdirResult{} 61 | err := apierror.NewFailedApiError("") 62 | 63 | var cWorkDir = activeUser.Workdir 64 | var cFileId = activeUser.WorkdirFileEntity.FileId 65 | if IsFamilyCloud(familyId) { 66 | cWorkDir = activeUser.FamilyWorkdir 67 | cFileId = activeUser.FamilyWorkdirFileEntity.FileId 68 | } 69 | if path.Dir(fullpath) == cWorkDir { 70 | rs, err = activeUser.PanClient().AppMkdirRecursive(familyId, cFileId, path.Clean(path.Dir(fullpath)), len(pathSlice) - 1, pathSlice) 71 | } else { 72 | rs, err = activeUser.PanClient().AppMkdirRecursive(familyId,"", "", 0, pathSlice) 73 | } 74 | 75 | if err != nil { 76 | fmt.Println("创建文件夹失败:" + err.Error()) 77 | return 78 | } 79 | 80 | if rs.FileId != "" { 81 | fmt.Println("创建文件夹成功: ", fullpath) 82 | } else { 83 | fmt.Println("创建文件夹失败: ", fullpath) 84 | } 85 | } -------------------------------------------------------------------------------- /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/cloudpan189-go/cmder" 19 | "github.com/tickstep/cloudpan189-go/internal/config" 20 | "github.com/tickstep/library-go/converter" 21 | "github.com/urfave/cli" 22 | ) 23 | 24 | type QuotaInfo struct { 25 | // 已使用个人空间大小 26 | UsedSize int64 27 | // 个人空间总大小 28 | Quota int64 29 | } 30 | 31 | func CmdQuota() cli.Command { 32 | return cli.Command{ 33 | Name: "quota", 34 | Usage: "获取当前帐号空间配额", 35 | Description: "获取网盘的总储存空间, 和已使用的储存空间", 36 | Category: "天翼云盘账号", 37 | Before: cmder.ReloadConfigFunc, 38 | Action: func(c *cli.Context) error { 39 | if config.Config.ActiveUser() == nil { 40 | fmt.Println("未登录账号") 41 | return nil 42 | } 43 | q, err := RunGetQuotaInfo() 44 | if err == nil { 45 | fmt.Printf("账号: %s, uid: %d, 个人空间总额: %s, 个人空间已使用: %s, 比率: %f%%\n", 46 | config.Config.ActiveUser().Nickname, config.Config.ActiveUser().UID, 47 | converter.ConvertFileSize(q.Quota, 2), converter.ConvertFileSize(q.UsedSize, 2), 48 | 100*float64(q.UsedSize)/float64(q.Quota)) 49 | } 50 | return nil 51 | }, 52 | } 53 | } 54 | 55 | func RunGetQuotaInfo() (quotaInfo *QuotaInfo, error error) { 56 | user, err := GetActivePanClient().GetUserInfo() 57 | if err != nil { 58 | return nil, err 59 | } 60 | return &QuotaInfo{ 61 | UsedSize: int64(user.UsedSize), 62 | Quota: int64(user.Quota), 63 | }, nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/command/rename.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/cloudpan189-api/cloudpan" 19 | "github.com/tickstep/cloudpan189-api/cloudpan/apierror" 20 | "github.com/tickstep/cloudpan189-api/cloudpan/apiutil" 21 | "github.com/tickstep/cloudpan189-go/cmder" 22 | "github.com/tickstep/cloudpan189-go/internal/config" 23 | "github.com/urfave/cli" 24 | "path" 25 | "strings" 26 | ) 27 | 28 | func CmdRename() cli.Command { 29 | return cli.Command{ 30 | Name: "rename", 31 | Usage: "重命名文件", 32 | UsageText: `重命名文件: 33 | cloudpan189-go rename <旧文件/目录名> <新文件/目录名>`, 34 | Description: ` 35 | 示例: 36 | 37 | 将文件 1.mp4 重命名为 2.mp4 38 | cloudpan189-go rename 1.mp4 2.mp4 39 | 40 | 将文件 /test/1.mp4 重命名为 /test/2.mp4 41 | 要求必须是同一个文件目录内 42 | cloudpan189-go rename /test/1.mp4 /test/2.mp4 43 | `, 44 | Category: "天翼云盘", 45 | Before: cmder.ReloadConfigFunc, 46 | Action: func(c *cli.Context) error { 47 | if c.NArg() != 2 { 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 | RunRename(parseFamilyId(c), c.Args().Get(0), c.Args().Get(1)) 56 | return nil 57 | }, 58 | Flags: []cli.Flag{ 59 | cli.StringFlag{ 60 | Name: "familyId", 61 | Usage: "家庭云ID", 62 | Value: "", 63 | }, 64 | }, 65 | } 66 | } 67 | 68 | func RunRename(familyId int64, oldName string, newName string) { 69 | if oldName == "" { 70 | fmt.Println("请指定命名文件") 71 | return 72 | } 73 | if newName == "" { 74 | fmt.Println("请指定文件新名称") 75 | return 76 | } 77 | activeUser := GetActiveUser() 78 | oldName = activeUser.PathJoin(familyId, strings.TrimSpace(oldName)) 79 | newName = activeUser.PathJoin(familyId, strings.TrimSpace(newName)) 80 | if path.Dir(oldName) != path.Dir(newName) { 81 | fmt.Println("只能命名同一个目录的文件") 82 | return 83 | } 84 | if !apiutil.CheckFileNameValid(path.Base(newName)) { 85 | fmt.Println("文件名不能包含特殊字符:" + apiutil.FileNameSpecialChars) 86 | return 87 | } 88 | 89 | fileId := "" 90 | r, err := GetActivePanClient().AppFileInfoByPath(familyId, activeUser.PathJoin(familyId, oldName)) 91 | if err != nil { 92 | fmt.Printf("原文件不存在: %s, %s\n", oldName, err) 93 | return 94 | } 95 | fileId = r.FileId 96 | 97 | var b *cloudpan.AppFileEntity 98 | var e *apierror.ApiError 99 | if IsFamilyCloud(familyId) { 100 | b, e = activeUser.PanClient().AppFamilyRenameFile(familyId, fileId, path.Base(newName)) 101 | } else { 102 | b, e = activeUser.PanClient().AppRenameFile(fileId, path.Base(newName)) 103 | } 104 | if e != nil { 105 | fmt.Println(e.Err) 106 | return 107 | } 108 | if b == nil { 109 | fmt.Println("重命名文件失败") 110 | return 111 | } 112 | fmt.Printf("重命名文件成功:%s -> %s\n", path.Base(oldName), path.Base(newName)) 113 | } 114 | -------------------------------------------------------------------------------- /internal/command/rm.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/cloudpan189-api/cloudpan" 19 | "github.com/tickstep/cloudpan189-go/cmder" 20 | "github.com/tickstep/cloudpan189-go/cmder/cmdtable" 21 | "github.com/tickstep/cloudpan189-go/internal/config" 22 | "github.com/tickstep/library-go/logger" 23 | "github.com/urfave/cli" 24 | "os" 25 | "path" 26 | "strconv" 27 | "time" 28 | ) 29 | 30 | func CmdRm() cli.Command { 31 | return cli.Command{ 32 | Name: "rm", 33 | Usage: "删除文件/目录", 34 | UsageText: cmder.App().Name + " rm <文件/目录的路径1> <文件/目录2> <文件/目录3> ...", 35 | Description: ` 36 | 注意: 删除多个文件和目录时, 请确保每一个文件和目录都存在, 否则删除操作会失败. 37 | 被删除的文件或目录可在网盘文件回收站找回. 38 | 39 | 示例: 40 | 41 | 删除 /我的资源/1.mp4 42 | cloudpan189-go rm /我的资源/1.mp4 43 | 44 | 删除 /我的资源/1.mp4 和 /我的资源/2.mp4 45 | cloudpan189-go rm /我的资源/1.mp4 /我的资源/2.mp4 46 | 47 | 删除 /我的资源 整个目录 !! 48 | cloudpan189-go rm /我的资源 49 | `, 50 | Category: "天翼云盘", 51 | Before: cmder.ReloadConfigFunc, 52 | Action: func(c *cli.Context) error { 53 | if c.NArg() == 0 { 54 | cli.ShowCommandHelp(c, c.Command.Name) 55 | return nil 56 | } 57 | if config.Config.ActiveUser() == nil { 58 | fmt.Println("未登录账号") 59 | return nil 60 | } 61 | RunRemove(parseFamilyId(c), c.Args()...) 62 | return nil 63 | }, 64 | Flags: []cli.Flag{ 65 | cli.StringFlag{ 66 | Name: "familyId", 67 | Usage: "家庭云ID", 68 | Value: "", 69 | }, 70 | }, 71 | } 72 | } 73 | 74 | // RunRemove 执行 批量删除文件/目录 75 | func RunRemove(familyId int64, paths ...string) { 76 | if IsFamilyCloud(familyId) { 77 | delFamilyCloudFiles(familyId, paths...) 78 | } else { 79 | delPersonCloudFiles(familyId, paths...) 80 | } 81 | } 82 | 83 | func delFamilyCloudFiles(familyId int64, paths ...string) { 84 | activeUser := GetActiveUser() 85 | infoList, _, delFileInfos := getBatchTaskInfoList(familyId, paths...) 86 | if infoList == nil || len(*infoList) == 0 { 87 | fmt.Println("没有有效的文件可删除") 88 | return 89 | } 90 | 91 | // create delete files task 92 | delParam := &cloudpan.BatchTaskParam{ 93 | TypeFlag: cloudpan.BatchTaskTypeDelete, 94 | TaskInfos: *infoList, 95 | } 96 | 97 | taskId, err := activeUser.PanClient().AppCreateBatchTask(familyId, delParam) 98 | if err != nil { 99 | fmt.Println("无法删除文件,请稍后重试") 100 | return 101 | } 102 | logger.Verboseln("delete file task id: " + taskId) 103 | 104 | // check task 105 | time.Sleep(time.Duration(200) * time.Millisecond) 106 | taskRes, err := activeUser.PanClient().AppCheckBatchTask(cloudpan.BatchTaskTypeDelete, taskId) 107 | if err != nil || taskRes.TaskStatus != cloudpan.BatchTaskStatusOk { 108 | fmt.Println("无法删除文件,请稍后重试") 109 | return 110 | } 111 | 112 | pnt := func() { 113 | tb := cmdtable.NewTable(os.Stdout) 114 | tb.SetHeader([]string{"#", "文件/目录"}) 115 | for k := range *delFileInfos { 116 | tb.Append([]string{strconv.Itoa(k), (*delFileInfos)[k].Path}) 117 | } 118 | tb.Render() 119 | } 120 | if taskRes.TaskStatus == cloudpan.BatchTaskStatusOk { 121 | fmt.Println("操作成功, 以下文件/目录已删除, 可在云盘文件回收站找回: ") 122 | pnt() 123 | } 124 | } 125 | 126 | func delPersonCloudFiles(familyId int64, paths ...string) { 127 | activeUser := GetActiveUser() 128 | infoList, _, delFileInfos := getBatchTaskInfoList(familyId, paths...) 129 | if infoList == nil || len(*infoList) == 0 { 130 | fmt.Println("没有有效的文件可删除") 131 | return 132 | } 133 | 134 | // create delete files task 135 | delParam := &cloudpan.BatchTaskParam{ 136 | TypeFlag: cloudpan.BatchTaskTypeDelete, 137 | TaskInfos: *infoList, 138 | } 139 | 140 | taskId, err := activeUser.PanClient().CreateBatchTask(delParam) 141 | if err != nil { 142 | fmt.Println("无法删除文件,请稍后重试") 143 | return 144 | } 145 | logger.Verboseln("delete file task id: " + taskId) 146 | 147 | // check task 148 | checkTime := 5 149 | var taskRes *cloudpan.CheckTaskResult 150 | for checkTime >= 0 { 151 | checkTime-- 152 | time.Sleep(time.Duration(1000) * time.Millisecond) 153 | taskRes, err = activeUser.PanClient().CheckBatchTask(cloudpan.BatchTaskTypeDelete, taskId) 154 | if err == nil { 155 | if taskRes.TaskStatus == cloudpan.BatchTaskStatusOk { 156 | // success 157 | break 158 | } 159 | } 160 | } 161 | if taskRes == nil || taskRes.TaskStatus != cloudpan.BatchTaskStatusOk { 162 | fmt.Println("无法删除文件,请稍后重试") 163 | return 164 | } 165 | 166 | pnt := func() { 167 | tb := cmdtable.NewTable(os.Stdout) 168 | tb.SetHeader([]string{"#", "文件/目录"}) 169 | for k := range *delFileInfos { 170 | tb.Append([]string{strconv.Itoa(k), (*delFileInfos)[k].Path}) 171 | } 172 | tb.Render() 173 | } 174 | if taskRes.TaskStatus == cloudpan.BatchTaskStatusOk { 175 | fmt.Println("操作成功, 以下文件/目录已删除, 可在云盘文件回收站找回: ") 176 | pnt() 177 | } 178 | } 179 | 180 | func getBatchTaskInfoList(familyId int64, paths ...string) (*cloudpan.BatchTaskInfoList, *[]string, *[]*cloudpan.AppFileEntity) { 181 | activeUser := GetActiveUser() 182 | failedRmPaths := make([]string, 0, len(paths)) 183 | delFileInfos := make([]*cloudpan.AppFileEntity, 0, len(paths)) 184 | infoList := cloudpan.BatchTaskInfoList{} 185 | for _, p := range paths { 186 | absolutePath := path.Clean(activeUser.PathJoin(familyId, p)) 187 | fe, err := activeUser.PanClient().AppFileInfoByPath(familyId, absolutePath) 188 | if err != nil { 189 | failedRmPaths = append(failedRmPaths, absolutePath) 190 | continue 191 | } 192 | isFolder := 0 193 | if fe.IsFolder { 194 | isFolder = 1 195 | } 196 | infoItem := &cloudpan.BatchTaskInfo{ 197 | FileId: fe.FileId, 198 | FileName: fe.FileName, 199 | IsFolder: isFolder, 200 | SrcParentId: fe.ParentId, 201 | } 202 | infoList = append(infoList, infoItem) 203 | delFileInfos = append(delFileInfos, fe) 204 | } 205 | return &infoList, &failedRmPaths, &delFileInfos 206 | } 207 | -------------------------------------------------------------------------------- /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/cloudpan189-api/cloudpan" 19 | "github.com/tickstep/cloudpan189-go/cmder" 20 | "github.com/tickstep/cloudpan189-go/internal/config" 21 | "github.com/urfave/cli" 22 | "strconv" 23 | ) 24 | 25 | func CmdLoglist() cli.Command { 26 | return cli.Command{ 27 | Name: "loglist", 28 | Usage: "列出帐号列表", 29 | Description: "列出所有已登录的天翼帐号", 30 | Category: "天翼云盘账号", 31 | Before: cmder.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 | cloudpan189-go su 49 | cloudpan189-go su 50 | `, 51 | Category: "天翼云盘账号", 52 | Before: cmder.ReloadConfigFunc, 53 | After: cmder.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 uint64 70 | ) 71 | 72 | if c.NArg() == 1 { 73 | // 直接切换 74 | uid, _ = strconv.ParseUint(inputData, 10, 64) 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, err := strconv.Atoi(index); err == nil && n >= 0 && n < numLogins { 88 | uid = config.Config.UserList[n].UID 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, inputData) 98 | if err != nil { 99 | fmt.Printf("切换用户失败, %s\n", err) 100 | return nil 101 | } 102 | 103 | if switchedUser == nil { 104 | switchedUser = cmder.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: cmder.ReloadConfigFunc, 125 | Action: func(c *cli.Context) error { 126 | if config.Config.ActiveUser() == nil { 127 | fmt.Println("未登录账号") 128 | return nil 129 | } 130 | activeUser := config.Config.ActiveUser() 131 | gender := "未知" 132 | if activeUser.Sex == "F" { 133 | gender = "女" 134 | } else if activeUser.Sex == "M" { 135 | gender = "男" 136 | } 137 | cloudName := "个人云" 138 | if config.Config.ActiveUser().ActiveFamilyId > 0 { 139 | cloudName = "家庭云(" + config.Config.ActiveUser().ActiveFamilyInfo.RemarkName + ")" 140 | } 141 | fmt.Printf("当前帐号 uid: %d, 昵称: %s, 用户名: %s, 性别: %s, 云:%s\n", activeUser.UID, activeUser.Nickname, activeUser.AccountName, gender, cloudName) 142 | return nil 143 | }, 144 | } 145 | } 146 | 147 | func RunGetUserInfo() (userInfo *cloudpan.UserInfo, error error) { 148 | return GetActivePanClient().GetUserInfo() 149 | } 150 | -------------------------------------------------------------------------------- /internal/command/user_sign.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/cloudpan189-api/cloudpan" 19 | "github.com/tickstep/cloudpan189-go/cmder" 20 | "github.com/tickstep/cloudpan189-go/internal/config" 21 | "github.com/urfave/cli" 22 | ) 23 | 24 | func CmdSign() cli.Command { 25 | return cli.Command{ 26 | Name: "sign", 27 | Usage: "用户签到", 28 | Description: "当前帐号进行签到", 29 | Category: "天翼云盘账号", 30 | Before: cmder.ReloadConfigFunc, 31 | Action: func(c *cli.Context) error { 32 | if config.Config.ActiveUser() == nil { 33 | fmt.Println("未登录账号") 34 | return nil 35 | } 36 | RunUserSign() 37 | return nil 38 | }, 39 | } 40 | } 41 | 42 | func RunUserSign() { 43 | activeUser := GetActiveUser() 44 | result, err := activeUser.PanClient().AppUserSign() 45 | if err != nil { 46 | fmt.Printf("签到失败: %s\n", err) 47 | return 48 | } 49 | if result.Status == cloudpan.AppUserSignStatusSuccess { 50 | fmt.Printf("签到成功,%s\n", result.Tip) 51 | } else if result.Status == cloudpan.AppUserSignStatusHasSign { 52 | fmt.Printf("今日已签到,%s\n", result.Tip) 53 | } else { 54 | fmt.Printf("签到失败,%s\n", result.Tip) 55 | } 56 | 57 | // 抽奖 58 | r, err := activeUser.PanClient().UserDrawPrize(cloudpan.ActivitySignin) 59 | if err != nil { 60 | fmt.Printf("第1次抽奖失败: %s\n", err) 61 | } else { 62 | if r.Success { 63 | fmt.Printf("第1次抽奖成功: %s\n", r.Tip) 64 | } else { 65 | fmt.Printf("第1次抽奖失败: %s\n", err) 66 | return 67 | } 68 | } 69 | 70 | r, err = activeUser.PanClient().UserDrawPrize(cloudpan.ActivitySignPhotos) 71 | if err != nil { 72 | fmt.Printf("第2次抽奖失败: %s\n", err) 73 | } else { 74 | if r.Success { 75 | fmt.Printf("第2次抽奖成功: %s\n", r.Tip) 76 | } else { 77 | fmt.Printf("第2次抽奖失败: %s\n", err) 78 | return 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal/command/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 command 15 | 16 | import ( 17 | "fmt" 18 | "github.com/tickstep/cloudpan189-api/cloudpan" 19 | "github.com/tickstep/cloudpan189-go/internal/config" 20 | "github.com/tickstep/library-go/logger" 21 | "path" 22 | ) 23 | 24 | var ( 25 | panCommandVerbose = logger.New("PANCOMMAND", config.EnvVerbose) 26 | ) 27 | 28 | // GetAppFileInfoByPaths 获取指定文件路径的文件详情信息 29 | func GetAppFileInfoByPaths(familyId int64, paths ...string) (fileInfoList []*cloudpan.AppFileEntity, failedPaths []string, error error) { 30 | if len(paths) <= 0 { 31 | return nil, nil, fmt.Errorf("请指定文件路径") 32 | } 33 | activeUser := GetActiveUser() 34 | 35 | for idx := 0; idx < len(paths); idx++ { 36 | absolutePath := path.Clean(activeUser.PathJoin(familyId, paths[idx])) 37 | fe, err := activeUser.PanClient().AppFileInfoByPath(familyId, absolutePath) 38 | if err != nil { 39 | failedPaths = append(failedPaths, absolutePath) 40 | continue 41 | } 42 | fileInfoList = append(fileInfoList, fe) 43 | } 44 | return 45 | } 46 | 47 | // matchPathByShellPattern 通配符匹配路径,允许返回多个匹配结果 48 | func matchPathByShellPattern(familyId int64, patterns ...string) (files []*cloudpan.AppFileEntity, e error) { 49 | acUser := GetActiveUser() 50 | for k := range patterns { 51 | ps, err := acUser.PanClient().MatchPathByShellPattern(familyId, acUser.PathJoin(familyId, patterns[k])) 52 | if err != nil { 53 | return nil, err 54 | } 55 | files = append(files, *ps...) 56 | } 57 | return files, nil 58 | } 59 | 60 | func makePathAbsolute(familyId int64, patterns ...string) (panpaths []string, err error) { 61 | acUser := GetActiveUser() 62 | for k := range patterns { 63 | ps := acUser.PathJoin(familyId, patterns[k]) 64 | panpaths = append(panpaths, ps) 65 | } 66 | return panpaths, nil 67 | } 68 | 69 | func IsFamilyCloud(familyId int64) bool { 70 | return familyId > 0 71 | } 72 | 73 | func GetFamilyCloudMark(familyId int64) string { 74 | if familyId > 0 { 75 | return "家庭云" 76 | } 77 | return "个人云" 78 | } 79 | -------------------------------------------------------------------------------- /internal/command/xcp.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/cloudpan189-api/cloudpan" 19 | "github.com/tickstep/cloudpan189-api/cloudpan/apierror" 20 | "github.com/tickstep/cloudpan189-go/cmder" 21 | "github.com/tickstep/cloudpan189-go/internal/config" 22 | "github.com/urfave/cli" 23 | ) 24 | 25 | type ( 26 | FileSourceType string 27 | ) 28 | 29 | const ( 30 | // 个人云文件 31 | PersonCloud FileSourceType = "person" 32 | 33 | // 家庭云文件 34 | FamilyCloud FileSourceType = "family" 35 | ) 36 | 37 | func CmdXcp() cli.Command { 38 | return cli.Command{ 39 | Name: "xcp", 40 | Usage: "转存拷贝文件/目录,个人云和家庭云之间转存文件", 41 | UsageText: cmder.App().Name + ` xcp <文件/目录> 42 | cloudpan189-go xcp <文件/目录1> <文件/目录2> <文件/目录3>`, 43 | Description: ` 44 | 注意: 拷贝多个文件和目录时, 请确保每一个文件和目录都存在, 否则拷贝操作会失败. 同样需要保证目标云不存在对应的文件,否则也会操作失败。 45 | 46 | 示例: 47 | 48 | 当前程序工作在个人云模式下,将 /个人云目录/1.mp4 转存复制到 家庭云根目录中 49 | cloudpan189-go xcp /个人云目录/1.mp4 50 | 51 | 当前程序工作在家庭云模式下,将 /家庭云目录/1.mp4 和 /家庭云目录/2.mp4 转存复制到 个人云 /来自家庭共享 目录中 52 | cloudpan189-go xcp /家庭云目录/1.mp4 /家庭云目录/2.mp4 53 | `, 54 | Category: "天翼云盘", 55 | Before: cmder.ReloadConfigFunc, 56 | Action: func(c *cli.Context) error { 57 | if c.NArg() <= 0 { 58 | cli.ShowCommandHelp(c, c.Command.Name) 59 | return nil 60 | } 61 | if config.Config.ActiveUser() == nil { 62 | fmt.Println("未登录账号") 63 | return nil 64 | } 65 | familyId := parseFamilyId(c) 66 | fileSource := PersonCloud 67 | if c.IsSet("source") { 68 | sourceStr := c.String("source") 69 | if sourceStr == "person" { 70 | fileSource = PersonCloud 71 | } else if sourceStr == "family" { 72 | fileSource = FamilyCloud 73 | } else { 74 | fmt.Println("不支持的参数") 75 | return nil 76 | } 77 | } else { 78 | if IsFamilyCloud(config.Config.ActiveUser().ActiveFamilyId) { 79 | fileSource = FamilyCloud 80 | } else { 81 | fileSource = PersonCloud 82 | } 83 | } 84 | RunXCopy(fileSource, familyId, c.Args()...) 85 | return nil 86 | }, 87 | Flags: []cli.Flag{ 88 | cli.StringFlag{ 89 | Name: "familyId", 90 | Usage: "家庭云ID", 91 | Value: "", 92 | Required: false, 93 | }, 94 | cli.StringFlag{ 95 | Name: "source", 96 | Usage: "文件源,person-个人云,family-家庭云", 97 | Value: "", 98 | Required: false, 99 | }, 100 | }, 101 | } 102 | } 103 | 104 | // RunXCopy 执行移动文件/目录 105 | func RunXCopy(source FileSourceType, familyId int64, paths ...string) { 106 | activeUser := GetActiveUser() 107 | 108 | // use the first family as default 109 | if familyId == 0 { 110 | familyResult,err := activeUser.PanClient().AppFamilyGetFamilyList() 111 | if err != nil { 112 | fmt.Println("获取家庭列表失败") 113 | return 114 | } 115 | for _,f := range familyResult.FamilyInfoList { 116 | if f.UserRole == 1 { 117 | familyId = f.FamilyId 118 | } 119 | } 120 | } 121 | 122 | var opFileList []*cloudpan.AppFileEntity 123 | var failedPaths []string 124 | var err error 125 | switch source { 126 | case FamilyCloud: 127 | opFileList, failedPaths, err = GetAppFileInfoByPaths(familyId, paths...) 128 | break 129 | case PersonCloud: 130 | opFileList, failedPaths, err = GetAppFileInfoByPaths(0, paths...) 131 | break 132 | default: 133 | fmt.Println("不支持的云类型") 134 | return 135 | } 136 | 137 | if err != nil { 138 | fmt.Println(err) 139 | return 140 | } 141 | if opFileList == nil || len(opFileList) == 0 { 142 | fmt.Println("没有有效的文件可复制") 143 | return 144 | } 145 | 146 | fileIdList := []string{} 147 | for _,fi := range opFileList { 148 | fileIdList = append(fileIdList, fi.FileId) 149 | } 150 | 151 | switch source { 152 | case FamilyCloud: 153 | // copy to person cloud 154 | _,e1 := activeUser.PanClient().AppFamilySaveFileToPersonCloud(familyId, fileIdList) 155 | if e1 != nil { 156 | if e1.ErrCode() == apierror.ApiCodeFileAlreadyExisted { 157 | fmt.Println("复制失败,个人云已经存在对应的文件") 158 | } else { 159 | fmt.Println("复制文件到个人云失败") 160 | } 161 | return 162 | } 163 | break 164 | case PersonCloud: 165 | // copy to family cloud 166 | _,e1 := activeUser.PanClient().AppSaveFileToFamilyCloud(familyId, fileIdList) 167 | if e1 != nil { 168 | if e1.ErrCode() == apierror.ApiCodeFileAlreadyExisted { 169 | fmt.Println("复制失败,家庭云已经存在对应的文件") 170 | } else { 171 | fmt.Println("复制文件到家庭云失败") 172 | } 173 | return 174 | } 175 | break 176 | default: 177 | fmt.Println("不支持的云类型") 178 | return 179 | } 180 | 181 | if len(failedPaths) > 0 { 182 | fmt.Println("以下文件复制失败:") 183 | for _,f := range failedPaths { 184 | fmt.Println(f) 185 | } 186 | fmt.Println("") 187 | } 188 | 189 | switch source { 190 | case FamilyCloud: 191 | // copy to person cloud 192 | fmt.Println("成功复制以下文件到个人云目录 /来自家庭共享") 193 | for _,fi := range opFileList { 194 | fmt.Println(fi.Path) 195 | } 196 | break 197 | case PersonCloud: 198 | // copy to family cloud 199 | fmt.Println("成功复制以下文件到家庭云根目录") 200 | for _,fi := range opFileList { 201 | fmt.Println(fi.Path) 202 | } 203 | break 204 | default: 205 | fmt.Println("不支持的云类型") 206 | return 207 | } 208 | } -------------------------------------------------------------------------------- /internal/config/cache.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/tickstep/cloudpan189-api/cloudpan" 5 | "github.com/tickstep/cloudpan189-api/cloudpan/apierror" 6 | "github.com/tickstep/library-go/expires" 7 | "path" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | // DeleteCache 删除含有 dirs 的缓存 13 | func (pu *PanUser) DeleteCache(dirs []string) { 14 | cache := pu.cacheOpMap.LazyInitCachePoolOp(strconv.FormatInt(pu.ActiveFamilyId, 10)) 15 | for _, v := range dirs { 16 | key := v + "_" + "OrderByName" 17 | _, ok := cache.Load(key) 18 | if ok { 19 | cache.Delete(key) 20 | } 21 | } 22 | } 23 | 24 | // DeleteOneCache 删除缓存 25 | func (pu *PanUser) DeleteOneCache(dirPath string) { 26 | ps := []string{dirPath} 27 | pu.DeleteCache(ps) 28 | } 29 | 30 | // CacheFilesDirectoriesList 缓存获取 31 | func (pu *PanUser) CacheFilesDirectoriesList(pathStr string) (fdl *cloudpan.AppFileList, apiError *apierror.ApiError) { 32 | data := pu.cacheOpMap.CacheOperation(strconv.FormatInt(pu.ActiveFamilyId, 10), pathStr+"_OrderByName", func() expires.DataExpires { 33 | var fi *cloudpan.AppFileEntity 34 | fi, apiError = pu.panClient.AppFileInfoByPath(pu.ActiveFamilyId, pathStr) 35 | if apiError != nil { 36 | return nil 37 | } 38 | fileListParam := cloudpan.NewAppFileListParam() 39 | fileListParam.FileId = fi.FileId 40 | fileListParam.FamilyId = pu.ActiveFamilyId 41 | r, apiError := pu.panClient.AppGetAllFileList(fileListParam) 42 | if apiError != nil { 43 | return nil 44 | } 45 | // construct full path 46 | for _, f := range r.FileList { 47 | f.Path = path.Join(pathStr, f.FileName) 48 | } 49 | return expires.NewDataExpires(&r.FileList, 10*time.Minute) 50 | }) 51 | if apiError != nil { 52 | return 53 | } 54 | return data.Data().(*cloudpan.AppFileList), 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_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/cloudpan189-go/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 | requester.SetLocalTCPAddrList(strings.Split(localAddrs, ",")...) 37 | } 38 | 39 | func (c *PanConfig) SetPreferIPType(ipType string) { 40 | c.PreferIPType = ipType 41 | t := requester.IPAny 42 | if strings.ToLower(ipType) == "ipv4" { 43 | t = requester.IPv4 44 | } else if strings.ToLower(ipType) == "ipv6" { 45 | t = requester.IPv6 46 | } 47 | requester.SetPreferIPType(t) 48 | } 49 | 50 | // SetCacheSizeByStr 设置cache_size 51 | func (c *PanConfig) SetCacheSizeByStr(sizeStr string) error { 52 | size, err := converter.ParseFileSizeStr(sizeStr) 53 | if err != nil { 54 | return err 55 | } 56 | c.CacheSize = int(size) 57 | return nil 58 | } 59 | 60 | // SetMaxDownloadRateByStr 设置 max_download_rate 61 | func (c *PanConfig) SetMaxDownloadRateByStr(sizeStr string) error { 62 | size, err := converter.ParseFileSizeStr(stripPerSecond(sizeStr)) 63 | if err != nil { 64 | return err 65 | } 66 | c.MaxDownloadRate = size 67 | return nil 68 | } 69 | 70 | // SetMaxUploadRateByStr 设置 max_upload_rate 71 | func (c *PanConfig) SetMaxUploadRateByStr(sizeStr string) error { 72 | size, err := converter.ParseFileSizeStr(stripPerSecond(sizeStr)) 73 | if err != nil { 74 | return err 75 | } 76 | c.MaxUploadRate = size 77 | return nil 78 | } 79 | 80 | // PrintTable 输出表格 81 | func (c *PanConfig) PrintTable() { 82 | tb := cmdtable.NewTable(os.Stdout) 83 | tb.SetHeader([]string{"名称", "值", "建议值", "描述"}) 84 | tb.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 85 | tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT}) 86 | tb.AppendBulk([][]string{ 87 | []string{"cache_size", converter.ConvertFileSize(int64(c.CacheSize), 2), "1KB ~ 256KB", "下载缓存, 如果硬盘占用高或下载速度慢, 请尝试调大此值"}, 88 | []string{"max_download_parallel", strconv.Itoa(c.MaxDownloadParallel), "1 ~ 20", "最大下载并发量,即同时下载文件最大数量"}, 89 | []string{"max_upload_parallel", strconv.Itoa(c.MaxUploadParallel), "1 ~ 20", "最大上传并发量,即同时上传文件最大数量"}, 90 | []string{"max_download_rate", showMaxRate(c.MaxDownloadRate), "", "限制最大下载速度, 0代表不限制"}, 91 | []string{"max_upload_rate", showMaxRate(c.MaxUploadRate), "", "限制最大上传速度, 0代表不限制"}, 92 | []string{"savedir", c.SaveDir, "", "下载文件的储存目录"}, 93 | []string{"proxy", c.Proxy, "", "设置代理, 支持 http/socks5 代理,例如:http://127.0.0.1:8888"}, 94 | []string{"local_addrs", c.LocalAddrs, "", "设置本地网卡地址, 多个地址用逗号隔开"}, 95 | []string{"ip_type", c.PreferIPType, "ipv4-优先IPv4,ipv6-优先IPv6", "设置域名解析IP优先类型。修改后需要重启应用生效"}, 96 | }) 97 | tb.Render() 98 | } 99 | -------------------------------------------------------------------------------- /internal/config/pan_user.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 | "github.com/tickstep/cloudpan189-api/cloudpan" 19 | "github.com/tickstep/cloudpan189-api/cloudpan/apierror" 20 | "github.com/tickstep/library-go/expires/cachemap" 21 | "github.com/tickstep/library-go/logger" 22 | "path" 23 | "path/filepath" 24 | ) 25 | 26 | type PanUser struct { 27 | UID uint64 `json:"uid"` 28 | Nickname string `json:"nickname"` 29 | AccountName string `json:"accountName"` 30 | Sex string `json:"sex"` 31 | Workdir string `json:"workdir"` 32 | WorkdirFileEntity cloudpan.AppFileEntity `json:"workdirFileEntity"` 33 | 34 | FamilyWorkdir string `json:"familyWorkdir"` 35 | FamilyWorkdirFileEntity cloudpan.AppFileEntity `json:"familyWorkdirFileEntity"` 36 | 37 | ActiveFamilyId int64 `json:"activeFamilyId"` // 0代表个人云 38 | ActiveFamilyInfo cloudpan.AppFamilyInfo `json:"activeFamilyInfo"` 39 | 40 | LoginUserName string `json:"loginUserName"` 41 | LoginUserPassword string `json:"loginUserPassword"` 42 | 43 | WebToken cloudpan.WebLoginToken `json:"webToken"` 44 | AppToken cloudpan.AppLoginToken `json:"appToken"` 45 | panClient *cloudpan.PanClient 46 | cacheOpMap cachemap.CacheOpMap 47 | } 48 | 49 | type PanUserList []*PanUser 50 | 51 | func SetupUserByCookie(webToken *cloudpan.WebLoginToken, appToken *cloudpan.AppLoginToken) (user *PanUser, err *apierror.ApiError) { 52 | tryRefreshWebToken := true 53 | 54 | doLoginAct: 55 | panClient := cloudpan.NewPanClient(*webToken, *appToken) 56 | u := &PanUser{ 57 | WebToken: *webToken, 58 | AppToken: *appToken, 59 | panClient: panClient, 60 | Workdir: "/", 61 | WorkdirFileEntity: *cloudpan.NewAppFileEntityForRootDir(), 62 | FamilyWorkdir: "/", 63 | FamilyWorkdirFileEntity: *cloudpan.NewAppFileEntityForRootDir(), 64 | } 65 | 66 | // web api token maybe expired 67 | userInfo, err := panClient.GetUserInfo() 68 | if err != nil { 69 | if err.Code == apierror.ApiCodeTokenExpiredCode && appToken.SessionKey != "" && tryRefreshWebToken { 70 | tryRefreshWebToken = false 71 | webCookie := cloudpan.RefreshCookieToken(appToken.SessionKey) 72 | if webCookie != "" { 73 | webToken.CookieLoginUser = webCookie 74 | goto doLoginAct 75 | } 76 | } 77 | return nil, err 78 | } 79 | name := "Unknown" 80 | if userInfo != nil { 81 | name = userInfo.Nickname 82 | if name == "" { 83 | name = userInfo.UserAccount 84 | } 85 | 86 | // update cloudUser 87 | u.UID = userInfo.UserId 88 | u.AccountName = userInfo.UserAccount 89 | } else { 90 | // error, maybe the token has expired 91 | return nil, apierror.NewFailedApiError("cannot get user info, the token has expired") 92 | } 93 | u.Nickname = name 94 | 95 | userDetailInfo, err := panClient.GetUserDetailInfo() 96 | if userDetailInfo != nil { 97 | if userDetailInfo.Gender == "F" { 98 | u.Sex = "F" 99 | } else if userDetailInfo.Gender == "M" { 100 | u.Sex = "M" 101 | } else { 102 | u.Sex = "U" 103 | } 104 | } else { 105 | // error, maybe the token has expired 106 | return nil, apierror.NewFailedApiError("cannot get user info, the token has expired") 107 | } 108 | 109 | return u, nil 110 | } 111 | 112 | func (pu *PanUser) PanClient() *cloudpan.PanClient { 113 | return pu.panClient 114 | } 115 | 116 | // PathJoin 合并工作目录和相对路径p, 若p为绝对路径则忽略 117 | func (pu *PanUser) PathJoin(familyId int64, p string) string { 118 | if path.IsAbs(p) { 119 | return p 120 | } 121 | if familyId > 0 { 122 | if familyId == pu.ActiveFamilyId { 123 | return path.Join(pu.FamilyWorkdir, p) 124 | } else { 125 | return path.Join("/", p) 126 | } 127 | } else { 128 | return path.Join(pu.Workdir, p) 129 | } 130 | } 131 | 132 | func (pu *PanUser) FreshWorkdirInfo() { 133 | fe, err := pu.PanClient().AppFileInfoById(pu.ActiveFamilyId, pu.WorkdirFileEntity.FileId) 134 | if err != nil { 135 | logger.Verboseln("刷新工作目录信息失败") 136 | return 137 | } 138 | if pu.ActiveFamilyId > 0 { 139 | pu.FamilyWorkdirFileEntity = *fe 140 | } else { 141 | pu.WorkdirFileEntity = *fe 142 | } 143 | } 144 | 145 | // GetSavePath 根据提供的网盘文件路径 panpath, 返回本地储存路径, 146 | // 返回绝对路径, 获取绝对路径出错时才返回相对路径... 147 | func (pu *PanUser) GetSavePath(filePanPath string) string { 148 | dirStr := filepath.Join(Config.SaveDir, fmt.Sprintf("%d", pu.UID), filePanPath) 149 | dir, err := filepath.Abs(dirStr) 150 | if err != nil { 151 | dir = filepath.Clean(dirStr) 152 | } 153 | return dir 154 | } 155 | -------------------------------------------------------------------------------- /internal/config/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 config 15 | 16 | import ( 17 | "encoding/hex" 18 | "github.com/olekukonko/tablewriter" 19 | "github.com/tickstep/cloudpan189-go/cmder/cmdtable" 20 | "github.com/tickstep/library-go/converter" 21 | "github.com/tickstep/library-go/crypto" 22 | "github.com/tickstep/library-go/ids" 23 | "github.com/tickstep/library-go/logger" 24 | "os" 25 | "strconv" 26 | "strings" 27 | ) 28 | 29 | func (pl *PanUserList) String() string { 30 | builder := &strings.Builder{} 31 | 32 | tb := cmdtable.NewTable(builder) 33 | tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER}) 34 | tb.SetHeader([]string{"#", "uid", "用户名", "昵称", "性别"}) 35 | 36 | for k, userInfo := range *pl { 37 | sex := "未知" 38 | if userInfo.Sex == "F" { 39 | sex = "女" 40 | } else if userInfo.Sex == "M" { 41 | sex = "男" 42 | } 43 | tb.Append([]string{strconv.Itoa(k), strconv.FormatUint(userInfo.UID, 10), userInfo.AccountName, userInfo.Nickname, sex}) 44 | } 45 | 46 | tb.Render() 47 | 48 | return builder.String() 49 | } 50 | 51 | // AverageParallel 返回平均的下载最大并发量 52 | func AverageParallel(parallel, downloadLoad int) int { 53 | if downloadLoad < 1 { 54 | return 1 55 | } 56 | 57 | p := parallel / downloadLoad 58 | if p < 1 { 59 | return 1 60 | } 61 | return p 62 | } 63 | 64 | func stripPerSecond(sizeStr string) string { 65 | i := strings.LastIndex(sizeStr, "/") 66 | if i < 0 { 67 | return sizeStr 68 | } 69 | return sizeStr[:i] 70 | } 71 | 72 | func showMaxRate(size int64) string { 73 | if size <= 0 { 74 | return "不限制" 75 | } 76 | return converter.ConvertFileSize(size, 2) + "/s" 77 | } 78 | 79 | // EncryptString 加密 80 | func EncryptString(text string) string { 81 | if text == "" { 82 | return "" 83 | } 84 | d := []byte(text) 85 | key := []byte(ids.GetUniqueId("cloudpan189", 16)) 86 | r, e := crypto.EncryptAES(d, key) 87 | if e != nil { 88 | return text 89 | } 90 | return hex.EncodeToString(r) 91 | } 92 | 93 | // DecryptString 解密 94 | func DecryptString(text string) string { 95 | defer func() { 96 | if err := recover(); err != nil { 97 | logger.Verboseln("decrypt string failed, maybe the key has been changed") 98 | } 99 | }() 100 | 101 | if text == "" { 102 | return "" 103 | } 104 | d, _ := hex.DecodeString(text) 105 | 106 | // use the machine unique id as the key 107 | // but in some OS, this key will be changed if you reinstall the OS 108 | key := []byte(ids.GetUniqueId("cloudpan189", 16)) 109 | r, e := crypto.DecryptAES(d, key) 110 | if e != nil { 111 | return text 112 | } 113 | return string(r) 114 | } 115 | 116 | // IsFolderExist 判断文件夹是否存在 117 | func IsFolderExist(pathStr string) bool { 118 | fi, err := os.Stat(pathStr) 119 | if err != nil { 120 | if os.IsExist(err) { 121 | return fi.IsDir() 122 | } 123 | if os.IsNotExist(err) { 124 | return false 125 | } 126 | return false 127 | } 128 | return fi.IsDir() 129 | } 130 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/cloudpan189-go/library/requester/transfer" 18 | ) 19 | 20 | const ( 21 | //CacheSize 默认的下载缓存 22 | CacheSize = 8192 23 | ) 24 | 25 | var ( 26 | // MinParallelSize 单个线程最小的数据量 27 | MinParallelSize int64 = 1 * 1024 * 1024 // 1MB 28 | 29 | // MaxParallelWorkerCount 单个文件下载最大并发线程数量 30 | MaxParallelWorkerCount int = 3 31 | ) 32 | 33 | //Config 下载配置 34 | type Config struct { 35 | Mode transfer.RangeGenMode // 下载Range分配模式 36 | MaxParallel int // 最大下载并发量 37 | CacheSize int // 下载缓冲 38 | BlockSize int64 // 每个Range区块的大小, RangeGenMode 为 RangeGenMode2 时才有效 39 | MaxRate int64 // 限制最大下载速度 40 | InstanceStateStorageFormat InstanceStateStorageFormat // 断点续传储存类型 41 | InstanceStatePath string // 断点续传信息路径 42 | TryHTTP bool // 是否尝试使用 http 连接 43 | ShowProgress bool // 是否展示下载进度条 44 | ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式 45 | } 46 | 47 | //NewConfig 返回默认配置 48 | func NewConfig() *Config { 49 | return &Config{ 50 | MaxParallel: 5, 51 | CacheSize: CacheSize, 52 | } 53 | } 54 | 55 | //Fix 修复配置信息, 使其合法 56 | func (cfg *Config) Fix() { 57 | fixCacheSize(&cfg.CacheSize) 58 | if cfg.MaxParallel < 1 { 59 | cfg.MaxParallel = 1 60 | } 61 | } 62 | 63 | //Copy 拷贝新的配置 64 | func (cfg *Config) Copy() *Config { 65 | newCfg := *cfg 66 | return &newCfg 67 | } 68 | -------------------------------------------------------------------------------- /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/cloudpan189-go/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/cloudpan189-go/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 | ) 67 | 68 | //GetStatusText 根据状态码获取状态信息 69 | func GetStatusText(sc StatusCode) string { 70 | switch sc { 71 | case StatusCodeInit: 72 | return "初始化" 73 | case StatusCodeSuccessed: 74 | return "成功" 75 | case StatusCodePending: 76 | return "等待响应" 77 | case StatusCodeDownloading: 78 | return "下载中" 79 | case StatusCodeWaitToWrite: 80 | return "等待写入数据" 81 | case StatusCodeInternalError: 82 | return "内部错误" 83 | case StatusCodeTooManyConnections: 84 | return "连接数太多" 85 | case StatusCodeNetError: 86 | return "网络错误" 87 | case StatusCodeFailed: 88 | return "下载失败" 89 | case StatusCodePaused: 90 | return "已暂停" 91 | case StatusCodeReseted: 92 | return "已重设连接" 93 | case StatusCodeCanceled: 94 | return "已取消" 95 | default: 96 | return "未知状态码" 97 | } 98 | } 99 | 100 | //NewWorkerStatus 初始化WorkerStatus 101 | func NewWorkerStatus() *WorkerStatus { 102 | return &WorkerStatus{ 103 | statusCode: StatusCodeInit, 104 | } 105 | } 106 | 107 | //SetStatusCode 设置worker状态码 108 | func (ws *WorkerStatus) SetStatusCode(sc StatusCode) { 109 | ws.statusCode = sc 110 | } 111 | 112 | //StatusCode 返回状态码 113 | func (ws *WorkerStatus) StatusCode() StatusCode { 114 | return ws.statusCode 115 | } 116 | 117 | //StatusText 返回状态信息 118 | func (ws *WorkerStatus) StatusText() string { 119 | return GetStatusText(ws.statusCode) 120 | } 121 | -------------------------------------------------------------------------------- /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 | "github.com/tickstep/library-go/logger" 18 | "github.com/tickstep/library-go/requester" 19 | mathrand "math/rand" 20 | "mime" 21 | "net/url" 22 | "path" 23 | "regexp" 24 | "strconv" 25 | "time" 26 | ) 27 | 28 | var ( 29 | // ContentRangeRE Content-Range 正则 30 | ContentRangeRE = regexp.MustCompile(`^.*? \d*?-\d*?/(\d*?)$`) 31 | 32 | // ranSource 随机数种子 33 | ranSource = mathrand.NewSource(time.Now().UnixNano()) 34 | 35 | // ran 一个随机数实例 36 | ran = mathrand.New(ranSource) 37 | ) 38 | 39 | // RandomNumber 生成指定区间随机数 40 | func RandomNumber(min, max int) int { 41 | if min > max { 42 | min, max = max, min 43 | } 44 | return ran.Intn(max-min) + min 45 | } 46 | 47 | // GetFileName 获取文件名 48 | func GetFileName(uri string, client *requester.HTTPClient) (filename string, err error) { 49 | if client == nil { 50 | client = requester.NewHTTPClient() 51 | } 52 | 53 | resp, err := client.Req("HEAD", uri, nil, nil) 54 | if resp != nil { 55 | defer resp.Body.Close() 56 | } 57 | if err != nil { 58 | return "", err 59 | } 60 | 61 | _, params, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition")) 62 | if err != nil { 63 | logger.Verbosef("DEBUG: GetFileName ParseMediaType error: %s\n", err) 64 | return path.Base(uri), nil 65 | } 66 | 67 | filename, err = url.QueryUnescape(params["filename"]) 68 | if err != nil { 69 | return 70 | } 71 | 72 | if filename == "" { 73 | filename = path.Base(uri) 74 | } 75 | 76 | return 77 | } 78 | 79 | // ParseContentRange 解析Content-Range 80 | func ParseContentRange(contentRange string) (contentLength int64) { 81 | raw := ContentRangeRE.FindStringSubmatch(contentRange) 82 | if len(raw) < 2 { 83 | return -1 84 | } 85 | 86 | c, err := strconv.ParseInt(raw[1], 10, 64) 87 | if err != nil { 88 | return -1 89 | } 90 | return c 91 | } 92 | 93 | func fixCacheSize(size *int) { 94 | if *size < 1024 { 95 | *size = 1024 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /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 | ) 20 | 21 | type ( 22 | // Fder 获取fd接口 23 | Fder interface { 24 | Fd() uintptr 25 | } 26 | 27 | // Writer 下载器数据输出接口 28 | Writer interface { 29 | io.WriterAt 30 | } 31 | ) 32 | 33 | // NewDownloaderWriterByFilename 创建下载器数据输出接口, 类似于os.OpenFile 34 | func NewDownloaderWriterByFilename(name string, flag int, perm os.FileMode) (writer Writer, file *os.File, err error) { 35 | file, err = os.OpenFile(name, flag, perm) 36 | if err != nil { 37 | return 38 | } 39 | 40 | writer = file 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /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/library-go/requester/rio/speeds" 20 | "github.com/tickstep/cloudpan189-go/library/requester/transfer" 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 | } 34 | 35 | fileBlock struct { 36 | readRange transfer.Range 37 | readed int64 38 | readerAt io.ReaderAt 39 | speedsStatRef *speeds.Speeds 40 | rateLimit *speeds.RateLimit 41 | mu sync.Mutex 42 | } 43 | 44 | bufioFileBlock struct { 45 | *fileBlock 46 | bufio *bufio.Reader 47 | } 48 | ) 49 | 50 | // SplitBlock 文件分块 51 | func SplitBlock(fileSize, blockSize int64) (blockList []*BlockState) { 52 | gen := transfer.NewRangeListGenBlockSize(fileSize, 0, blockSize) 53 | rangeCount := gen.RangeCount() 54 | blockList = make([]*BlockState, 0, rangeCount) 55 | for i := 0; i < rangeCount; i++ { 56 | id, r := gen.GenRange() 57 | blockList = append(blockList, &BlockState{ 58 | ID: id, 59 | Range: *r, 60 | }) 61 | } 62 | return 63 | } 64 | 65 | // NewBufioSplitUnit io.ReaderAt实现SplitUnit接口, 有Buffer支持 66 | func NewBufioSplitUnit(readerAt io.ReaderAt, readRange transfer.Range, speedsStat *speeds.Speeds, rateLimit *speeds.RateLimit) SplitUnit { 67 | su := &fileBlock{ 68 | readerAt: readerAt, 69 | readRange: readRange, 70 | speedsStatRef: speedsStat, 71 | rateLimit: rateLimit, 72 | } 73 | return &bufioFileBlock{ 74 | fileBlock: su, 75 | bufio: bufio.NewReaderSize(su, BufioReadSize), 76 | } 77 | } 78 | 79 | func (bfb *bufioFileBlock) Read(b []byte) (n int, err error) { 80 | return bfb.bufio.Read(b) // 间接调用fileBlock 的Read 81 | } 82 | 83 | // Read 只允许一个线程读同一个文件 84 | func (fb *fileBlock) Read(b []byte) (n int, err error) { 85 | fb.mu.Lock() 86 | defer fb.mu.Unlock() 87 | 88 | left := int(fb.Left()) 89 | if left <= 0 { 90 | return 0, io.EOF 91 | } 92 | 93 | if len(b) > left { 94 | n, err = fb.readerAt.ReadAt(b[:left], fb.readed+fb.readRange.Begin) 95 | } else { 96 | n, err = fb.readerAt.ReadAt(b, fb.readed+fb.readRange.Begin) 97 | } 98 | 99 | n64 := int64(n) 100 | fb.readed += n64 101 | if fb.rateLimit != nil { 102 | fb.rateLimit.Add(n64) // 限速阻塞 103 | } 104 | if fb.speedsStatRef != nil { 105 | fb.speedsStatRef.Add(n64) 106 | } 107 | return 108 | } 109 | 110 | func (fb *fileBlock) Seek(offset int64, whence int) (int64, error) { 111 | fb.mu.Lock() 112 | defer fb.mu.Unlock() 113 | 114 | switch whence { 115 | case os.SEEK_SET: 116 | fb.readed = offset 117 | case os.SEEK_CUR: 118 | fb.readed += offset 119 | case os.SEEK_END: 120 | fb.readed = fb.readRange.End - fb.readRange.Begin + offset 121 | default: 122 | return 0, fmt.Errorf("unsupport whence: %d", whence) 123 | } 124 | if fb.readed < 0 { 125 | fb.readed = 0 126 | } 127 | return fb.readed, nil 128 | } 129 | 130 | func (fb *fileBlock) Len() int64 { 131 | return fb.readRange.End - fb.readRange.Begin 132 | } 133 | 134 | func (fb *fileBlock) Left() int64 { 135 | return fb.readRange.End - fb.readRange.Begin - fb.readed 136 | } 137 | 138 | func (fb *fileBlock) Range() transfer.Range { 139 | return fb.readRange 140 | } 141 | 142 | func (fb *fileBlock) Readed() int64 { 143 | return fb.readed 144 | } 145 | -------------------------------------------------------------------------------- /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/cloudpan189-go/library/requester/transfer" 21 | "github.com/tickstep/cloudpan189-go/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) 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 | type ( 17 | // MultiError 多线程上传的错误 18 | MultiError struct { 19 | Err error 20 | // IsRetry 是否重试, 21 | Terminated bool 22 | } 23 | ) 24 | 25 | func (me *MultiError) Error() string { 26 | return me.Err.Error() 27 | } 28 | -------------------------------------------------------------------------------- /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/cloudpan189-go/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), 42 | uploadDone: false, 43 | }) 44 | } else { 45 | // 已经完成的, 也要加入 (可继续优化) 46 | workers = append(workers, &worker{ 47 | id: blockState.ID, 48 | partOffset: blockState.Range.Begin, 49 | splitUnit: &fileBlock{ 50 | readRange: blockState.Range, 51 | readed: blockState.Range.End - blockState.Range.Begin, 52 | readerAt: muer.file, 53 | }, 54 | uploadDone: true, 55 | }) 56 | } 57 | } 58 | return workers 59 | } 60 | -------------------------------------------------------------------------------- /internal/file/uploader/multiworker.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 | "context" 18 | "github.com/tickstep/cloudpan189-go/internal/waitgroup" 19 | "github.com/oleiade/lane" 20 | "os" 21 | "strconv" 22 | ) 23 | 24 | type ( 25 | worker struct { 26 | id int 27 | partOffset int64 28 | splitUnit SplitUnit 29 | uploadDone bool 30 | } 31 | 32 | workerList []*worker 33 | ) 34 | 35 | func (werl *workerList) Readed() int64 { 36 | var readed int64 37 | for _, wer := range *werl { 38 | readed += wer.splitUnit.Readed() 39 | } 40 | return readed 41 | } 42 | 43 | func (muer *MultiUploader) upload() (uperr error) { 44 | err := muer.multiUpload.Precreate() 45 | if err != nil { 46 | return err 47 | } 48 | 49 | var ( 50 | uploadDeque = lane.NewDeque() 51 | ) 52 | 53 | // 加入队列 54 | for _, wer := range muer.workers { 55 | if !wer.uploadDone { 56 | uploadDeque.Append(wer) 57 | } 58 | } 59 | 60 | for { 61 | wg := waitgroup.NewWaitGroup(muer.config.Parallel) 62 | for { 63 | e := uploadDeque.Shift() 64 | if e == nil { // 任务为空 65 | break 66 | } 67 | 68 | wer := e.(*worker) 69 | wg.AddDelta() 70 | go func() { 71 | defer wg.Done() 72 | 73 | var ( 74 | ctx, cancel = context.WithCancel(context.Background()) 75 | doneChan = make(chan struct{}) 76 | uploadDone bool 77 | terr error 78 | ) 79 | go func() { 80 | if !wer.uploadDone { 81 | uploaderVerbose.Info("begin to upload part: " + strconv.Itoa(wer.id)) 82 | uploadDone, terr = muer.multiUpload.UploadFile(ctx, int(wer.id), wer.partOffset, wer.splitUnit.Range().End, wer.splitUnit) 83 | } else { 84 | uploadDone = true 85 | } 86 | close(doneChan) 87 | }() 88 | select { 89 | case <-muer.canceled: 90 | cancel() 91 | return 92 | case <-doneChan: 93 | // continue 94 | uploaderVerbose.Info("multiUpload worker upload file done") 95 | } 96 | cancel() 97 | if terr != nil { 98 | if me, ok := terr.(*MultiError); ok { 99 | if me.Terminated { // 终止 100 | muer.closeCanceledOnce.Do(func() { // 只关闭一次 101 | close(muer.canceled) 102 | }) 103 | uperr = me.Err 104 | return 105 | } 106 | } 107 | 108 | uploaderVerbose.Warnf("upload err: %s, id: %d\n", terr, wer.id) 109 | wer.splitUnit.Seek(0, os.SEEK_SET) 110 | uploadDeque.Append(wer) 111 | return 112 | } 113 | wer.uploadDone = uploadDone 114 | 115 | // 通知更新 116 | if muer.updateInstanceStateChan != nil && len(muer.updateInstanceStateChan) < cap(muer.updateInstanceStateChan) { 117 | muer.updateInstanceStateChan <- struct{}{} 118 | } 119 | }() 120 | } 121 | wg.Wait() 122 | 123 | // 没有任务了 124 | if uploadDeque.Size() == 0 { 125 | break 126 | } 127 | } 128 | 129 | select { 130 | case <-muer.canceled: 131 | if uperr != nil { 132 | return uperr 133 | } 134 | return context.Canceled 135 | default: 136 | } 137 | 138 | // upload file commit 139 | // 检测是否全部分片上传成功 140 | allSuccess := true 141 | for _, wer := range muer.workers { 142 | allSuccess = allSuccess && wer.uploadDone 143 | } 144 | if allSuccess { 145 | e := muer.multiUpload.CommitFile() 146 | if e != nil { 147 | uploaderVerbose.Warn("upload file commit failed: " + e.Error()) 148 | return e 149 | } 150 | } else { 151 | uploaderVerbose.Warn("upload file not all success: " + muer.uploadFileId) 152 | } 153 | 154 | return 155 | } 156 | -------------------------------------------------------------------------------- /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 | "time" 18 | ) 19 | 20 | type ( 21 | // Status 上传状态接口 22 | Status interface { 23 | TotalSize() int64 // 总大小 24 | Uploaded() int64 // 已上传数据 25 | SpeedsPerSecond() int64 // 每秒的上传速度 26 | TimeElapsed() time.Duration // 上传时间 27 | } 28 | 29 | // UploadStatus 上传状态 30 | UploadStatus struct { 31 | totalSize int64 // 总大小 32 | uploaded int64 // 已上传数据 33 | speedsPerSecond int64 // 每秒的上传速度 34 | timeElapsed time.Duration // 上传时间 35 | } 36 | 37 | UploadStatusFunc func(status Status, updateChan <-chan struct{}) 38 | ) 39 | 40 | // TotalSize 返回总大小 41 | func (us *UploadStatus) TotalSize() int64 { 42 | return us.totalSize 43 | } 44 | 45 | // Uploaded 返回已上传数据 46 | func (us *UploadStatus) Uploaded() int64 { 47 | return us.uploaded 48 | } 49 | 50 | // SpeedsPerSecond 返回每秒的上传速度 51 | func (us *UploadStatus) SpeedsPerSecond() int64 { 52 | return us.speedsPerSecond 53 | } 54 | 55 | // TimeElapsed 返回上传时间 56 | func (us *UploadStatus) TimeElapsed() time.Duration { 57 | return us.timeElapsed 58 | } 59 | 60 | // GetStatusChan 获取上传状态 61 | func (u *Uploader) GetStatusChan() <-chan Status { 62 | c := make(chan Status) 63 | 64 | go func() { 65 | for { 66 | select { 67 | case <-u.finished: 68 | close(c) 69 | return 70 | default: 71 | if !u.executed { 72 | time.Sleep(1 * time.Second) 73 | continue 74 | } 75 | 76 | old := u.readed64.Readed() 77 | time.Sleep(1 * time.Second) // 每秒统计 78 | 79 | readed := u.readed64.Readed() 80 | c <- &UploadStatus{ 81 | totalSize: u.readed64.Len(), 82 | uploaded: readed, 83 | speedsPerSecond: readed - old, 84 | timeElapsed: time.Since(u.executeTime) / 1e7 * 1e7, 85 | } 86 | } 87 | } 88 | }() 89 | return c 90 | } 91 | 92 | func (muer *MultiUploader) uploadStatusEvent() { 93 | if muer.onUploadStatusEvent == nil { 94 | return 95 | } 96 | 97 | go func() { 98 | ticker := time.NewTicker(1 * time.Second) // 每秒统计 99 | defer ticker.Stop() 100 | for { 101 | select { 102 | case <-muer.finished: 103 | return 104 | case <-ticker.C: 105 | readed := muer.workers.Readed() 106 | muer.onUploadStatusEvent(&UploadStatus{ 107 | totalSize: muer.file.Len(), 108 | uploaded: readed, 109 | speedsPerSecond: muer.speedsStat.GetSpeeds(), 110 | timeElapsed: time.Since(muer.executeTime) / 1e8 * 1e8, 111 | }, muer.updateInstanceStateChan) 112 | } 113 | } 114 | }() 115 | } 116 | -------------------------------------------------------------------------------- /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/cloudpan189-go/internal/config" 18 | "github.com/tickstep/cloudpan189-go/internal/utils" 19 | "github.com/tickstep/library-go/converter" 20 | "github.com/tickstep/library-go/logger" 21 | "github.com/tickstep/library-go/requester" 22 | "github.com/tickstep/library-go/requester/rio" 23 | "net/http" 24 | "time" 25 | ) 26 | 27 | const ( 28 | // BufioReadSize bufio 缓冲区大小, 用于上传时读取文件 29 | BufioReadSize = int(64 * converter.KB) // 64KB 30 | ) 31 | 32 | type ( 33 | //CheckFunc 上传完成的检测函数 34 | CheckFunc func(resp *http.Response, uploadErr error) 35 | 36 | // Uploader 上传 37 | Uploader struct { 38 | url string // 上传地址 39 | readed64 Readed64 // 要上传的对象 40 | contentType string 41 | 42 | client *requester.HTTPClient 43 | 44 | executeTime time.Time 45 | executed bool 46 | finished chan struct{} 47 | 48 | checkFunc CheckFunc 49 | onExecute func() 50 | onFinish func() 51 | } 52 | ) 53 | 54 | var ( 55 | uploaderVerbose = logger.New("UPLOADER", config.EnvVerbose) 56 | ) 57 | 58 | // NewUploader 返回 uploader 对象, url: 上传地址, readerlen64: 实现 rio.ReaderLen64 接口的对象, 例如文件 59 | func NewUploader(url string, readerlen64 rio.ReaderLen64) (uploader *Uploader) { 60 | uploader = &Uploader{ 61 | url: url, 62 | readed64: NewReaded64(readerlen64), 63 | } 64 | 65 | return 66 | } 67 | 68 | func (u *Uploader) lazyInit() { 69 | if u.finished == nil { 70 | u.finished = make(chan struct{}) 71 | } 72 | if u.client == nil { 73 | u.client = requester.NewHTTPClient() 74 | } 75 | u.client.SetTimeout(0) 76 | u.client.SetResponseHeaderTimeout(0) 77 | } 78 | 79 | // SetClient 设置http客户端 80 | func (u *Uploader) SetClient(c *requester.HTTPClient) { 81 | u.client = c 82 | } 83 | 84 | //SetContentType 设置Content-Type 85 | func (u *Uploader) SetContentType(contentType string) { 86 | u.contentType = contentType 87 | } 88 | 89 | //SetCheckFunc 设置上传完成的检测函数 90 | func (u *Uploader) SetCheckFunc(checkFunc CheckFunc) { 91 | u.checkFunc = checkFunc 92 | } 93 | 94 | // Execute 执行上传, 收到返回值信号则为上传结束 95 | func (u *Uploader) Execute() { 96 | utils.Trigger(u.onExecute) 97 | 98 | // 开始上传 99 | u.executeTime = time.Now() 100 | u.executed = true 101 | resp, _, err := u.execute() 102 | 103 | // 上传结束 104 | close(u.finished) 105 | 106 | if u.checkFunc != nil { 107 | u.checkFunc(resp, err) 108 | } 109 | 110 | utils.Trigger(u.onFinish) // 触发上传结束的事件 111 | } 112 | 113 | func (u *Uploader) execute() (resp *http.Response, code int, err error) { 114 | u.lazyInit() 115 | header := map[string]string{} 116 | if u.contentType != "" { 117 | header["Content-Type"] = u.contentType 118 | } 119 | 120 | resp, err = u.client.Req(http.MethodPost, u.url, u.readed64, header) 121 | if err != nil { 122 | return nil, 2, err 123 | } 124 | 125 | return resp, 0, nil 126 | } 127 | 128 | // OnExecute 任务开始时触发的事件 129 | func (u *Uploader) OnExecute(fn func()) { 130 | u.onExecute = fn 131 | } 132 | 133 | // OnFinish 任务完成时触发的事件 134 | func (u *Uploader) OnFinish(fn func()) { 135 | u.onFinish = fn 136 | } 137 | -------------------------------------------------------------------------------- /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/cloudpan189-go/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/cloudpan189-api/cloudpan" 18 | "os" 19 | ) 20 | 21 | // CheckFileValid 检测文件有效性 22 | func CheckFileValid(filePath string, fileInfo *cloudpan.AppFileEntity) error { 23 | // 检查MD5 24 | // 检查文件大小 25 | // 检查digest签名 26 | return nil 27 | } 28 | 29 | // FileExist 检查文件是否存在, 30 | // 只有当文件存在, 文件大小不为0或断点续传文件不存在时, 才判断为存在 31 | func FileExist(path string) bool { 32 | if info, err := os.Stat(path); err == nil { 33 | if info.Size() == 0 { 34 | return false 35 | } 36 | if _, err = os.Stat(path + DownloadSuffix); err != nil { 37 | return true 38 | } 39 | } 40 | 41 | return false 42 | } 43 | -------------------------------------------------------------------------------- /internal/functions/panupload/sync_database.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep & chenall 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 | type SyncDb interface { 17 | //读取记录,返回值不会是nil 18 | Get(key string) (ufm *UploadedFileMeta) 19 | //删除单条记录 20 | Del(key string) error 21 | //根据前辍删除数据库记录,比如删除一个目录时可以连同子目录一起删除 22 | DelWithPrefix(prefix string) error 23 | Put(key string, value *UploadedFileMeta) error 24 | Close() error 25 | //读取数据库指定路径前辍的第一条记录(也作为循环获取的初始化,配置Next函数使用) 26 | First(prefix string) (*UploadedFileMeta, error) 27 | //获取指定路径前辍的的下一条记录 28 | Next(prefix string) (*UploadedFileMeta, error) 29 | //是否进行自动数据库清理 30 | //注: 清理规则,所有以 prefix 前辍开头并且未更新的记录都将被清理,只有在必要的时候才开启这个功能。 31 | AutoClean(prefix string, cleanFlag bool) 32 | } 33 | 34 | type autoCleanInfo struct { 35 | PreFix string 36 | SyncTime int64 37 | } 38 | 39 | func OpenSyncDb(file string, bucket string) (SyncDb, error) { 40 | return openBoltDb(file, bucket) 41 | } 42 | 43 | type dbTableField struct { 44 | Path string 45 | Data []byte 46 | } 47 | -------------------------------------------------------------------------------- /internal/functions/panupload/sync_database_bolt.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep & chenall 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 | "bytes" 18 | "fmt" 19 | jsoniter "github.com/json-iterator/go" 20 | "github.com/tickstep/bolt" 21 | "github.com/tickstep/library-go/logger" 22 | "time" 23 | ) 24 | 25 | type boltDB struct { 26 | db *bolt.DB 27 | bucket string 28 | next map[string]*boltDBScan 29 | cleanInfo *autoCleanInfo 30 | } 31 | 32 | type boltDBScan struct { 33 | entries []*boltKV 34 | off int 35 | size int 36 | } 37 | 38 | type boltKV struct { 39 | k []byte 40 | v []byte 41 | } 42 | 43 | func openBoltDb(file string, bucket string) (SyncDb, error) { 44 | db, err := bolt.Open(file + "_bolt.db", 0600, &bolt.Options{Timeout: 5 * time.Second}) 45 | 46 | if err != nil { 47 | return nil, err 48 | } 49 | logger.Verboseln("open boltDB ok") 50 | return &boltDB{db: db, bucket: bucket, next: make(map[string]*boltDBScan)}, nil 51 | } 52 | 53 | func (db *boltDB) Get(key string) (data *UploadedFileMeta) { 54 | data = &UploadedFileMeta{Path: key} 55 | db.db.View(func(tx *bolt.Tx) error { 56 | b := tx.Bucket([]byte(db.bucket)) 57 | if b == nil { 58 | return nil 59 | } 60 | v := b.Get([]byte(key)) 61 | return jsoniter.Unmarshal(v, data) 62 | }) 63 | 64 | return data 65 | } 66 | 67 | func (db *boltDB) Del(key string) error { 68 | return db.db.Update(func(tx *bolt.Tx) error { 69 | b := tx.Bucket([]byte(db.bucket)) 70 | if b == nil { 71 | return nil 72 | } 73 | return b.Delete([]byte(key)) 74 | }) 75 | } 76 | 77 | func (db *boltDB) AutoClean(prefix string, cleanFlag bool) { 78 | if !cleanFlag { 79 | db.cleanInfo = nil 80 | } else if db.cleanInfo == nil { 81 | db.cleanInfo = &autoCleanInfo{ 82 | PreFix: prefix, 83 | SyncTime: time.Now().Unix(), 84 | } 85 | } 86 | } 87 | 88 | func (db *boltDB) clean() (count uint) { 89 | for ufm, err := db.First(db.cleanInfo.PreFix); err == nil; ufm, err = db.Next(db.cleanInfo.PreFix) { 90 | if ufm.LastSyncTime != db.cleanInfo.SyncTime { 91 | db.DelWithPrefix(ufm.Path) 92 | } 93 | } 94 | return 95 | } 96 | 97 | func (db *boltDB) DelWithPrefix(prefix string) error { 98 | return db.db.Update(func(tx *bolt.Tx) error { 99 | b := tx.Bucket([]byte(db.bucket)) 100 | if b == nil { 101 | return nil 102 | } 103 | c := b.Cursor() 104 | for k, _ := c.Seek([]byte(prefix)); k != nil && bytes.HasPrefix(k, []byte(prefix)); k, _ = c.Next() { 105 | b.Delete(k) 106 | } 107 | return nil 108 | }) 109 | } 110 | 111 | func (db *boltDB) First(prefix string) (*UploadedFileMeta, error) { 112 | db.db.View(func(tx *bolt.Tx) error { 113 | b := tx.Bucket([]byte(db.bucket)) 114 | if b == nil { 115 | return nil 116 | } 117 | c := b.Cursor() 118 | db.next[prefix] = &boltDBScan{ 119 | entries: []*boltKV{}, 120 | off: 0, 121 | size: 0, 122 | } 123 | for k, v := c.Seek([]byte(prefix)); k != nil && bytes.HasPrefix(k, []byte(prefix)); k, v = c.Next() { 124 | //fmt.Printf("key=%s, value=%s\n", k, v) 125 | if len(k) > 0 { 126 | db.next[prefix].entries = append(db.next[prefix].entries, &boltKV{ 127 | k: k, 128 | v: v, 129 | }) 130 | } 131 | } 132 | db.next[prefix].off = 0 133 | db.next[prefix].size = len(db.next[prefix].entries) 134 | return nil 135 | }) 136 | return db.Next(prefix) 137 | } 138 | 139 | func (db *boltDB) Next(prefix string) (*UploadedFileMeta, error) { 140 | data := &UploadedFileMeta{} 141 | if _,ok := db.next[prefix]; ok { 142 | if db.next[prefix].off >= db.next[prefix].size { 143 | return nil, fmt.Errorf("no any more record") 144 | } 145 | kv := db.next[prefix].entries[db.next[prefix].off] 146 | db.next[prefix].off++ 147 | if kv != nil { 148 | jsoniter.Unmarshal(kv.v, &data) 149 | data.Path = string(kv.k) 150 | return data, nil 151 | } 152 | } 153 | return nil, fmt.Errorf("no any more record") 154 | } 155 | 156 | func (db *boltDB) Put(key string, value *UploadedFileMeta) error { 157 | if db.cleanInfo != nil { 158 | value.LastSyncTime = db.cleanInfo.SyncTime 159 | } 160 | 161 | return db.db.Update(func(tx *bolt.Tx) error { 162 | data, err := jsoniter.Marshal(value) 163 | if err != nil { 164 | return err 165 | } 166 | b := tx.Bucket([]byte(db.bucket)) 167 | if b == nil { 168 | b,err = tx.CreateBucket([]byte(db.bucket)) 169 | if err != nil { 170 | return err 171 | } 172 | } 173 | return b.Put([]byte(key), data) 174 | }) 175 | } 176 | 177 | func (db *boltDB) Close() error { 178 | if db.cleanInfo != nil { 179 | db.clean() 180 | } 181 | if db.db != nil { 182 | return db.db.Close() 183 | } 184 | return nil 185 | } -------------------------------------------------------------------------------- /internal/functions/panupload/upload.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 | "context" 18 | "io" 19 | "net/http" 20 | "time" 21 | 22 | "github.com/tickstep/cloudpan189-api/cloudpan" 23 | "github.com/tickstep/cloudpan189-api/cloudpan/apierror" 24 | "github.com/tickstep/cloudpan189-go/internal/file/uploader" 25 | "github.com/tickstep/library-go/requester" 26 | "github.com/tickstep/library-go/requester/rio" 27 | ) 28 | 29 | type ( 30 | PanUpload struct { 31 | panClient *cloudpan.PanClient 32 | targetPath string 33 | familyId int64 34 | 35 | // UploadFileId 上传文件请求ID 36 | uploadFileId string 37 | // FileUploadUrl 上传文件数据的URL路径 38 | fileUploadUrl string 39 | // FileCommitUrl 上传文件完成后确认路径 40 | fileCommitUrl string 41 | // 请求的X-Request-ID 42 | xRequestId string 43 | } 44 | 45 | UploadedFileMeta struct { 46 | IsFolder bool `json:"isFolder,omitempty"` // 是否目录 47 | Path string `json:"-"` // 本地路径,不记录到数据库 48 | MD5 string `json:"md5,omitempty"` // 文件的 md5 49 | FileID string `json:"id,omitempty"` //文件、目录ID 50 | ParentId string `json:"parentId,omitempty"` //父文件夹ID 51 | Rev string `json:"rev,omitempty"` //文件版本 52 | Size int64 `json:"length,omitempty"` // 文件大小 53 | ModTime int64 `json:"modtime,omitempty"` // 修改日期 54 | LastSyncTime int64 `json:"synctime,omitempty"` //最后同步时间 55 | } 56 | 57 | EmptyReaderLen64 struct { 58 | } 59 | ) 60 | 61 | func (e EmptyReaderLen64) Read(p []byte) (n int, err error) { 62 | return 0, io.EOF 63 | } 64 | 65 | func (e EmptyReaderLen64) Len() int64 { 66 | return 0 67 | } 68 | 69 | func NewPanUpload(panClient *cloudpan.PanClient, targetPath, uploadUrl, commitUrl, uploadFileId, xRequestId string, familyId int64) uploader.MultiUpload { 70 | return &PanUpload{ 71 | panClient: panClient, 72 | targetPath: targetPath, 73 | familyId: familyId, 74 | uploadFileId: uploadFileId, 75 | fileUploadUrl: uploadUrl, 76 | fileCommitUrl: commitUrl, 77 | xRequestId: xRequestId, 78 | } 79 | } 80 | 81 | func (pu *PanUpload) lazyInit() { 82 | if pu.panClient == nil { 83 | pu.panClient = &cloudpan.PanClient{} 84 | } 85 | } 86 | 87 | func (pu *PanUpload) Precreate() (err error) { 88 | return nil 89 | } 90 | 91 | func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int64, partEnd int64, r rio.ReaderLen64) (uploadDone bool, uperr error) { 92 | pu.lazyInit() 93 | 94 | var respErr *uploader.MultiError 95 | fileRange := &cloudpan.AppFileUploadRange{ 96 | Offset: partOffset, 97 | Len: partEnd - partOffset, 98 | } 99 | var apiError *apierror.ApiError 100 | uploadFunc := func(httpMethod, fullUrl string, headers map[string]string) (resp *http.Response, err error) { 101 | client := requester.NewHTTPClient() 102 | client.SetTimeout(0) 103 | 104 | doneChan := make(chan struct{}, 1) 105 | go func() { 106 | resp, err = client.Req(httpMethod, fullUrl, r, headers) 107 | doneChan <- struct{}{} 108 | 109 | if resp != nil { 110 | // 不可恢复的错误 111 | switch resp.StatusCode { 112 | case 400, 401, 403, 413, 600: 113 | respErr = &uploader.MultiError{ 114 | Terminated: true, 115 | } 116 | } 117 | } 118 | }() 119 | select { 120 | case <-ctx.Done(): // 取消 121 | // 返回, 让那边关闭连接 122 | return resp, ctx.Err() 123 | case <-doneChan: 124 | // return 125 | } 126 | return 127 | } 128 | if pu.familyId > 0 { 129 | apiError = pu.panClient.AppFamilyUploadFileData(pu.familyId, pu.fileUploadUrl, pu.uploadFileId, pu.xRequestId, fileRange, uploadFunc) 130 | } else { 131 | apiError = pu.panClient.AppUploadFileData(pu.fileUploadUrl, pu.uploadFileId, pu.xRequestId, fileRange, uploadFunc) 132 | } 133 | 134 | if respErr != nil { 135 | return false, respErr 136 | } 137 | 138 | if apiError != nil { 139 | return false, apiError 140 | } 141 | 142 | return true, nil 143 | } 144 | 145 | func (pu *PanUpload) CommitFile() (cerr error) { 146 | time.Sleep(time.Duration(500) * time.Millisecond) 147 | pu.lazyInit() 148 | var er *apierror.ApiError 149 | if pu.familyId > 0 { 150 | _, er = pu.panClient.AppFamilyUploadFileCommit(pu.familyId, pu.fileCommitUrl, pu.uploadFileId, pu.xRequestId) 151 | } else { 152 | _, er = pu.panClient.AppUploadFileCommit(pu.fileCommitUrl, pu.uploadFileId, pu.xRequestId) 153 | } 154 | if er != nil { 155 | return er 156 | } 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /internal/functions/panupload/upload_database.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep & chenall 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 | "errors" 18 | "os" 19 | "path/filepath" 20 | "strings" 21 | "time" 22 | 23 | "github.com/tickstep/cloudpan189-go/internal/config" 24 | "github.com/tickstep/cloudpan189-go/internal/file/uploader" 25 | "github.com/tickstep/cloudpan189-go/internal/localfile" 26 | "github.com/tickstep/library-go/converter" 27 | "github.com/tickstep/library-go/jsonhelper" 28 | ) 29 | 30 | type ( 31 | // Uploading 未完成上传的信息 32 | Uploading struct { 33 | *localfile.LocalFileMeta 34 | State *uploader.InstanceState `json:"state"` 35 | } 36 | 37 | // UploadingDatabase 未完成上传的数据库 38 | UploadingDatabase struct { 39 | UploadingList []*Uploading `json:"upload_state"` 40 | Timestamp int64 `json:"timestamp"` 41 | 42 | dataFile *os.File 43 | } 44 | ) 45 | 46 | // NewUploadingDatabase 初始化未完成上传的数据库, 从库中读取内容 47 | func NewUploadingDatabase() (ud *UploadingDatabase, err error) { 48 | file, err := os.OpenFile(filepath.Join(config.GetConfigDir(), UploadingFileName), os.O_CREATE|os.O_RDWR, 0777) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | ud = &UploadingDatabase{ 54 | dataFile: file, 55 | } 56 | info, err := file.Stat() 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | if info.Size() <= 0 { 62 | return ud, nil 63 | } 64 | 65 | err = jsonhelper.UnmarshalData(file, ud) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return ud, nil 71 | } 72 | 73 | // Save 保存内容 74 | func (ud *UploadingDatabase) Save() error { 75 | if ud.dataFile == nil { 76 | return errors.New("dataFile is nil") 77 | } 78 | 79 | ud.Timestamp = time.Now().Unix() 80 | 81 | var ( 82 | builder = &strings.Builder{} 83 | err = jsonhelper.MarshalData(builder, ud) 84 | ) 85 | if err != nil { 86 | panic(err) 87 | } 88 | 89 | err = ud.dataFile.Truncate(int64(builder.Len())) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | str := builder.String() 95 | _, err = ud.dataFile.WriteAt(converter.ToBytes(str), 0) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | return nil 101 | } 102 | 103 | // UpdateUploading 更新正在上传 104 | func (ud *UploadingDatabase) UpdateUploading(meta *localfile.LocalFileMeta, state *uploader.InstanceState) { 105 | if meta == nil { 106 | return 107 | } 108 | 109 | meta.CompleteAbsPath() 110 | for k, uploading := range ud.UploadingList { 111 | if uploading.LocalFileMeta == nil { 112 | continue 113 | } 114 | if uploading.LocalFileMeta.EqualLengthMD5(meta) || uploading.LocalFileMeta.Path == meta.Path { 115 | ud.UploadingList[k].State = state 116 | return 117 | } 118 | } 119 | 120 | ud.UploadingList = append(ud.UploadingList, &Uploading{ 121 | LocalFileMeta: meta, 122 | State: state, 123 | }) 124 | } 125 | 126 | func (ud *UploadingDatabase) deleteIndex(k int) { 127 | ud.UploadingList = append(ud.UploadingList[:k], ud.UploadingList[k+1:]...) 128 | } 129 | 130 | // Delete 删除 131 | func (ud *UploadingDatabase) Delete(meta *localfile.LocalFileMeta) bool { 132 | if meta == nil { 133 | return false 134 | } 135 | 136 | meta.CompleteAbsPath() 137 | for k, uploading := range ud.UploadingList { 138 | if uploading.LocalFileMeta == nil { 139 | continue 140 | } 141 | if uploading.LocalFileMeta.EqualLengthMD5(meta) || uploading.LocalFileMeta.Path == meta.Path { 142 | ud.deleteIndex(k) 143 | return true 144 | } 145 | } 146 | return false 147 | } 148 | 149 | // Search 搜索 150 | func (ud *UploadingDatabase) Search(meta *localfile.LocalFileMeta) *uploader.InstanceState { 151 | if meta == nil { 152 | return nil 153 | } 154 | 155 | meta.CompleteAbsPath() 156 | ud.clearModTimeChange() 157 | for _, uploading := range ud.UploadingList { 158 | if uploading.LocalFileMeta == nil { 159 | continue 160 | } 161 | if uploading.LocalFileMeta.EqualLengthMD5(meta) { 162 | return uploading.State 163 | } 164 | if uploading.LocalFileMeta.Path == meta.Path { 165 | // 移除旧的信息 166 | // 目前只是比较了文件大小 167 | if meta.Length != uploading.LocalFileMeta.Length { 168 | ud.Delete(meta) 169 | return nil 170 | } 171 | 172 | // 覆盖数据 173 | meta.MD5 = uploading.LocalFileMeta.MD5 174 | meta.ParentFolderId = uploading.LocalFileMeta.ParentFolderId 175 | meta.UploadFileId = uploading.LocalFileMeta.UploadFileId 176 | meta.FileUploadUrl = uploading.LocalFileMeta.FileUploadUrl 177 | meta.FileCommitUrl = uploading.LocalFileMeta.FileCommitUrl 178 | meta.FileDataExists = uploading.LocalFileMeta.FileDataExists 179 | meta.XRequestId = uploading.LocalFileMeta.XRequestId 180 | return uploading.State 181 | } 182 | } 183 | return nil 184 | } 185 | 186 | func (ud *UploadingDatabase) clearModTimeChange() { 187 | for i := 0; i < len(ud.UploadingList); i++ { 188 | uploading := ud.UploadingList[i] 189 | if uploading.LocalFileMeta == nil { 190 | continue 191 | } 192 | 193 | if uploading.ModTime == -1 { // 忽略 194 | continue 195 | } 196 | 197 | info, err := os.Stat(uploading.LocalFileMeta.Path) 198 | if err != nil { 199 | ud.deleteIndex(i) 200 | i-- 201 | cmdUploadVerbose.Warnf("clear invalid file path: %s, err: %s\n", uploading.LocalFileMeta.Path, err) 202 | continue 203 | } 204 | 205 | if uploading.LocalFileMeta.ModTime != info.ModTime().Unix() { 206 | ud.deleteIndex(i) 207 | i-- 208 | cmdUploadVerbose.Infof("clear modified file path: %s\n", uploading.LocalFileMeta.Path) 209 | continue 210 | } 211 | } 212 | } 213 | 214 | // Close 关闭数据库 215 | func (ud *UploadingDatabase) Close() error { 216 | return ud.dataFile.Close() 217 | } 218 | -------------------------------------------------------------------------------- /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/cloudpan189-go/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 | "github.com/tickstep/cloudpan189-go/internal/config" 18 | "github.com/tickstep/library-go/converter" 19 | "github.com/tickstep/library-go/logger" 20 | ) 21 | 22 | const ( 23 | // MaxUploadBlockSize 最大上传的文件分片大小 24 | MaxUploadBlockSize = 2 * converter.GB 25 | // MinUploadBlockSize 最小的上传的文件分片大小 26 | MinUploadBlockSize = 4 * converter.MB 27 | // MaxRapidUploadSize 秒传文件支持的最大文件大小 28 | MaxRapidUploadSize = 20 * converter.GB 29 | 30 | UploadingFileName = "cloud189_uploading.json" 31 | ) 32 | 33 | var ( 34 | cmdUploadVerbose = logger.New("CLOUD189_UPLOAD", config.EnvVerbose) 35 | ) 36 | 37 | func getBlockSize(fileSize int64) int64 { 38 | blockNum := fileSize / MinUploadBlockSize 39 | if blockNum > 999 { 40 | return fileSize/999 + 1 41 | } 42 | return MinUploadBlockSize 43 | } 44 | -------------------------------------------------------------------------------- /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 | //expires.StripMono(&s.startTime) 39 | } 40 | 41 | func (s *Statistic) Elapsed() time.Duration { 42 | return time.Now().Sub(s.startTime) 43 | } 44 | -------------------------------------------------------------------------------- /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 | // CompleteAbsPath 补齐绝对路径 34 | func (lfm *LocalFileMeta) CompleteAbsPath() { 35 | if filepath.IsAbs(lfm.Path) { 36 | return 37 | } 38 | 39 | absPath, err := filepath.Abs(lfm.Path) 40 | if err != nil { 41 | return 42 | } 43 | // windows 44 | if os.PathSeparator == '\\' { 45 | absPath = strings.ReplaceAll(absPath, "\\", "/") 46 | } 47 | lfm.Path = absPath 48 | } 49 | 50 | // GetFileSum 获取文件的大小, md5, crc32 51 | func GetFileSum(localPath string, flag int) (lfc *LocalFileEntity, err error) { 52 | lfc = NewLocalFileEntity(localPath) 53 | defer lfc.Close() 54 | 55 | err = lfc.OpenPath() 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | err = lfc.Sum(flag) 61 | if err != nil { 62 | return nil, err 63 | } 64 | return lfc, nil 65 | } 66 | -------------------------------------------------------------------------------- /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 | fmt.Printf("Warning: %s\n", err) 27 | return nil 28 | } 29 | 30 | privMode := info.Mode() 31 | 32 | oldPath := filepath.Join(filepath.Dir(targetPath), "old-"+filepath.Base(targetPath)) 33 | 34 | err = os.Rename(targetPath, oldPath) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | newFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY, privMode) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | _, err = io.Copy(newFile, src) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | err = newFile.Close() 50 | if err != nil { 51 | fmt.Printf("Warning: 关闭文件发生错误: %s\n", err) 52 | } 53 | 54 | err = os.Remove(oldPath) 55 | if err != nil { 56 | fmt.Printf("Warning: 移除旧文件发生错误: %s\n", err) 57 | } 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /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/cloudpan189-go/internal/waitgroup" 20 | "strconv" 21 | "sync" 22 | "time" 23 | ) 24 | 25 | type ( 26 | TaskExecutor struct { 27 | incr *incremental.Int // 任务id生成 28 | deque *lane.Deque // 队列 29 | parallel int // 任务的最大并发量 30 | locker sync.Mutex 31 | 32 | // 是否统计失败队列 33 | IsFailedDeque bool 34 | failedDeque *lane.Deque 35 | } 36 | ) 37 | 38 | func NewTaskExecutor() *TaskExecutor { 39 | return &TaskExecutor{} 40 | } 41 | 42 | func (te *TaskExecutor) lazyInit() { 43 | if te.deque == nil { 44 | te.deque = lane.NewDeque() 45 | } 46 | if te.incr == nil { 47 | te.incr = &incremental.Int{} 48 | } 49 | if te.parallel < 1 { 50 | te.parallel = 1 51 | } 52 | if te.IsFailedDeque { 53 | te.failedDeque = lane.NewDeque() 54 | } 55 | } 56 | 57 | // 设置任务的最大并发量 58 | func (te *TaskExecutor) SetParallel(parallel int) { 59 | te.parallel = parallel 60 | } 61 | 62 | //Append 将任务加到任务队列末尾 63 | func (te *TaskExecutor) Append(unit TaskUnit, maxRetry int) *TaskInfo { 64 | te.lazyInit() 65 | taskInfo := &TaskInfo{ 66 | id: strconv.Itoa(te.incr.Next()), 67 | maxRetry: maxRetry, 68 | } 69 | unit.SetTaskInfo(taskInfo) 70 | te.locker.Lock() 71 | te.deque.Append(&TaskInfoItem{ 72 | Info: taskInfo, 73 | Unit: unit, 74 | }) 75 | te.locker.Unlock() 76 | return taskInfo 77 | } 78 | 79 | //AppendNoRetry 将任务加到任务队列末尾, 不重试 80 | func (te *TaskExecutor) AppendNoRetry(unit TaskUnit) { 81 | te.Append(unit, 0) 82 | } 83 | 84 | //Count 返回任务数量 85 | func (te *TaskExecutor) Count() int { 86 | if te.deque == nil { 87 | return 0 88 | } 89 | return te.deque.Size() 90 | } 91 | 92 | //Execute 执行任务 93 | func (te *TaskExecutor) Execute() { 94 | te.lazyInit() 95 | 96 | for { 97 | wg := waitgroup.NewWaitGroup(te.parallel) 98 | for { 99 | te.locker.Lock() 100 | e := te.deque.Shift() 101 | te.locker.Unlock() 102 | if e == nil { // 任务为空 103 | break 104 | } 105 | 106 | // 获取任务 107 | task, ok := e.(*TaskInfoItem) 108 | if !ok { 109 | // type cast failed 110 | } 111 | wg.AddDelta() 112 | 113 | go func(task *TaskInfoItem) { 114 | defer wg.Done() 115 | 116 | result := task.Unit.Run() 117 | 118 | // 返回结果为空 119 | if result == nil { 120 | task.Unit.OnComplete(result) 121 | return 122 | } 123 | 124 | if result.Succeed { 125 | task.Unit.OnSuccess(result) 126 | task.Unit.OnComplete(result) 127 | return 128 | } 129 | 130 | // 需要进行重试 131 | if result.NeedRetry { 132 | // 重试次数超出限制 133 | // 执行失败 134 | if task.Info.IsExceedRetry() { 135 | task.Unit.OnFailed(result) 136 | if te.IsFailedDeque { 137 | // 加入失败队列 138 | te.failedDeque.Append(task) 139 | } 140 | task.Unit.OnComplete(result) 141 | return 142 | } 143 | 144 | task.Info.retry++ // 增加重试次数 145 | task.Unit.OnRetry(result) // 调用重试 146 | task.Unit.OnComplete(result) 147 | 148 | time.Sleep(task.Unit.RetryWait()) // 等待 149 | te.locker.Lock() 150 | te.deque.Append(task) // 重新加入队列末尾 151 | te.locker.Unlock() 152 | return 153 | } 154 | 155 | // 执行失败 156 | task.Unit.OnFailed(result) 157 | if te.IsFailedDeque { 158 | // 加入失败队列 159 | te.failedDeque.Append(task) 160 | } 161 | task.Unit.OnComplete(result) 162 | }(task) 163 | } 164 | 165 | wg.Wait() 166 | 167 | // 没有任务了 168 | if te.deque.Size() == 0 { 169 | break 170 | } 171 | } 172 | } 173 | 174 | //FailedDeque 获取失败队列 175 | func (te *TaskExecutor) FailedDeque() *lane.Deque { 176 | return te.failedDeque 177 | } 178 | 179 | //Stop 停止执行 180 | func (te *TaskExecutor) Stop() { 181 | 182 | } 183 | 184 | //Pause 暂停执行 185 | func (te *TaskExecutor) Pause() { 186 | 187 | } 188 | 189 | //Resume 恢复执行 190 | func (te *TaskExecutor) Resume() { 191 | } 192 | -------------------------------------------------------------------------------- /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 | // 执行任务 22 | Run() (result *TaskUnitRunResult) 23 | // 重试任务执行的方法 24 | // 当达到最大重试次数, 执行失败 25 | OnRetry(lastRunResult *TaskUnitRunResult) 26 | // 每次执行成功执行的方法 27 | OnSuccess(lastRunResult *TaskUnitRunResult) 28 | // 每次执行失败执行的方法 29 | OnFailed(lastRunResult *TaskUnitRunResult) 30 | // 每次执行结束执行的方法, 不管成功失败 31 | OnComplete(lastRunResult *TaskUnitRunResult) 32 | // 重试等待的时间 33 | RetryWait() time.Duration 34 | } 35 | 36 | // 任务单元执行结果 37 | TaskUnitRunResult struct { 38 | Succeed bool // 是否执行成功 39 | NeedRetry bool // 是否需要重试 40 | 41 | // 以下是额外的信息 42 | Err error // 错误信息 43 | ResultCode int // 结果代码 44 | ResultMessage string // 结果描述 45 | Extra interface{} // 额外的信息 46 | } 47 | ) 48 | 49 | var ( 50 | // TaskUnitRunResultSuccess 任务执行成功 51 | TaskUnitRunResultSuccess = &TaskUnitRunResult{} 52 | ) 53 | -------------------------------------------------------------------------------- /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/cloudpan189-go/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) OnRetry(lastRunResult *taskframework.TaskUnitRunResult) { 55 | fmt.Printf("[%s] prepare retry, times [%d/%d]...\n", tu.taskInfo.Id(), tu.taskInfo.Retry(), tu.taskInfo.MaxRetry()) 56 | } 57 | 58 | func (tu *TestUnit) RetryWait() time.Duration { 59 | return 1 * time.Second 60 | } 61 | 62 | func TestTaskExecutor(t *testing.T) { 63 | te := taskframework.NewTaskExecutor() 64 | te.SetParallel(2) 65 | for i := 0; i < 3; i++ { 66 | tu := TestUnit{ 67 | retry: false, 68 | } 69 | te.Append(&tu, 2) 70 | } 71 | te.Execute() 72 | } 73 | -------------------------------------------------------------------------------- /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.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 utils 15 | 16 | import ( 17 | "compress/gzip" 18 | "flag" 19 | "io" 20 | "io/ioutil" 21 | "net/http/cookiejar" 22 | "net/url" 23 | "path" 24 | "regexp" 25 | "strconv" 26 | "strings" 27 | ) 28 | 29 | // TrimPathPrefix 去除目录的前缀 30 | func TrimPathPrefix(path, prefixPath string) string { 31 | if prefixPath == "/" { 32 | return path 33 | } 34 | return strings.TrimPrefix(path, prefixPath) 35 | } 36 | 37 | // ContainsString 检测字符串是否在字符串数组里 38 | func ContainsString(ss []string, s string) bool { 39 | for k := range ss { 40 | if ss[k] == s { 41 | return true 42 | } 43 | } 44 | return false 45 | } 46 | 47 | // GetURLCookieString 返回cookie字串 48 | func GetURLCookieString(urlString string, jar *cookiejar.Jar) string { 49 | u, _ := url.Parse(urlString) 50 | cookies := jar.Cookies(u) 51 | cookieString := "" 52 | for _, v := range cookies { 53 | cookieString += v.String() + "; " 54 | } 55 | cookieString = strings.TrimRight(cookieString, "; ") 56 | return cookieString 57 | } 58 | 59 | // DecompressGZIP 对 io.Reader 数据, 进行 gzip 解压 60 | func DecompressGZIP(r io.Reader) ([]byte, error) { 61 | gzipReader, err := gzip.NewReader(r) 62 | if err != nil { 63 | return nil, err 64 | } 65 | gzipReader.Close() 66 | return ioutil.ReadAll(gzipReader) 67 | } 68 | 69 | // FlagProvided 检测命令行是否提供名为 name 的 flag, 支持多个name(names) 70 | func FlagProvided(names ...string) bool { 71 | if len(names) == 0 { 72 | return false 73 | } 74 | var targetFlag *flag.Flag 75 | for _, name := range names { 76 | targetFlag = flag.Lookup(name) 77 | if targetFlag == nil { 78 | return false 79 | } 80 | if targetFlag.DefValue == targetFlag.Value.String() { 81 | return false 82 | } 83 | } 84 | return true 85 | } 86 | 87 | // Trigger 用于触发事件 88 | func Trigger(f func()) { 89 | if f == nil { 90 | return 91 | } 92 | go f() 93 | } 94 | 95 | // TriggerOnSync 用于触发事件, 同步触发 96 | func TriggerOnSync(f func()) { 97 | if f == nil { 98 | return 99 | } 100 | f() 101 | } 102 | 103 | func ParseVersionNum(versionStr string) int { 104 | versionStr = strings.ReplaceAll(versionStr, "-dev", "") 105 | versionStr = strings.ReplaceAll(versionStr, "v", "") 106 | versionParts := strings.Split(versionStr, ".") 107 | verNum := parseInt(versionParts[0])*1e4 + parseInt(versionParts[1])*1e2 + parseInt(versionParts[2]) 108 | return verNum 109 | } 110 | func parseInt(numStr string) int { 111 | num, e := strconv.Atoi(numStr) 112 | if e != nil { 113 | return 0 114 | } 115 | return num 116 | } 117 | 118 | // IsExcludeFile 是否是指定排除的文件 119 | func IsExcludeFile(filePath string, excludeNames *[]string) bool { 120 | if excludeNames == nil || len(*excludeNames) == 0 { 121 | return false 122 | } 123 | 124 | for _, pattern := range *excludeNames { 125 | fileName := path.Base(strings.ReplaceAll(filePath, "\\", "/")) 126 | m, _ := regexp.MatchString(pattern, fileName) 127 | if m { 128 | return true 129 | } 130 | } 131 | return false 132 | } 133 | -------------------------------------------------------------------------------- /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/crypto/crypto.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 crypto 15 | 16 | import ( 17 | "fmt" 18 | "github.com/tickstep/library-go/archive" 19 | "github.com/tickstep/library-go/crypto" 20 | "io" 21 | "os" 22 | "strings" 23 | ) 24 | 25 | // CryptoMethodSupport 检测是否支持加密解密方法 26 | func CryptoMethodSupport(method string) bool { 27 | switch method { 28 | case "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "aes-128-ofb", "aes-192-ofb", "aes-256-ofb": 29 | return true 30 | } 31 | 32 | return false 33 | } 34 | 35 | // EncryptFile 加密本地文件 36 | func EncryptFile(method string, key []byte, filePath string, isGzip bool) (encryptedFilePath string, err error) { 37 | if !CryptoMethodSupport(method) { 38 | return "", fmt.Errorf("unknown encrypt method: %s", method) 39 | } 40 | 41 | if isGzip { 42 | err = archive.GZIPCompressFile(filePath) 43 | if err != nil { 44 | return 45 | } 46 | } 47 | 48 | plainFile, err := os.OpenFile(filePath, os.O_RDONLY, 0) 49 | if err != nil { 50 | return 51 | } 52 | 53 | defer plainFile.Close() 54 | 55 | var cipherReader io.Reader 56 | switch method { 57 | case "aes-128-ctr": 58 | cipherReader, err = crypto.Aes128CTREncrypt(crypto.Convert16bytes(key), plainFile) 59 | case "aes-192-ctr": 60 | cipherReader, err = crypto.Aes192CTREncrypt(crypto.Convert24bytes(key), plainFile) 61 | case "aes-256-ctr": 62 | cipherReader, err = crypto.Aes256CTREncrypt(crypto.Convert32bytes(key), plainFile) 63 | case "aes-128-cfb": 64 | cipherReader, err = crypto.Aes128CFBEncrypt(crypto.Convert16bytes(key), plainFile) 65 | case "aes-192-cfb": 66 | cipherReader, err = crypto.Aes192CFBEncrypt(crypto.Convert24bytes(key), plainFile) 67 | case "aes-256-cfb": 68 | cipherReader, err = crypto.Aes256CFBEncrypt(crypto.Convert32bytes(key), plainFile) 69 | case "aes-128-ofb": 70 | cipherReader, err = crypto.Aes128OFBEncrypt(crypto.Convert16bytes(key), plainFile) 71 | case "aes-192-ofb": 72 | cipherReader, err = crypto.Aes192OFBEncrypt(crypto.Convert24bytes(key), plainFile) 73 | case "aes-256-ofb": 74 | cipherReader, err = crypto.Aes256OFBEncrypt(crypto.Convert32bytes(key), plainFile) 75 | default: 76 | return "", fmt.Errorf("unknown encrypt method: %s", method) 77 | } 78 | 79 | if err != nil { 80 | return 81 | } 82 | 83 | plainFileInfo, err := plainFile.Stat() 84 | if err != nil { 85 | return 86 | } 87 | 88 | encryptedFilePath = filePath + ".encrypt" 89 | encryptedFile, err := os.OpenFile(encryptedFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, plainFileInfo.Mode()) 90 | if err != nil { 91 | return 92 | } 93 | 94 | defer encryptedFile.Close() 95 | 96 | _, err = io.Copy(encryptedFile, cipherReader) 97 | if err != nil { 98 | return 99 | } 100 | 101 | os.Remove(filePath) 102 | 103 | return encryptedFilePath, nil 104 | } 105 | 106 | // DecryptFile 加密本地文件 107 | func DecryptFile(method string, key []byte, filePath string, isGzip bool) (decryptedFilePath string, err error) { 108 | if !CryptoMethodSupport(method) { 109 | return "", fmt.Errorf("unknown decrypt method: %s", method) 110 | } 111 | 112 | cipherFile, err := os.OpenFile(filePath, os.O_RDONLY, 0644) 113 | if err != nil { 114 | return 115 | } 116 | 117 | var plainReader io.Reader 118 | switch method { 119 | case "aes-128-ctr": 120 | plainReader, err = crypto.Aes128CTRDecrypt(crypto.Convert16bytes(key), cipherFile) 121 | case "aes-192-ctr": 122 | plainReader, err = crypto.Aes192CTRDecrypt(crypto.Convert24bytes(key), cipherFile) 123 | case "aes-256-ctr": 124 | plainReader, err = crypto.Aes256CTRDecrypt(crypto.Convert32bytes(key), cipherFile) 125 | case "aes-128-cfb": 126 | plainReader, err = crypto.Aes128CFBDecrypt(crypto.Convert16bytes(key), cipherFile) 127 | case "aes-192-cfb": 128 | plainReader, err = crypto.Aes192CFBDecrypt(crypto.Convert24bytes(key), cipherFile) 129 | case "aes-256-cfb": 130 | plainReader, err = crypto.Aes256CFBDecrypt(crypto.Convert32bytes(key), cipherFile) 131 | case "aes-128-ofb": 132 | plainReader, err = crypto.Aes128OFBDecrypt(crypto.Convert16bytes(key), cipherFile) 133 | case "aes-192-ofb": 134 | plainReader, err = crypto.Aes192OFBDecrypt(crypto.Convert24bytes(key), cipherFile) 135 | case "aes-256-ofb": 136 | plainReader, err = crypto.Aes256OFBDecrypt(crypto.Convert32bytes(key), cipherFile) 137 | default: 138 | return "", fmt.Errorf("unknown decrypt method: %s", method) 139 | } 140 | 141 | if err != nil { 142 | return 143 | } 144 | 145 | cipherFileInfo, err := cipherFile.Stat() 146 | if err != nil { 147 | return 148 | } 149 | 150 | decryptedFilePath = strings.TrimSuffix(filePath, ".encrypt") 151 | decryptedTmpFilePath := decryptedFilePath + ".decrypted" 152 | decryptedTmpFile, err := os.OpenFile(decryptedTmpFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, cipherFileInfo.Mode()) 153 | if err != nil { 154 | return 155 | } 156 | 157 | _, err = io.Copy(decryptedTmpFile, plainReader) 158 | if err != nil { 159 | return 160 | } 161 | 162 | decryptedTmpFile.Close() 163 | cipherFile.Close() 164 | 165 | if isGzip { 166 | err = archive.GZIPUnompressFile(decryptedTmpFilePath) 167 | if err != nil { 168 | os.Remove(decryptedTmpFilePath) 169 | return 170 | } 171 | 172 | // 删除已加密的文件 173 | os.Remove(filePath) 174 | } 175 | 176 | if filePath != decryptedFilePath { 177 | os.Rename(decryptedTmpFilePath, decryptedFilePath) 178 | } else { 179 | decryptedFilePath = decryptedTmpFilePath 180 | } 181 | 182 | return decryptedFilePath, nil 183 | } 184 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /library/requester/transfer/rangelist.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 | "errors" 18 | "fmt" 19 | "github.com/tickstep/library-go/converter" 20 | "sync" 21 | "sync/atomic" 22 | ) 23 | type ( 24 | Range struct { 25 | Begin int64 `json:"begin,omitempty"` 26 | End int64 `json:"end,omitempty"` 27 | } 28 | 29 | // RangeGenMode 线程分配方式 30 | RangeGenMode int32 31 | 32 | //RangeList 请求范围列表 33 | RangeList []*Range 34 | 35 | //RangeListGen Range 生成器 36 | RangeListGen struct { 37 | total int64 38 | begin int64 39 | blockSize int64 40 | parallel int 41 | count int // 已生成次数 42 | rangeGenMode RangeGenMode 43 | mu sync.Mutex 44 | } 45 | ) 46 | 47 | const ( 48 | // DefaultBlockSize 默认的BlockSize 49 | DefaultBlockSize = 256 * converter.KB 50 | 51 | // RangeGenMode_Default 根据parallel平均生成 52 | RangeGenMode_Default RangeGenMode = 0 53 | // RangeGenMode_BlockSize 根据blockSize生成 54 | RangeGenMode_BlockSize RangeGenMode = 1 55 | ) 56 | 57 | var ( 58 | // ErrUnknownRangeGenMode RangeGenMode 非法 59 | ErrUnknownRangeGenMode = errors.New("Unknown RangeGenMode") 60 | ) 61 | 62 | //Len 长度 63 | func (r *Range) Len() int64 { 64 | return r.LoadEnd() - r.LoadBegin() 65 | } 66 | 67 | //LoadBegin 读取Begin, 原子操作 68 | func (r *Range) LoadBegin() int64 { 69 | return atomic.LoadInt64(&r.Begin) 70 | } 71 | 72 | //AddBegin 增加Begin, 原子操作 73 | func (r *Range) AddBegin(i int64) (newi int64) { 74 | return atomic.AddInt64(&r.Begin, i) 75 | } 76 | 77 | //LoadEnd 读取End, 原子操作 78 | func (r *Range) LoadEnd() int64 { 79 | return atomic.LoadInt64(&r.End) 80 | } 81 | 82 | //StoreBegin 储存End, 原子操作 83 | func (r *Range) StoreBegin(end int64) { 84 | atomic.StoreInt64(&r.Begin, end) 85 | } 86 | 87 | //StoreEnd 储存End, 原子操作 88 | func (r *Range) StoreEnd(end int64) { 89 | atomic.StoreInt64(&r.End, end) 90 | } 91 | 92 | // ShowDetails 显示Range细节 93 | func (r *Range) ShowDetails() string { 94 | return fmt.Sprintf("{%d-%d}", r.LoadBegin(), r.LoadEnd()) 95 | } 96 | 97 | //Len 获取所有的Range的剩余长度 98 | func (rl *RangeList) Len() int64 { 99 | var l int64 100 | for _, wrange := range *rl { 101 | if wrange == nil { 102 | continue 103 | } 104 | l += wrange.Len() 105 | } 106 | return l 107 | } 108 | 109 | // NewRangeListGenDefault 初始化默认Range生成器, 根据parallel平均生成 110 | func NewRangeListGenDefault(totalSize, begin int64, count, parallel int) *RangeListGen { 111 | return &RangeListGen{ 112 | total: totalSize, 113 | begin: begin, 114 | parallel: parallel, 115 | count: count, 116 | rangeGenMode: RangeGenMode_Default, 117 | } 118 | } 119 | 120 | // NewRangeListGenBlockSize 初始化Range生成器, 根据blockSize生成 121 | func NewRangeListGenBlockSize(totalSize, begin, blockSize int64) *RangeListGen { 122 | return &RangeListGen{ 123 | total: totalSize, 124 | begin: begin, 125 | blockSize: blockSize, 126 | rangeGenMode: RangeGenMode_BlockSize, 127 | } 128 | } 129 | 130 | // RangeGenMode 返回Range生成方式 131 | func (gen *RangeListGen) RangeGenMode() RangeGenMode { 132 | return gen.rangeGenMode 133 | } 134 | 135 | // RangeCount 返回预计生成的Range数量 136 | func (gen *RangeListGen) RangeCount() (rangeCount int) { 137 | switch gen.rangeGenMode { 138 | case RangeGenMode_Default: 139 | rangeCount = gen.parallel - gen.count 140 | case RangeGenMode_BlockSize: 141 | rangeCount = int((gen.total - gen.begin) / gen.blockSize) 142 | if gen.total%gen.blockSize != 0 { 143 | rangeCount++ 144 | } 145 | } 146 | return 147 | } 148 | 149 | // LoadBegin 返回begin 150 | func (gen *RangeListGen) LoadBegin() (begin int64) { 151 | gen.mu.Lock() 152 | begin = gen.begin 153 | gen.mu.Unlock() 154 | return 155 | } 156 | 157 | // LoadBlockSize 返回blockSize 158 | func (gen *RangeListGen) LoadBlockSize() (blockSize int64) { 159 | switch gen.rangeGenMode { 160 | case RangeGenMode_Default: 161 | if gen.blockSize <= 0 { 162 | gen.blockSize = (gen.total - gen.begin) / int64(gen.parallel) 163 | } 164 | blockSize = gen.blockSize 165 | case RangeGenMode_BlockSize: 166 | blockSize = gen.blockSize 167 | } 168 | return 169 | } 170 | 171 | // IsDone 是否已分配完成 172 | func (gen *RangeListGen) IsDone() bool { 173 | return gen.begin >= gen.total 174 | } 175 | 176 | // GenRange 生成 Range 177 | func (gen *RangeListGen) GenRange() (index int, r *Range) { 178 | var ( 179 | end int64 180 | ) 181 | if gen.parallel < 1 { 182 | gen.parallel = 1 183 | } 184 | switch gen.rangeGenMode { 185 | case RangeGenMode_Default: 186 | gen.LoadBlockSize() 187 | gen.mu.Lock() 188 | defer gen.mu.Unlock() 189 | 190 | if gen.IsDone() { 191 | return gen.count, nil 192 | } 193 | 194 | gen.count++ 195 | if gen.count >= gen.parallel { 196 | end = gen.total 197 | } else { 198 | end = gen.begin + gen.blockSize 199 | } 200 | r = &Range{ 201 | Begin: gen.begin, 202 | End: end, 203 | } 204 | 205 | gen.begin = end 206 | index = gen.count - 1 207 | return 208 | case RangeGenMode_BlockSize: 209 | if gen.blockSize <= 0 { 210 | gen.blockSize = DefaultBlockSize 211 | } 212 | gen.mu.Lock() 213 | defer gen.mu.Unlock() 214 | 215 | if gen.IsDone() { 216 | return gen.count, nil 217 | } 218 | 219 | gen.count++ 220 | end = gen.begin + gen.blockSize 221 | if end >= gen.total { 222 | end = gen.total 223 | } 224 | r = &Range{ 225 | Begin: gen.begin, 226 | End: end, 227 | } 228 | gen.begin = end 229 | index = gen.count - 1 230 | return 231 | } 232 | 233 | return 0, nil 234 | } 235 | -------------------------------------------------------------------------------- /package/debian/Packages.sh: -------------------------------------------------------------------------------- 1 | dpkg-scanpackages . | gzip > Packages.gz -------------------------------------------------------------------------------- /package/debian/linux-amd64/control: -------------------------------------------------------------------------------- 1 | Package: cloudpan189-go 2 | Version: 0.1.1 3 | Homepage: https://github.com/tickstep/cloudpan189-go 4 | Section: utils 5 | Priority: optional 6 | Architecture: amd64 7 | Installed-Size: 4096 8 | Maintainer: tickstep 9 | Description: cloudpan189-go 使用Go语言编写的天翼云盘命令行客户端, 为操作天翼云盘, 提供实用功能. -------------------------------------------------------------------------------- /resource_windows_386.syso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tickstep/cloudpan189-go/58c99009978e9dd4761cff5632df31db601562f9/resource_windows_386.syso -------------------------------------------------------------------------------- /resource_windows_amd64.syso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tickstep/cloudpan189-go/58c99009978e9dd4761cff5632df31db601562f9/resource_windows_amd64.syso -------------------------------------------------------------------------------- /versioninfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "FixedFileInfo": { 3 | "FileVersion": { 4 | "Major": 0, 5 | "Minor": 1, 6 | "Patch": 3, 7 | "Build": 0 8 | }, 9 | "ProductVersion": { 10 | "Major": 0, 11 | "Minor": 1, 12 | "Patch": 3, 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.1.3", 26 | "InternalName": "", 27 | "LegalCopyright": "© 2020-2023 tickstep.", 28 | "LegalTrademarks": "", 29 | "OriginalFilename": "", 30 | "PrivateBuild": "", 31 | "ProductName": "cloudpan189-go", 32 | "ProductVersion": "v0.1.3", 33 | "SpecialBuild": "" 34 | }, 35 | "VarFileInfo": { 36 | "Translation": { 37 | "LangID": "0409", 38 | "CharsetID": "04B0" 39 | } 40 | }, 41 | "IconPath": "assets/cloudpan189-go.ico", 42 | "ManifestPath": "cloudpan189-go.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=cloudpan189-go 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%-windows-arm windows arm 18 | call :build_task %name%-%version%-linux-386 linux 386 19 | call :build_task %name%-%version%-linux-amd64 linux amd64 20 | call :build_task %name%-%version%-darwin-macos-amd64 darwin amd64 21 | call :build_task %name%-%version%-darwin-macos-arm64 darwin arm64 22 | 23 | goto:EOF 24 | 25 | REM ============= build function ================ 26 | :build_task 27 | setlocal 28 | 29 | set targetName=%1 30 | set GOOS=%2 31 | set GOARCH=%3 32 | set goarm=%4 33 | set GO386=sse2 34 | set CGO_ENABLED=0 35 | set GOARM=%goarm% 36 | 37 | echo "Building %targetName% ..." 38 | if %GOOS% == windows ( 39 | goversioninfo -o=resource_windows_386.syso 40 | goversioninfo -64 -o=resource_windows_amd64.syso 41 | go build -ldflags "-linkmode internal -X main.Version=%version% -s -w" -o "%output%/%1/%name%.exe" 42 | ) ^ 43 | else ( 44 | go build -ldflags "-X main.Version=%version% -s -w" -o "%output%/%1/%name%" 45 | ) 46 | 47 | copy README.md %output%\%1 48 | 49 | endlocal 50 | 51 | --------------------------------------------------------------------------------