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