Mr.L commited on
Commit
7107f0b
·
1 Parent(s): ff2ab3b

feat: add full alist source code for Docker build

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
cmd/admin.go ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
3
+ */
4
+ package cmd
5
+
6
+ import (
7
+ "github.com/alist-org/alist/v3/internal/conf"
8
+ "github.com/alist-org/alist/v3/internal/op"
9
+ "github.com/alist-org/alist/v3/internal/setting"
10
+ "github.com/alist-org/alist/v3/pkg/utils"
11
+ "github.com/alist-org/alist/v3/pkg/utils/random"
12
+ "github.com/spf13/cobra"
13
+ )
14
+
15
+ // AdminCmd represents the password command
16
+ var AdminCmd = &cobra.Command{
17
+ Use: "admin",
18
+ Aliases: []string{"password"},
19
+ Short: "Show admin user's info and some operations about admin user's password",
20
+ Run: func(cmd *cobra.Command, args []string) {
21
+ Init()
22
+ defer Release()
23
+ admin, err := op.GetAdmin()
24
+ if err != nil {
25
+ utils.Log.Errorf("failed get admin user: %+v", err)
26
+ } else {
27
+ utils.Log.Infof("Admin user's username: %s", admin.Username)
28
+ utils.Log.Infof("The password can only be output at the first startup, and then stored as a hash value, which cannot be reversed")
29
+ utils.Log.Infof("You can reset the password with a random string by running [alist admin random]")
30
+ utils.Log.Infof("You can also set a new password by running [alist admin set NEW_PASSWORD]")
31
+ }
32
+ },
33
+ }
34
+
35
+ var RandomPasswordCmd = &cobra.Command{
36
+ Use: "random",
37
+ Short: "Reset admin user's password to a random string",
38
+ Run: func(cmd *cobra.Command, args []string) {
39
+ newPwd := random.String(8)
40
+ setAdminPassword(newPwd)
41
+ },
42
+ }
43
+
44
+ var SetPasswordCmd = &cobra.Command{
45
+ Use: "set",
46
+ Short: "Set admin user's password",
47
+ Run: func(cmd *cobra.Command, args []string) {
48
+ if len(args) == 0 {
49
+ utils.Log.Errorf("Please enter the new password")
50
+ return
51
+ }
52
+ setAdminPassword(args[0])
53
+ },
54
+ }
55
+
56
+ var ShowTokenCmd = &cobra.Command{
57
+ Use: "token",
58
+ Short: "Show admin token",
59
+ Run: func(cmd *cobra.Command, args []string) {
60
+ Init()
61
+ defer Release()
62
+ token := setting.GetStr(conf.Token)
63
+ utils.Log.Infof("Admin token: %s", token)
64
+ },
65
+ }
66
+
67
+ func setAdminPassword(pwd string) {
68
+ Init()
69
+ defer Release()
70
+ admin, err := op.GetAdmin()
71
+ if err != nil {
72
+ utils.Log.Errorf("failed get admin user: %+v", err)
73
+ return
74
+ }
75
+ admin.SetPassword(pwd)
76
+ if err := op.UpdateUser(admin); err != nil {
77
+ utils.Log.Errorf("failed update admin user: %+v", err)
78
+ return
79
+ }
80
+ utils.Log.Infof("admin user has been updated:")
81
+ utils.Log.Infof("username: %s", admin.Username)
82
+ utils.Log.Infof("password: %s", pwd)
83
+ DelAdminCacheOnline()
84
+ }
85
+
86
+ func init() {
87
+ RootCmd.AddCommand(AdminCmd)
88
+ AdminCmd.AddCommand(RandomPasswordCmd)
89
+ AdminCmd.AddCommand(SetPasswordCmd)
90
+ AdminCmd.AddCommand(ShowTokenCmd)
91
+ // Here you will define your flags and configuration settings.
92
+
93
+ // Cobra supports Persistent Flags which will work for this command
94
+ // and all subcommands, e.g.:
95
+ // passwordCmd.PersistentFlags().String("foo", "", "A help for foo")
96
+
97
+ // Cobra supports local flags which will only run when this command
98
+ // is called directly, e.g.:
99
+ // passwordCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
100
+ }
cmd/cancel2FA.go ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
3
+ */
4
+ package cmd
5
+
6
+ import (
7
+ "github.com/alist-org/alist/v3/internal/op"
8
+ "github.com/alist-org/alist/v3/pkg/utils"
9
+ "github.com/spf13/cobra"
10
+ )
11
+
12
+ // Cancel2FACmd represents the delete2fa command
13
+ var Cancel2FACmd = &cobra.Command{
14
+ Use: "cancel2fa",
15
+ Short: "Delete 2FA of admin user",
16
+ Run: func(cmd *cobra.Command, args []string) {
17
+ Init()
18
+ defer Release()
19
+ admin, err := op.GetAdmin()
20
+ if err != nil {
21
+ utils.Log.Errorf("failed to get admin user: %+v", err)
22
+ } else {
23
+ err := op.Cancel2FAByUser(admin)
24
+ if err != nil {
25
+ utils.Log.Errorf("failed to cancel 2FA: %+v", err)
26
+ } else {
27
+ utils.Log.Info("2FA canceled")
28
+ DelAdminCacheOnline()
29
+ }
30
+ }
31
+ },
32
+ }
33
+
34
+ func init() {
35
+ RootCmd.AddCommand(Cancel2FACmd)
36
+
37
+ // Here you will define your flags and configuration settings.
38
+
39
+ // Cobra supports Persistent Flags which will work for this command
40
+ // and all subcommands, e.g.:
41
+ // cancel2FACmd.PersistentFlags().String("foo", "", "A help for foo")
42
+
43
+ // Cobra supports local flags which will only run when this command
44
+ // is called directly, e.g.:
45
+ // cancel2FACmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
46
+ }
cmd/common.go ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package cmd
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "strconv"
7
+
8
+ "github.com/alist-org/alist/v3/internal/bootstrap"
9
+ "github.com/alist-org/alist/v3/internal/bootstrap/data"
10
+ "github.com/alist-org/alist/v3/internal/db"
11
+ "github.com/alist-org/alist/v3/pkg/utils"
12
+ log "github.com/sirupsen/logrus"
13
+ )
14
+
15
+ func Init() {
16
+ bootstrap.InitConfig()
17
+ bootstrap.Log()
18
+ bootstrap.InitDB()
19
+ data.InitData()
20
+ bootstrap.InitIndex()
21
+ }
22
+
23
+ func Release() {
24
+ db.Close()
25
+ }
26
+
27
+ var pid = -1
28
+ var pidFile string
29
+
30
+ func initDaemon() {
31
+ ex, err := os.Executable()
32
+ if err != nil {
33
+ log.Fatal(err)
34
+ }
35
+ exPath := filepath.Dir(ex)
36
+ _ = os.MkdirAll(filepath.Join(exPath, "daemon"), 0700)
37
+ pidFile = filepath.Join(exPath, "daemon/pid")
38
+ if utils.Exists(pidFile) {
39
+ bytes, err := os.ReadFile(pidFile)
40
+ if err != nil {
41
+ log.Fatal("failed to read pid file", err)
42
+ }
43
+ id, err := strconv.Atoi(string(bytes))
44
+ if err != nil {
45
+ log.Fatal("failed to parse pid data", err)
46
+ }
47
+ pid = id
48
+ }
49
+ }
cmd/flags/config.go ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ package flags
2
+
3
+ var (
4
+ DataDir string
5
+ Debug bool
6
+ NoPrefix bool
7
+ Dev bool
8
+ ForceBinDir bool
9
+ LogStd bool
10
+ )
cmd/lang.go ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Package cmd
3
+ Copyright © 2022 Noah Hsu<[email protected]>
4
+ */
5
+ package cmd
6
+
7
+ import (
8
+ "fmt"
9
+ "io"
10
+ "os"
11
+ "reflect"
12
+ "strings"
13
+
14
+ _ "github.com/alist-org/alist/v3/drivers"
15
+ "github.com/alist-org/alist/v3/internal/bootstrap/data"
16
+ "github.com/alist-org/alist/v3/internal/conf"
17
+ "github.com/alist-org/alist/v3/internal/op"
18
+ "github.com/alist-org/alist/v3/pkg/utils"
19
+ log "github.com/sirupsen/logrus"
20
+ "github.com/spf13/cobra"
21
+ )
22
+
23
+ type KV[V any] map[string]V
24
+
25
+ type Drivers KV[KV[interface{}]]
26
+
27
+ func firstUpper(s string) string {
28
+ if s == "" {
29
+ return ""
30
+ }
31
+ return strings.ToUpper(s[:1]) + s[1:]
32
+ }
33
+
34
+ func convert(s string) string {
35
+ ss := strings.Split(s, "_")
36
+ ans := strings.Join(ss, " ")
37
+ return firstUpper(ans)
38
+ }
39
+
40
+ func writeFile(name string, data interface{}) {
41
+ f, err := os.Open(fmt.Sprintf("../alist-web/src/lang/en/%s.json", name))
42
+ if err != nil {
43
+ log.Errorf("failed to open %s.json: %+v", name, err)
44
+ return
45
+ }
46
+ defer f.Close()
47
+ content, err := io.ReadAll(f)
48
+ if err != nil {
49
+ log.Errorf("failed to read %s.json: %+v", name, err)
50
+ return
51
+ }
52
+ oldData := make(map[string]interface{})
53
+ newData := make(map[string]interface{})
54
+ err = utils.Json.Unmarshal(content, &oldData)
55
+ if err != nil {
56
+ log.Errorf("failed to unmarshal %s.json: %+v", name, err)
57
+ return
58
+ }
59
+ content, err = utils.Json.Marshal(data)
60
+ if err != nil {
61
+ log.Errorf("failed to marshal json: %+v", err)
62
+ return
63
+ }
64
+ err = utils.Json.Unmarshal(content, &newData)
65
+ if err != nil {
66
+ log.Errorf("failed to unmarshal json: %+v", err)
67
+ return
68
+ }
69
+ if reflect.DeepEqual(oldData, newData) {
70
+ log.Infof("%s.json no changed, skip", name)
71
+ } else {
72
+ log.Infof("%s.json changed, update file", name)
73
+ //log.Infof("old: %+v\nnew:%+v", oldData, data)
74
+ utils.WriteJsonToFile(fmt.Sprintf("lang/%s.json", name), newData, true)
75
+ }
76
+ }
77
+
78
+ func generateDriversJson() {
79
+ drivers := make(Drivers)
80
+ drivers["drivers"] = make(KV[interface{}])
81
+ drivers["config"] = make(KV[interface{}])
82
+ driverInfoMap := op.GetDriverInfoMap()
83
+ for k, v := range driverInfoMap {
84
+ drivers["drivers"][k] = convert(k)
85
+ items := make(KV[interface{}])
86
+ config := map[string]string{}
87
+ if v.Config.Alert != "" {
88
+ alert := strings.SplitN(v.Config.Alert, "|", 2)
89
+ if len(alert) > 1 {
90
+ config["alert"] = alert[1]
91
+ }
92
+ }
93
+ drivers["config"][k] = config
94
+ for i := range v.Additional {
95
+ item := v.Additional[i]
96
+ items[item.Name] = convert(item.Name)
97
+ if item.Help != "" {
98
+ items[fmt.Sprintf("%s-tips", item.Name)] = item.Help
99
+ }
100
+ if item.Type == conf.TypeSelect && len(item.Options) > 0 {
101
+ options := make(KV[string])
102
+ _options := strings.Split(item.Options, ",")
103
+ for _, o := range _options {
104
+ options[o] = convert(o)
105
+ }
106
+ items[fmt.Sprintf("%ss", item.Name)] = options
107
+ }
108
+ }
109
+ drivers[k] = items
110
+ }
111
+ writeFile("drivers", drivers)
112
+ }
113
+
114
+ func generateSettingsJson() {
115
+ settings := data.InitialSettings()
116
+ settingsLang := make(KV[any])
117
+ for _, setting := range settings {
118
+ settingsLang[setting.Key] = convert(setting.Key)
119
+ if setting.Help != "" {
120
+ settingsLang[fmt.Sprintf("%s-tips", setting.Key)] = setting.Help
121
+ }
122
+ if setting.Type == conf.TypeSelect && len(setting.Options) > 0 {
123
+ options := make(KV[string])
124
+ _options := strings.Split(setting.Options, ",")
125
+ for _, o := range _options {
126
+ options[o] = convert(o)
127
+ }
128
+ settingsLang[fmt.Sprintf("%ss", setting.Key)] = options
129
+ }
130
+ }
131
+ writeFile("settings", settingsLang)
132
+ //utils.WriteJsonToFile("lang/settings.json", settingsLang)
133
+ }
134
+
135
+ // LangCmd represents the lang command
136
+ var LangCmd = &cobra.Command{
137
+ Use: "lang",
138
+ Short: "Generate language json file",
139
+ Run: func(cmd *cobra.Command, args []string) {
140
+ err := os.MkdirAll("lang", 0777)
141
+ if err != nil {
142
+ utils.Log.Fatal("failed create folder: %s", err.Error())
143
+ }
144
+ generateDriversJson()
145
+ generateSettingsJson()
146
+ },
147
+ }
148
+
149
+ func init() {
150
+ RootCmd.AddCommand(LangCmd)
151
+
152
+ // Here you will define your flags and configuration settings.
153
+
154
+ // Cobra supports Persistent Flags which will work for this command
155
+ // and all subcommands, e.g.:
156
+ // langCmd.PersistentFlags().String("foo", "", "A help for foo")
157
+
158
+ // Cobra supports local flags which will only run when this command
159
+ // is called directly, e.g.:
160
+ // langCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
161
+ }
cmd/restart.go ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
3
+ */
4
+ package cmd
5
+
6
+ import (
7
+ "github.com/spf13/cobra"
8
+ )
9
+
10
+ // RestartCmd represents the restart command
11
+ var RestartCmd = &cobra.Command{
12
+ Use: "restart",
13
+ Short: "Restart alist server by daemon/pid file",
14
+ Run: func(cmd *cobra.Command, args []string) {
15
+ stop()
16
+ start()
17
+ },
18
+ }
19
+
20
+ func init() {
21
+ RootCmd.AddCommand(RestartCmd)
22
+
23
+ // Here you will define your flags and configuration settings.
24
+
25
+ // Cobra supports Persistent Flags which will work for this command
26
+ // and all subcommands, e.g.:
27
+ // restartCmd.PersistentFlags().String("foo", "", "A help for foo")
28
+
29
+ // Cobra supports local flags which will only run when this command
30
+ // is called directly, e.g.:
31
+ // restartCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
32
+ }
cmd/root.go ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+
7
+ "github.com/alist-org/alist/v3/cmd/flags"
8
+ _ "github.com/alist-org/alist/v3/drivers"
9
+ _ "github.com/alist-org/alist/v3/internal/offline_download"
10
+ "github.com/spf13/cobra"
11
+ )
12
+
13
+ var RootCmd = &cobra.Command{
14
+ Use: "alist",
15
+ Short: "A file list program that supports multiple storage.",
16
+ Long: `A file list program that supports multiple storage,
17
+ built with love by Xhofe and friends in Go/Solid.js.
18
+ Complete documentation is available at https://alist.nn.ci/`,
19
+ }
20
+
21
+ func Execute() {
22
+ if err := RootCmd.Execute(); err != nil {
23
+ fmt.Fprintln(os.Stderr, err)
24
+ os.Exit(1)
25
+ }
26
+ }
27
+
28
+ func init() {
29
+ RootCmd.PersistentFlags().StringVar(&flags.DataDir, "data", "data", "data folder")
30
+ RootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "start with debug mode")
31
+ RootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix")
32
+ RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
33
+ RootCmd.PersistentFlags().BoolVar(&flags.ForceBinDir, "force-bin-dir", false, "Force to use the directory where the binary file is located as data directory")
34
+ RootCmd.PersistentFlags().BoolVar(&flags.LogStd, "log-std", false, "Force to log to std")
35
+ }
cmd/server.go ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "net"
8
+ "net/http"
9
+ "os"
10
+ "os/signal"
11
+ "strconv"
12
+ "sync"
13
+ "syscall"
14
+ "time"
15
+
16
+ "github.com/alist-org/alist/v3/cmd/flags"
17
+ "github.com/alist-org/alist/v3/internal/bootstrap"
18
+ "github.com/alist-org/alist/v3/internal/conf"
19
+ "github.com/alist-org/alist/v3/pkg/utils"
20
+ "github.com/alist-org/alist/v3/server"
21
+ "github.com/gin-gonic/gin"
22
+ log "github.com/sirupsen/logrus"
23
+ "github.com/spf13/cobra"
24
+ )
25
+
26
+ // ServerCmd represents the server command
27
+ var ServerCmd = &cobra.Command{
28
+ Use: "server",
29
+ Short: "Start the server at the specified address",
30
+ Long: `Start the server at the specified address
31
+ the address is defined in config file`,
32
+ Run: func(cmd *cobra.Command, args []string) {
33
+ Init()
34
+ if conf.Conf.DelayedStart != 0 {
35
+ utils.Log.Infof("delayed start for %d seconds", conf.Conf.DelayedStart)
36
+ time.Sleep(time.Duration(conf.Conf.DelayedStart) * time.Second)
37
+ }
38
+ bootstrap.InitOfflineDownloadTools()
39
+ bootstrap.LoadStorages()
40
+ bootstrap.InitTaskManager()
41
+ if !flags.Debug && !flags.Dev {
42
+ gin.SetMode(gin.ReleaseMode)
43
+ }
44
+ r := gin.New()
45
+ r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
46
+ server.Init(r)
47
+ var httpSrv, httpsSrv, unixSrv *http.Server
48
+ if conf.Conf.Scheme.HttpPort != -1 {
49
+ httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
50
+ utils.Log.Infof("start HTTP server @ %s", httpBase)
51
+ httpSrv = &http.Server{Addr: httpBase, Handler: r}
52
+ go func() {
53
+ err := httpSrv.ListenAndServe()
54
+ if err != nil && !errors.Is(err, http.ErrServerClosed) {
55
+ utils.Log.Fatalf("failed to start http: %s", err.Error())
56
+ }
57
+ }()
58
+ }
59
+ if conf.Conf.Scheme.HttpsPort != -1 {
60
+ httpsBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpsPort)
61
+ utils.Log.Infof("start HTTPS server @ %s", httpsBase)
62
+ httpsSrv = &http.Server{Addr: httpsBase, Handler: r}
63
+ go func() {
64
+ err := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
65
+ if err != nil && !errors.Is(err, http.ErrServerClosed) {
66
+ utils.Log.Fatalf("failed to start https: %s", err.Error())
67
+ }
68
+ }()
69
+ }
70
+ if conf.Conf.Scheme.UnixFile != "" {
71
+ utils.Log.Infof("start unix server @ %s", conf.Conf.Scheme.UnixFile)
72
+ unixSrv = &http.Server{Handler: r}
73
+ go func() {
74
+ listener, err := net.Listen("unix", conf.Conf.Scheme.UnixFile)
75
+ if err != nil {
76
+ utils.Log.Fatalf("failed to listen unix: %+v", err)
77
+ }
78
+ // set socket file permission
79
+ mode, err := strconv.ParseUint(conf.Conf.Scheme.UnixFilePerm, 8, 32)
80
+ if err != nil {
81
+ utils.Log.Errorf("failed to parse socket file permission: %+v", err)
82
+ } else {
83
+ err = os.Chmod(conf.Conf.Scheme.UnixFile, os.FileMode(mode))
84
+ if err != nil {
85
+ utils.Log.Errorf("failed to chmod socket file: %+v", err)
86
+ }
87
+ }
88
+ err = unixSrv.Serve(listener)
89
+ if err != nil && !errors.Is(err, http.ErrServerClosed) {
90
+ utils.Log.Fatalf("failed to start unix: %s", err.Error())
91
+ }
92
+ }()
93
+ }
94
+ if conf.Conf.S3.Port != -1 && conf.Conf.S3.Enable {
95
+ s3r := gin.New()
96
+ s3r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
97
+ server.InitS3(s3r)
98
+ s3Base := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.S3.Port)
99
+ utils.Log.Infof("start S3 server @ %s", s3Base)
100
+ go func() {
101
+ var err error
102
+ if conf.Conf.S3.SSL {
103
+ httpsSrv = &http.Server{Addr: s3Base, Handler: s3r}
104
+ err = httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
105
+ }
106
+ if !conf.Conf.S3.SSL {
107
+ httpSrv = &http.Server{Addr: s3Base, Handler: s3r}
108
+ err = httpSrv.ListenAndServe()
109
+ }
110
+ if err != nil && !errors.Is(err, http.ErrServerClosed) {
111
+ utils.Log.Fatalf("failed to start s3 server: %s", err.Error())
112
+ }
113
+ }()
114
+ }
115
+ // Wait for interrupt signal to gracefully shutdown the server with
116
+ // a timeout of 1 second.
117
+ quit := make(chan os.Signal, 1)
118
+ // kill (no param) default send syscanll.SIGTERM
119
+ // kill -2 is syscall.SIGINT
120
+ // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
121
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
122
+ <-quit
123
+ utils.Log.Println("Shutdown server...")
124
+ Release()
125
+ ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
126
+ defer cancel()
127
+ var wg sync.WaitGroup
128
+ if conf.Conf.Scheme.HttpPort != -1 {
129
+ wg.Add(1)
130
+ go func() {
131
+ defer wg.Done()
132
+ if err := httpSrv.Shutdown(ctx); err != nil {
133
+ utils.Log.Fatal("HTTP server shutdown err: ", err)
134
+ }
135
+ }()
136
+ }
137
+ if conf.Conf.Scheme.HttpsPort != -1 {
138
+ wg.Add(1)
139
+ go func() {
140
+ defer wg.Done()
141
+ if err := httpsSrv.Shutdown(ctx); err != nil {
142
+ utils.Log.Fatal("HTTPS server shutdown err: ", err)
143
+ }
144
+ }()
145
+ }
146
+ if conf.Conf.Scheme.UnixFile != "" {
147
+ wg.Add(1)
148
+ go func() {
149
+ defer wg.Done()
150
+ if err := unixSrv.Shutdown(ctx); err != nil {
151
+ utils.Log.Fatal("Unix server shutdown err: ", err)
152
+ }
153
+ }()
154
+ }
155
+ wg.Wait()
156
+ utils.Log.Println("Server exit")
157
+ },
158
+ }
159
+
160
+ func init() {
161
+ RootCmd.AddCommand(ServerCmd)
162
+
163
+ // Here you will define your flags and configuration settings.
164
+
165
+ // Cobra supports Persistent Flags which will work for this command
166
+ // and all subcommands, e.g.:
167
+ // serverCmd.PersistentFlags().String("foo", "", "A help for foo")
168
+
169
+ // Cobra supports local flags which will only run when this command
170
+ // is called directly, e.g.:
171
+ // serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
172
+ }
173
+
174
+ // OutAlistInit 暴露用于外部启动server的函数
175
+ func OutAlistInit() {
176
+ var (
177
+ cmd *cobra.Command
178
+ args []string
179
+ )
180
+ ServerCmd.Run(cmd, args)
181
+ }
cmd/start.go ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
3
+ */
4
+ package cmd
5
+
6
+ import (
7
+ "os"
8
+ "os/exec"
9
+ "path/filepath"
10
+ "strconv"
11
+
12
+ log "github.com/sirupsen/logrus"
13
+ "github.com/spf13/cobra"
14
+ )
15
+
16
+ // StartCmd represents the start command
17
+ var StartCmd = &cobra.Command{
18
+ Use: "start",
19
+ Short: "Silent start alist server with `--force-bin-dir`",
20
+ Run: func(cmd *cobra.Command, args []string) {
21
+ start()
22
+ },
23
+ }
24
+
25
+ func start() {
26
+ initDaemon()
27
+ if pid != -1 {
28
+ _, err := os.FindProcess(pid)
29
+ if err == nil {
30
+ log.Info("alist already started, pid ", pid)
31
+ return
32
+ }
33
+ }
34
+ args := os.Args
35
+ args[1] = "server"
36
+ args = append(args, "--force-bin-dir")
37
+ cmd := &exec.Cmd{
38
+ Path: args[0],
39
+ Args: args,
40
+ Env: os.Environ(),
41
+ }
42
+ stdout, err := os.OpenFile(filepath.Join(filepath.Dir(pidFile), "start.log"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
43
+ if err != nil {
44
+ log.Fatal(os.Getpid(), ": failed to open start log file:", err)
45
+ }
46
+ cmd.Stderr = stdout
47
+ cmd.Stdout = stdout
48
+ err = cmd.Start()
49
+ if err != nil {
50
+ log.Fatal("failed to start children process: ", err)
51
+ }
52
+ log.Infof("success start pid: %d", cmd.Process.Pid)
53
+ err = os.WriteFile(pidFile, []byte(strconv.Itoa(cmd.Process.Pid)), 0666)
54
+ if err != nil {
55
+ log.Warn("failed to record pid, you may not be able to stop the program with `./alist stop`")
56
+ }
57
+ }
58
+
59
+ func init() {
60
+ RootCmd.AddCommand(StartCmd)
61
+
62
+ // Here you will define your flags and configuration settings.
63
+
64
+ // Cobra supports Persistent Flags which will work for this command
65
+ // and all subcommands, e.g.:
66
+ // startCmd.PersistentFlags().String("foo", "", "A help for foo")
67
+
68
+ // Cobra supports local flags which will only run when this command
69
+ // is called directly, e.g.:
70
+ // startCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
71
+ }
cmd/stop.go ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
3
+ */
4
+ package cmd
5
+
6
+ import (
7
+ "os"
8
+
9
+ log "github.com/sirupsen/logrus"
10
+ "github.com/spf13/cobra"
11
+ )
12
+
13
+ // StopCmd represents the stop command
14
+ var StopCmd = &cobra.Command{
15
+ Use: "stop",
16
+ Short: "Stop alist server by daemon/pid file",
17
+ Run: func(cmd *cobra.Command, args []string) {
18
+ stop()
19
+ },
20
+ }
21
+
22
+ func stop() {
23
+ initDaemon()
24
+ if pid == -1 {
25
+ log.Info("Seems not have been started. Try use `alist start` to start server.")
26
+ return
27
+ }
28
+ process, err := os.FindProcess(pid)
29
+ if err != nil {
30
+ log.Errorf("failed to find process by pid: %d, reason: %v", pid, process)
31
+ return
32
+ }
33
+ err = process.Kill()
34
+ if err != nil {
35
+ log.Errorf("failed to kill process %d: %v", pid, err)
36
+ } else {
37
+ log.Info("killed process: ", pid)
38
+ }
39
+ err = os.Remove(pidFile)
40
+ if err != nil {
41
+ log.Errorf("failed to remove pid file")
42
+ }
43
+ pid = -1
44
+ }
45
+
46
+ func init() {
47
+ RootCmd.AddCommand(StopCmd)
48
+
49
+ // Here you will define your flags and configuration settings.
50
+
51
+ // Cobra supports Persistent Flags which will work for this command
52
+ // and all subcommands, e.g.:
53
+ // stopCmd.PersistentFlags().String("foo", "", "A help for foo")
54
+
55
+ // Cobra supports local flags which will only run when this command
56
+ // is called directly, e.g.:
57
+ // stopCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
58
+ }
cmd/storage.go ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright © 2023 NAME HERE <EMAIL ADDRESS>
3
+ */
4
+ package cmd
5
+
6
+ import (
7
+ "os"
8
+ "strconv"
9
+
10
+ "github.com/alist-org/alist/v3/internal/db"
11
+ "github.com/alist-org/alist/v3/pkg/utils"
12
+ "github.com/charmbracelet/bubbles/table"
13
+ tea "github.com/charmbracelet/bubbletea"
14
+ "github.com/charmbracelet/lipgloss"
15
+ "github.com/spf13/cobra"
16
+ )
17
+
18
+ // storageCmd represents the storage command
19
+ var storageCmd = &cobra.Command{
20
+ Use: "storage",
21
+ Short: "Manage storage",
22
+ }
23
+
24
+ var disableStorageCmd = &cobra.Command{
25
+ Use: "disable",
26
+ Short: "Disable a storage",
27
+ Run: func(cmd *cobra.Command, args []string) {
28
+ if len(args) < 1 {
29
+ utils.Log.Errorf("mount path is required")
30
+ return
31
+ }
32
+ mountPath := args[0]
33
+ Init()
34
+ defer Release()
35
+ storage, err := db.GetStorageByMountPath(mountPath)
36
+ if err != nil {
37
+ utils.Log.Errorf("failed to query storage: %+v", err)
38
+ } else {
39
+ storage.Disabled = true
40
+ err = db.UpdateStorage(storage)
41
+ if err != nil {
42
+ utils.Log.Errorf("failed to update storage: %+v", err)
43
+ } else {
44
+ utils.Log.Infof("Storage with mount path [%s] have been disabled", mountPath)
45
+ }
46
+ }
47
+ },
48
+ }
49
+
50
+ var baseStyle = lipgloss.NewStyle().
51
+ BorderStyle(lipgloss.NormalBorder()).
52
+ BorderForeground(lipgloss.Color("240"))
53
+
54
+ type model struct {
55
+ table table.Model
56
+ }
57
+
58
+ func (m model) Init() tea.Cmd { return nil }
59
+
60
+ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
61
+ var cmd tea.Cmd
62
+ switch msg := msg.(type) {
63
+ case tea.KeyMsg:
64
+ switch msg.String() {
65
+ case "esc":
66
+ if m.table.Focused() {
67
+ m.table.Blur()
68
+ } else {
69
+ m.table.Focus()
70
+ }
71
+ case "q", "ctrl+c":
72
+ return m, tea.Quit
73
+ //case "enter":
74
+ // return m, tea.Batch(
75
+ // tea.Printf("Let's go to %s!", m.table.SelectedRow()[1]),
76
+ // )
77
+ }
78
+ }
79
+ m.table, cmd = m.table.Update(msg)
80
+ return m, cmd
81
+ }
82
+
83
+ func (m model) View() string {
84
+ return baseStyle.Render(m.table.View()) + "\n"
85
+ }
86
+
87
+ var storageTableHeight int
88
+ var listStorageCmd = &cobra.Command{
89
+ Use: "list",
90
+ Short: "List all storages",
91
+ Run: func(cmd *cobra.Command, args []string) {
92
+ Init()
93
+ defer Release()
94
+ storages, _, err := db.GetStorages(1, -1)
95
+ if err != nil {
96
+ utils.Log.Errorf("failed to query storages: %+v", err)
97
+ } else {
98
+ utils.Log.Infof("Found %d storages", len(storages))
99
+ columns := []table.Column{
100
+ {Title: "ID", Width: 4},
101
+ {Title: "Driver", Width: 16},
102
+ {Title: "Mount Path", Width: 30},
103
+ {Title: "Enabled", Width: 7},
104
+ }
105
+
106
+ var rows []table.Row
107
+ for i := range storages {
108
+ storage := storages[i]
109
+ enabled := "true"
110
+ if storage.Disabled {
111
+ enabled = "false"
112
+ }
113
+ rows = append(rows, table.Row{
114
+ strconv.Itoa(int(storage.ID)),
115
+ storage.Driver,
116
+ storage.MountPath,
117
+ enabled,
118
+ })
119
+ }
120
+ t := table.New(
121
+ table.WithColumns(columns),
122
+ table.WithRows(rows),
123
+ table.WithFocused(true),
124
+ table.WithHeight(storageTableHeight),
125
+ )
126
+
127
+ s := table.DefaultStyles()
128
+ s.Header = s.Header.
129
+ BorderStyle(lipgloss.NormalBorder()).
130
+ BorderForeground(lipgloss.Color("240")).
131
+ BorderBottom(true).
132
+ Bold(false)
133
+ s.Selected = s.Selected.
134
+ Foreground(lipgloss.Color("229")).
135
+ Background(lipgloss.Color("57")).
136
+ Bold(false)
137
+ t.SetStyles(s)
138
+
139
+ m := model{t}
140
+ if _, err := tea.NewProgram(m).Run(); err != nil {
141
+ utils.Log.Errorf("failed to run program: %+v", err)
142
+ os.Exit(1)
143
+ }
144
+ }
145
+ },
146
+ }
147
+
148
+ func init() {
149
+
150
+ RootCmd.AddCommand(storageCmd)
151
+ storageCmd.AddCommand(disableStorageCmd)
152
+ storageCmd.AddCommand(listStorageCmd)
153
+ storageCmd.PersistentFlags().IntVarP(&storageTableHeight, "height", "H", 10, "Table height")
154
+ // Here you will define your flags and configuration settings.
155
+
156
+ // Cobra supports Persistent Flags which will work for this command
157
+ // and all subcommands, e.g.:
158
+ // storageCmd.PersistentFlags().String("foo", "", "A help for foo")
159
+
160
+ // Cobra supports local flags which will only run when this command
161
+ // is called directly, e.g.:
162
+ // storageCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
163
+ }
cmd/user.go ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package cmd
2
+
3
+ import (
4
+ "crypto/tls"
5
+ "fmt"
6
+ "time"
7
+
8
+ "github.com/alist-org/alist/v3/internal/conf"
9
+ "github.com/alist-org/alist/v3/internal/op"
10
+ "github.com/alist-org/alist/v3/internal/setting"
11
+ "github.com/alist-org/alist/v3/pkg/utils"
12
+ "github.com/go-resty/resty/v2"
13
+ )
14
+
15
+ func DelAdminCacheOnline() {
16
+ admin, err := op.GetAdmin()
17
+ if err != nil {
18
+ utils.Log.Errorf("[del_admin_cache] get admin error: %+v", err)
19
+ return
20
+ }
21
+ DelUserCacheOnline(admin.Username)
22
+ }
23
+
24
+ func DelUserCacheOnline(username string) {
25
+ client := resty.New().SetTimeout(1 * time.Second).SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
26
+ token := setting.GetStr(conf.Token)
27
+ port := conf.Conf.Scheme.HttpPort
28
+ u := fmt.Sprintf("http://localhost:%d/api/admin/user/del_cache", port)
29
+ if port == -1 {
30
+ if conf.Conf.Scheme.HttpsPort == -1 {
31
+ utils.Log.Warnf("[del_user_cache] no open port")
32
+ return
33
+ }
34
+ u = fmt.Sprintf("https://localhost:%d/api/admin/user/del_cache", conf.Conf.Scheme.HttpsPort)
35
+ }
36
+ res, err := client.R().SetHeader("Authorization", token).SetQueryParam("username", username).Post(u)
37
+ if err != nil {
38
+ utils.Log.Warnf("[del_user_cache_online] failed: %+v", err)
39
+ return
40
+ }
41
+ if res.StatusCode() != 200 {
42
+ utils.Log.Warnf("[del_user_cache_online] failed: %+v", res.String())
43
+ return
44
+ }
45
+ code := utils.Json.Get(res.Body(), "code").ToInt()
46
+ msg := utils.Json.Get(res.Body(), "message").ToString()
47
+ if code != 200 {
48
+ utils.Log.Errorf("[del_user_cache_online] error: %s", msg)
49
+ return
50
+ }
51
+ utils.Log.Debugf("[del_user_cache_online] del user [%s] cache success", username)
52
+ }
cmd/version.go ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
3
+ */
4
+ package cmd
5
+
6
+ import (
7
+ "fmt"
8
+ "os"
9
+
10
+ "github.com/alist-org/alist/v3/internal/conf"
11
+ "github.com/spf13/cobra"
12
+ )
13
+
14
+ // VersionCmd represents the version command
15
+ var VersionCmd = &cobra.Command{
16
+ Use: "version",
17
+ Short: "Show current version of AList",
18
+ Run: func(cmd *cobra.Command, args []string) {
19
+ fmt.Printf(`Built At: %s
20
+ Go Version: %s
21
+ Author: %s
22
+ Commit ID: %s
23
+ Version: %s
24
+ WebVersion: %s
25
+ `,
26
+ conf.BuiltAt, conf.GoVersion, conf.GitAuthor, conf.GitCommit, conf.Version, conf.WebVersion)
27
+ os.Exit(0)
28
+ },
29
+ }
30
+
31
+ func init() {
32
+ RootCmd.AddCommand(VersionCmd)
33
+
34
+ // Here you will define your flags and configuration settings.
35
+
36
+ // Cobra supports Persistent Flags which will work for this command
37
+ // and all subcommands, e.g.:
38
+ // versionCmd.PersistentFlags().String("foo", "", "A help for foo")
39
+
40
+ // Cobra supports local flags which will only run when this command
41
+ // is called directly, e.g.:
42
+ // versionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
43
+ }
drivers/115/appver.go ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115
2
+
3
+ import (
4
+ driver115 "github.com/SheltonZhu/115driver/pkg/driver"
5
+ "github.com/alist-org/alist/v3/drivers/base"
6
+ log "github.com/sirupsen/logrus"
7
+ )
8
+
9
+ var (
10
+ md5Salt = "Qclm8MGWUv59TnrR0XPg"
11
+ appVer = "27.0.5.7"
12
+ )
13
+
14
+ func (d *Pan115) getAppVersion() ([]driver115.AppVersion, error) {
15
+ result := driver115.VersionResp{}
16
+ resp, err := base.RestyClient.R().Get(driver115.ApiGetVersion)
17
+
18
+ err = driver115.CheckErr(err, &result, resp)
19
+ if err != nil {
20
+ return nil, err
21
+ }
22
+
23
+ return result.Data.GetAppVersions(), nil
24
+ }
25
+
26
+ func (d *Pan115) getAppVer() string {
27
+ // todo add some cache?
28
+ vers, err := d.getAppVersion()
29
+ if err != nil {
30
+ log.Warnf("[115] get app version failed: %v", err)
31
+ return appVer
32
+ }
33
+ for _, ver := range vers {
34
+ if ver.AppName == "win" {
35
+ return ver.Version
36
+ }
37
+ }
38
+ return appVer
39
+ }
40
+
41
+ func (d *Pan115) initAppVer() {
42
+ appVer = d.getAppVer()
43
+ }
drivers/115/driver.go ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115
2
+
3
+ import (
4
+ "context"
5
+ "strings"
6
+ "sync"
7
+
8
+ driver115 "github.com/SheltonZhu/115driver/pkg/driver"
9
+ "github.com/alist-org/alist/v3/internal/driver"
10
+ "github.com/alist-org/alist/v3/internal/model"
11
+ "github.com/alist-org/alist/v3/pkg/http_range"
12
+ "github.com/alist-org/alist/v3/pkg/utils"
13
+ "github.com/pkg/errors"
14
+ "golang.org/x/time/rate"
15
+ )
16
+
17
+ type Pan115 struct {
18
+ model.Storage
19
+ Addition
20
+ client *driver115.Pan115Client
21
+ limiter *rate.Limiter
22
+ appVerOnce sync.Once
23
+ }
24
+
25
+ func (d *Pan115) Config() driver.Config {
26
+ return config
27
+ }
28
+
29
+ func (d *Pan115) GetAddition() driver.Additional {
30
+ return &d.Addition
31
+ }
32
+
33
+ func (d *Pan115) Init(ctx context.Context) error {
34
+ d.appVerOnce.Do(d.initAppVer)
35
+ if d.LimitRate > 0 {
36
+ d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)
37
+ }
38
+ return d.login()
39
+ }
40
+
41
+ func (d *Pan115) WaitLimit(ctx context.Context) error {
42
+ if d.limiter != nil {
43
+ return d.limiter.Wait(ctx)
44
+ }
45
+ return nil
46
+ }
47
+
48
+ func (d *Pan115) Drop(ctx context.Context) error {
49
+ return nil
50
+ }
51
+
52
+ func (d *Pan115) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
53
+ if err := d.WaitLimit(ctx); err != nil {
54
+ return nil, err
55
+ }
56
+ files, err := d.getFiles(dir.GetID())
57
+ if err != nil && !errors.Is(err, driver115.ErrNotExist) {
58
+ return nil, err
59
+ }
60
+ return utils.SliceConvert(files, func(src FileObj) (model.Obj, error) {
61
+ return &src, nil
62
+ })
63
+ }
64
+
65
+ func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
66
+ if err := d.WaitLimit(ctx); err != nil {
67
+ return nil, err
68
+ }
69
+ userAgent := args.Header.Get("User-Agent")
70
+ downloadInfo, err := d.
71
+ DownloadWithUA(file.(*FileObj).PickCode, userAgent)
72
+ if err != nil {
73
+ return nil, err
74
+ }
75
+ link := &model.Link{
76
+ URL: downloadInfo.Url.Url,
77
+ Header: downloadInfo.Header,
78
+ }
79
+ return link, nil
80
+ }
81
+
82
+ func (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
83
+ if err := d.WaitLimit(ctx); err != nil {
84
+ return nil, err
85
+ }
86
+
87
+ result := driver115.MkdirResp{}
88
+ form := map[string]string{
89
+ "pid": parentDir.GetID(),
90
+ "cname": dirName,
91
+ }
92
+ req := d.client.NewRequest().
93
+ SetFormData(form).
94
+ SetResult(&result).
95
+ ForceContentType("application/json;charset=UTF-8")
96
+
97
+ resp, err := req.Post(driver115.ApiDirAdd)
98
+
99
+ err = driver115.CheckErr(err, &result, resp)
100
+ if err != nil {
101
+ return nil, err
102
+ }
103
+ f, err := d.getNewFile(result.FileID)
104
+ if err != nil {
105
+ return nil, nil
106
+ }
107
+ return f, nil
108
+ }
109
+
110
+ func (d *Pan115) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
111
+ if err := d.WaitLimit(ctx); err != nil {
112
+ return nil, err
113
+ }
114
+ if err := d.client.Move(dstDir.GetID(), srcObj.GetID()); err != nil {
115
+ return nil, err
116
+ }
117
+ f, err := d.getNewFile(srcObj.GetID())
118
+ if err != nil {
119
+ return nil, nil
120
+ }
121
+ return f, nil
122
+ }
123
+
124
+ func (d *Pan115) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
125
+ if err := d.WaitLimit(ctx); err != nil {
126
+ return nil, err
127
+ }
128
+ if err := d.client.Rename(srcObj.GetID(), newName); err != nil {
129
+ return nil, err
130
+ }
131
+ f, err := d.getNewFile((srcObj.GetID()))
132
+ if err != nil {
133
+ return nil, nil
134
+ }
135
+ return f, nil
136
+ }
137
+
138
+ func (d *Pan115) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
139
+ if err := d.WaitLimit(ctx); err != nil {
140
+ return err
141
+ }
142
+ return d.client.Copy(dstDir.GetID(), srcObj.GetID())
143
+ }
144
+
145
+ func (d *Pan115) Remove(ctx context.Context, obj model.Obj) error {
146
+ if err := d.WaitLimit(ctx); err != nil {
147
+ return err
148
+ }
149
+ return d.client.Delete(obj.GetID())
150
+ }
151
+
152
+ func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
153
+ if err := d.WaitLimit(ctx); err != nil {
154
+ return nil, err
155
+ }
156
+
157
+ var (
158
+ fastInfo *driver115.UploadInitResp
159
+ dirID = dstDir.GetID()
160
+ )
161
+
162
+ if ok, err := d.client.UploadAvailable(); err != nil || !ok {
163
+ return nil, err
164
+ }
165
+ if stream.GetSize() > d.client.UploadMetaInfo.SizeLimit {
166
+ return nil, driver115.ErrUploadTooLarge
167
+ }
168
+ //if digest, err = d.client.GetDigestResult(stream); err != nil {
169
+ // return err
170
+ //}
171
+
172
+ const PreHashSize int64 = 128 * utils.KB
173
+ hashSize := PreHashSize
174
+ if stream.GetSize() < PreHashSize {
175
+ hashSize = stream.GetSize()
176
+ }
177
+ reader, err := stream.RangeRead(http_range.Range{Start: 0, Length: hashSize})
178
+ if err != nil {
179
+ return nil, err
180
+ }
181
+ preHash, err := utils.HashReader(utils.SHA1, reader)
182
+ if err != nil {
183
+ return nil, err
184
+ }
185
+ preHash = strings.ToUpper(preHash)
186
+ fullHash := stream.GetHash().GetHash(utils.SHA1)
187
+ if len(fullHash) <= 0 {
188
+ tmpF, err := stream.CacheFullInTempFile()
189
+ if err != nil {
190
+ return nil, err
191
+ }
192
+ fullHash, err = utils.HashFile(utils.SHA1, tmpF)
193
+ if err != nil {
194
+ return nil, err
195
+ }
196
+ }
197
+ fullHash = strings.ToUpper(fullHash)
198
+
199
+ // rapid-upload
200
+ // note that 115 add timeout for rapid-upload,
201
+ // and "sig invalid" err is thrown even when the hash is correct after timeout.
202
+ if fastInfo, err = d.rapidUpload(stream.GetSize(), stream.GetName(), dirID, preHash, fullHash, stream); err != nil {
203
+ return nil, err
204
+ }
205
+ if matched, err := fastInfo.Ok(); err != nil {
206
+ return nil, err
207
+ } else if matched {
208
+ f, err := d.getNewFileByPickCode(fastInfo.PickCode)
209
+ if err != nil {
210
+ return nil, nil
211
+ }
212
+ return f, nil
213
+ }
214
+
215
+ var uploadResult *UploadResult
216
+ // 闪传失败,上传
217
+ if stream.GetSize() <= 10*utils.MB { // 文件大小小于10MB,改用普通模式上传
218
+ if uploadResult, err = d.UploadByOSS(&fastInfo.UploadOSSParams, stream, dirID); err != nil {
219
+ return nil, err
220
+ }
221
+ } else {
222
+ // 分片上传
223
+ if uploadResult, err = d.UploadByMultipart(&fastInfo.UploadOSSParams, stream.GetSize(), stream, dirID); err != nil {
224
+ return nil, err
225
+ }
226
+ }
227
+
228
+ file, err := d.getNewFile(uploadResult.Data.FileID)
229
+ if err != nil {
230
+ return nil, nil
231
+ }
232
+ return file, nil
233
+ }
234
+
235
+ func (d *Pan115) OfflineList(ctx context.Context) ([]*driver115.OfflineTask, error) {
236
+ resp, err := d.client.ListOfflineTask(0)
237
+ if err != nil {
238
+ return nil, err
239
+ }
240
+ return resp.Tasks, nil
241
+ }
242
+
243
+ func (d *Pan115) OfflineDownload(ctx context.Context, uris []string, dstDir model.Obj) ([]string, error) {
244
+ return d.client.AddOfflineTaskURIs(uris, dstDir.GetID())
245
+ }
246
+
247
+ func (d *Pan115) DeleteOfflineTasks(ctx context.Context, hashes []string, deleteFiles bool) error {
248
+ return d.client.DeleteOfflineTasks(hashes, deleteFiles)
249
+ }
250
+
251
+ var _ driver.Driver = (*Pan115)(nil)
drivers/115/meta.go ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/internal/driver"
5
+ "github.com/alist-org/alist/v3/internal/op"
6
+ )
7
+
8
+ type Addition struct {
9
+ Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
10
+ QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
11
+ QRCodeSource string `json:"qrcode_source" type:"select" options:"web,android,ios,tv,alipaymini,wechatmini,qandroid" default:"linux" help:"select the QR code device, default linux"`
12
+ PageSize int64 `json:"page_size" type:"number" default:"1000" help:"list api per page size of 115 driver"`
13
+ LimitRate float64 `json:"limit_rate" type:"float" default:"2" help:"limit all api request rate ([limit]r/1s)"`
14
+ driver.RootID
15
+ }
16
+
17
+ var config = driver.Config{
18
+ Name: "115 Cloud",
19
+ DefaultRoot: "0",
20
+ // OnlyProxy: true,
21
+ // OnlyLocal: true,
22
+ // NoOverwriteUpload: true,
23
+ }
24
+
25
+ func init() {
26
+ op.RegisterDriver(func() driver.Driver {
27
+ return &Pan115{}
28
+ })
29
+ }
drivers/115/types.go ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115
2
+
3
+ import (
4
+ "time"
5
+
6
+ "github.com/SheltonZhu/115driver/pkg/driver"
7
+ "github.com/alist-org/alist/v3/internal/model"
8
+ "github.com/alist-org/alist/v3/pkg/utils"
9
+ )
10
+
11
+ var _ model.Obj = (*FileObj)(nil)
12
+
13
+ type FileObj struct {
14
+ driver.File
15
+ }
16
+
17
+ func (f *FileObj) CreateTime() time.Time {
18
+ return f.File.CreateTime
19
+ }
20
+
21
+ func (f *FileObj) GetHash() utils.HashInfo {
22
+ return utils.NewHashInfo(utils.SHA1, f.Sha1)
23
+ }
24
+
25
+ type UploadResult struct {
26
+ driver.BasicResp
27
+ Data struct {
28
+ PickCode string `json:"pick_code"`
29
+ FileSize int `json:"file_size"`
30
+ FileID string `json:"file_id"`
31
+ ThumbURL string `json:"thumb_url"`
32
+ Sha1 string `json:"sha1"`
33
+ Aid int `json:"aid"`
34
+ FileName string `json:"file_name"`
35
+ Cid string `json:"cid"`
36
+ IsVideo int `json:"is_video"`
37
+ } `json:"data"`
38
+ }
drivers/115/util.go ADDED
@@ -0,0 +1,537 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115
2
+
3
+ import (
4
+ "bytes"
5
+ "crypto/md5"
6
+ "crypto/tls"
7
+ "encoding/hex"
8
+ "encoding/json"
9
+ "fmt"
10
+ "io"
11
+ "net/http"
12
+ "net/url"
13
+ "strconv"
14
+ "strings"
15
+ "sync"
16
+ "time"
17
+
18
+ "github.com/alist-org/alist/v3/internal/conf"
19
+ "github.com/alist-org/alist/v3/internal/model"
20
+ "github.com/alist-org/alist/v3/pkg/http_range"
21
+ "github.com/alist-org/alist/v3/pkg/utils"
22
+ "github.com/aliyun/aliyun-oss-go-sdk/oss"
23
+
24
+ driver115 "github.com/SheltonZhu/115driver/pkg/driver"
25
+ crypto "github.com/gaoyb7/115drive-webdav/115"
26
+ "github.com/orzogc/fake115uploader/cipher"
27
+ "github.com/pkg/errors"
28
+ )
29
+
30
+ // var UserAgent = driver115.UA115Browser
31
+ func (d *Pan115) login() error {
32
+ var err error
33
+ opts := []driver115.Option{
34
+ driver115.UA(d.getUA()),
35
+ func(c *driver115.Pan115Client) {
36
+ c.Client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
37
+ },
38
+ }
39
+ d.client = driver115.New(opts...)
40
+ cr := &driver115.Credential{}
41
+ if d.QRCodeToken != "" {
42
+ s := &driver115.QRCodeSession{
43
+ UID: d.QRCodeToken,
44
+ }
45
+ if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
46
+ return errors.Wrap(err, "failed to login by qrcode")
47
+ }
48
+ d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s;KID=%s", cr.UID, cr.CID, cr.SEID, cr.KID)
49
+ d.QRCodeToken = ""
50
+ } else if d.Cookie != "" {
51
+ if err = cr.FromCookie(d.Cookie); err != nil {
52
+ return errors.Wrap(err, "failed to login by cookies")
53
+ }
54
+ d.client.ImportCredential(cr)
55
+ } else {
56
+ return errors.New("missing cookie or qrcode account")
57
+ }
58
+ return d.client.LoginCheck()
59
+ }
60
+
61
+ func (d *Pan115) getFiles(fileId string) ([]FileObj, error) {
62
+ res := make([]FileObj, 0)
63
+ if d.PageSize <= 0 {
64
+ d.PageSize = driver115.FileListLimit
65
+ }
66
+ files, err := d.client.ListWithLimit(fileId, d.PageSize)
67
+ if err != nil {
68
+ return nil, err
69
+ }
70
+ for _, file := range *files {
71
+ res = append(res, FileObj{file})
72
+ }
73
+ return res, nil
74
+ }
75
+
76
+ func (d *Pan115) getNewFile(fileId string) (*FileObj, error) {
77
+ file, err := d.client.GetFile(fileId)
78
+ if err != nil {
79
+ return nil, err
80
+ }
81
+ return &FileObj{*file}, nil
82
+ }
83
+
84
+ func (d *Pan115) getNewFileByPickCode(pickCode string) (*FileObj, error) {
85
+ result := driver115.GetFileInfoResponse{}
86
+ req := d.client.NewRequest().
87
+ SetQueryParam("pick_code", pickCode).
88
+ ForceContentType("application/json;charset=UTF-8").
89
+ SetResult(&result)
90
+ resp, err := req.Get(driver115.ApiFileInfo)
91
+ if err := driver115.CheckErr(err, &result, resp); err != nil {
92
+ return nil, err
93
+ }
94
+ if len(result.Files) == 0 {
95
+ return nil, errors.New("not get file info")
96
+ }
97
+ fileInfo := result.Files[0]
98
+
99
+ f := &FileObj{}
100
+ f.From(fileInfo)
101
+ return f, nil
102
+ }
103
+
104
+ func (d *Pan115) getUA() string {
105
+ return fmt.Sprintf("Mozilla/5.0 115Browser/%s", appVer)
106
+ }
107
+
108
+ func (d *Pan115) DownloadWithUA(pickCode, ua string) (*driver115.DownloadInfo, error) {
109
+ key := crypto.GenerateKey()
110
+ result := driver115.DownloadResp{}
111
+ params, err := utils.Json.Marshal(map[string]string{"pickcode": pickCode})
112
+ if err != nil {
113
+ return nil, err
114
+ }
115
+
116
+ data := crypto.Encode(params, key)
117
+
118
+ bodyReader := strings.NewReader(url.Values{"data": []string{data}}.Encode())
119
+ reqUrl := fmt.Sprintf("%s?t=%s", driver115.ApiDownloadGetUrl, driver115.Now().String())
120
+ req, _ := http.NewRequest(http.MethodPost, reqUrl, bodyReader)
121
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
122
+ req.Header.Set("Cookie", d.Cookie)
123
+ req.Header.Set("User-Agent", ua)
124
+
125
+ resp, err := d.client.Client.GetClient().Do(req)
126
+ if err != nil {
127
+ return nil, err
128
+ }
129
+ defer resp.Body.Close()
130
+
131
+ body, err := io.ReadAll(resp.Body)
132
+ if err != nil {
133
+ return nil, err
134
+ }
135
+ if err := utils.Json.Unmarshal(body, &result); err != nil {
136
+ return nil, err
137
+ }
138
+
139
+ if err = result.Err(string(body)); err != nil {
140
+ return nil, err
141
+ }
142
+
143
+ bytes, err := crypto.Decode(string(result.EncodedData), key)
144
+ if err != nil {
145
+ return nil, err
146
+ }
147
+
148
+ downloadInfo := driver115.DownloadData{}
149
+ if err := utils.Json.Unmarshal(bytes, &downloadInfo); err != nil {
150
+ return nil, err
151
+ }
152
+
153
+ for _, info := range downloadInfo {
154
+ if info.FileSize < 0 {
155
+ return nil, driver115.ErrDownloadEmpty
156
+ }
157
+ info.Header = resp.Request.Header
158
+ return info, nil
159
+ }
160
+ return nil, driver115.ErrUnexpected
161
+ }
162
+
163
+ func (c *Pan115) GenerateToken(fileID, preID, timeStamp, fileSize, signKey, signVal string) string {
164
+ userID := strconv.FormatInt(c.client.UserID, 10)
165
+ userIDMd5 := md5.Sum([]byte(userID))
166
+ tokenMd5 := md5.Sum([]byte(md5Salt + fileID + fileSize + signKey + signVal + userID + timeStamp + hex.EncodeToString(userIDMd5[:]) + appVer))
167
+ return hex.EncodeToString(tokenMd5[:])
168
+ }
169
+
170
+ func (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID string, stream model.FileStreamer) (*driver115.UploadInitResp, error) {
171
+ var (
172
+ ecdhCipher *cipher.EcdhCipher
173
+ encrypted []byte
174
+ decrypted []byte
175
+ encodedToken string
176
+ err error
177
+ target = "U_1_" + dirID
178
+ bodyBytes []byte
179
+ result = driver115.UploadInitResp{}
180
+ fileSizeStr = strconv.FormatInt(fileSize, 10)
181
+ )
182
+ if ecdhCipher, err = cipher.NewEcdhCipher(); err != nil {
183
+ return nil, err
184
+ }
185
+
186
+ userID := strconv.FormatInt(d.client.UserID, 10)
187
+ form := url.Values{}
188
+ form.Set("appid", "0")
189
+ form.Set("appversion", appVer)
190
+ form.Set("userid", userID)
191
+ form.Set("filename", fileName)
192
+ form.Set("filesize", fileSizeStr)
193
+ form.Set("fileid", fileID)
194
+ form.Set("target", target)
195
+ form.Set("sig", d.client.GenerateSignature(fileID, target))
196
+
197
+ signKey, signVal := "", ""
198
+ for retry := true; retry; {
199
+ t := driver115.NowMilli()
200
+
201
+ if encodedToken, err = ecdhCipher.EncodeToken(t.ToInt64()); err != nil {
202
+ return nil, err
203
+ }
204
+
205
+ params := map[string]string{
206
+ "k_ec": encodedToken,
207
+ }
208
+
209
+ form.Set("t", t.String())
210
+ form.Set("token", d.GenerateToken(fileID, preID, t.String(), fileSizeStr, signKey, signVal))
211
+ if signKey != "" && signVal != "" {
212
+ form.Set("sign_key", signKey)
213
+ form.Set("sign_val", signVal)
214
+ }
215
+ if encrypted, err = ecdhCipher.Encrypt([]byte(form.Encode())); err != nil {
216
+ return nil, err
217
+ }
218
+
219
+ req := d.client.NewRequest().
220
+ SetQueryParams(params).
221
+ SetBody(encrypted).
222
+ SetHeaderVerbatim("Content-Type", "application/x-www-form-urlencoded").
223
+ SetDoNotParseResponse(true)
224
+ resp, err := req.Post(driver115.ApiUploadInit)
225
+ if err != nil {
226
+ return nil, err
227
+ }
228
+ data := resp.RawBody()
229
+ defer data.Close()
230
+ if bodyBytes, err = io.ReadAll(data); err != nil {
231
+ return nil, err
232
+ }
233
+ if decrypted, err = ecdhCipher.Decrypt(bodyBytes); err != nil {
234
+ return nil, err
235
+ }
236
+ if err = driver115.CheckErr(json.Unmarshal(decrypted, &result), &result, resp); err != nil {
237
+ return nil, err
238
+ }
239
+ if result.Status == 7 {
240
+ // Update signKey & signVal
241
+ signKey = result.SignKey
242
+ signVal, err = UploadDigestRange(stream, result.SignCheck)
243
+ if err != nil {
244
+ return nil, err
245
+ }
246
+ } else {
247
+ retry = false
248
+ }
249
+ result.SHA1 = fileID
250
+ }
251
+
252
+ return &result, nil
253
+ }
254
+
255
+ func UploadDigestRange(stream model.FileStreamer, rangeSpec string) (result string, err error) {
256
+ var start, end int64
257
+ if _, err = fmt.Sscanf(rangeSpec, "%d-%d", &start, &end); err != nil {
258
+ return
259
+ }
260
+
261
+ length := end - start + 1
262
+ reader, err := stream.RangeRead(http_range.Range{Start: start, Length: length})
263
+ if err != nil {
264
+ return "", err
265
+ }
266
+ hashStr, err := utils.HashReader(utils.SHA1, reader)
267
+ if err != nil {
268
+ return "", err
269
+ }
270
+ result = strings.ToUpper(hashStr)
271
+ return
272
+ }
273
+
274
+ // UploadByOSS use aliyun sdk to upload
275
+ func (c *Pan115) UploadByOSS(params *driver115.UploadOSSParams, r io.Reader, dirID string) (*UploadResult, error) {
276
+ ossToken, err := c.client.GetOSSToken()
277
+ if err != nil {
278
+ return nil, err
279
+ }
280
+ ossClient, err := oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret)
281
+ if err != nil {
282
+ return nil, err
283
+ }
284
+ bucket, err := ossClient.Bucket(params.Bucket)
285
+ if err != nil {
286
+ return nil, err
287
+ }
288
+
289
+ var bodyBytes []byte
290
+ if err = bucket.PutObject(params.Object, r, append(
291
+ driver115.OssOption(params, ossToken),
292
+ oss.CallbackResult(&bodyBytes),
293
+ )...); err != nil {
294
+ return nil, err
295
+ }
296
+
297
+ var uploadResult UploadResult
298
+ if err = json.Unmarshal(bodyBytes, &uploadResult); err != nil {
299
+ return nil, err
300
+ }
301
+ return &uploadResult, uploadResult.Err(string(bodyBytes))
302
+ }
303
+
304
+ // UploadByMultipart upload by mutipart blocks
305
+ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize int64, stream model.FileStreamer, dirID string, opts ...driver115.UploadMultipartOption) (*UploadResult, error) {
306
+ var (
307
+ chunks []oss.FileChunk
308
+ parts []oss.UploadPart
309
+ imur oss.InitiateMultipartUploadResult
310
+ ossClient *oss.Client
311
+ bucket *oss.Bucket
312
+ ossToken *driver115.UploadOSSTokenResp
313
+ bodyBytes []byte
314
+ err error
315
+ )
316
+
317
+ tmpF, err := stream.CacheFullInTempFile()
318
+ if err != nil {
319
+ return nil, err
320
+ }
321
+
322
+ options := driver115.DefalutUploadMultipartOptions()
323
+ if len(opts) > 0 {
324
+ for _, f := range opts {
325
+ f(options)
326
+ }
327
+ }
328
+ // oss 启用Sequential必须按顺序上传
329
+ options.ThreadsNum = 1
330
+
331
+ if ossToken, err = d.client.GetOSSToken(); err != nil {
332
+ return nil, err
333
+ }
334
+
335
+ if ossClient, err = oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret, oss.EnableMD5(true), oss.EnableCRC(true)); err != nil {
336
+ return nil, err
337
+ }
338
+
339
+ if bucket, err = ossClient.Bucket(params.Bucket); err != nil {
340
+ return nil, err
341
+ }
342
+
343
+ // ossToken一小时后就会失效,所以每50分钟重新获取一次
344
+ ticker := time.NewTicker(options.TokenRefreshTime)
345
+ defer ticker.Stop()
346
+ // 设置超时
347
+ timeout := time.NewTimer(options.Timeout)
348
+
349
+ if chunks, err = SplitFile(fileSize); err != nil {
350
+ return nil, err
351
+ }
352
+
353
+ if imur, err = bucket.InitiateMultipartUpload(params.Object,
354
+ oss.SetHeader(driver115.OssSecurityTokenHeaderName, ossToken.SecurityToken),
355
+ oss.UserAgentHeader(driver115.OSSUserAgent),
356
+ oss.EnableSha1(), oss.Sequential(),
357
+ ); err != nil {
358
+ return nil, err
359
+ }
360
+
361
+ wg := sync.WaitGroup{}
362
+ wg.Add(len(chunks))
363
+
364
+ chunksCh := make(chan oss.FileChunk)
365
+ errCh := make(chan error)
366
+ UploadedPartsCh := make(chan oss.UploadPart)
367
+ quit := make(chan struct{})
368
+
369
+ // producer
370
+ go chunksProducer(chunksCh, chunks)
371
+ go func() {
372
+ wg.Wait()
373
+ quit <- struct{}{}
374
+ }()
375
+
376
+ // consumers
377
+ for i := 0; i < options.ThreadsNum; i++ {
378
+ go func(threadId int) {
379
+ defer func() {
380
+ if r := recover(); r != nil {
381
+ errCh <- fmt.Errorf("recovered in %v", r)
382
+ }
383
+ }()
384
+ for chunk := range chunksCh {
385
+ var part oss.UploadPart // 出现错误就继续尝试,共尝试3次
386
+ for retry := 0; retry < 3; retry++ {
387
+ select {
388
+ case <-ticker.C:
389
+ if ossToken, err = d.client.GetOSSToken(); err != nil { // 到时重新获取ossToken
390
+ errCh <- errors.Wrap(err, "刷新token时出现错误")
391
+ }
392
+ default:
393
+ }
394
+
395
+ buf := make([]byte, chunk.Size)
396
+ if _, err = tmpF.ReadAt(buf, chunk.Offset); err != nil && !errors.Is(err, io.EOF) {
397
+ continue
398
+ }
399
+
400
+ if part, err = bucket.UploadPart(imur, bytes.NewBuffer(buf), chunk.Size, chunk.Number, driver115.OssOption(params, ossToken)...); err == nil {
401
+ break
402
+ }
403
+ }
404
+ if err != nil {
405
+ errCh <- errors.Wrap(err, fmt.Sprintf("上传 %s 的第%d个分片时出现错误:%v", stream.GetName(), chunk.Number, err))
406
+ }
407
+ UploadedPartsCh <- part
408
+ }
409
+ }(i)
410
+ }
411
+
412
+ go func() {
413
+ for part := range UploadedPartsCh {
414
+ parts = append(parts, part)
415
+ wg.Done()
416
+ }
417
+ }()
418
+ LOOP:
419
+ for {
420
+ select {
421
+ case <-ticker.C:
422
+ // 到时重新获取ossToken
423
+ if ossToken, err = d.client.GetOSSToken(); err != nil {
424
+ return nil, err
425
+ }
426
+ case <-quit:
427
+ break LOOP
428
+ case <-errCh:
429
+ return nil, err
430
+ case <-timeout.C:
431
+ return nil, fmt.Errorf("time out")
432
+ }
433
+ }
434
+
435
+ // 不知道啥原因,oss那边分片上传不计算sha1,导致115服务器校验错误
436
+ // params.Callback.Callback = strings.ReplaceAll(params.Callback.Callback, "${sha1}", params.SHA1)
437
+ if _, err := bucket.CompleteMultipartUpload(imur, parts, append(
438
+ driver115.OssOption(params, ossToken),
439
+ oss.CallbackResult(&bodyBytes),
440
+ )...); err != nil {
441
+ return nil, err
442
+ }
443
+
444
+ var uploadResult UploadResult
445
+ if err = json.Unmarshal(bodyBytes, &uploadResult); err != nil {
446
+ return nil, err
447
+ }
448
+ return &uploadResult, uploadResult.Err(string(bodyBytes))
449
+ }
450
+
451
+ func chunksProducer(ch chan oss.FileChunk, chunks []oss.FileChunk) {
452
+ for _, chunk := range chunks {
453
+ ch <- chunk
454
+ }
455
+ }
456
+
457
+ func SplitFile(fileSize int64) (chunks []oss.FileChunk, err error) {
458
+ for i := int64(1); i < 10; i++ {
459
+ if fileSize < i*utils.GB { // 文件大小小于iGB时分为i*1000片
460
+ if chunks, err = SplitFileByPartNum(fileSize, int(i*1000)); err != nil {
461
+ return
462
+ }
463
+ break
464
+ }
465
+ }
466
+ if fileSize > 9*utils.GB { // 文件大小大于9GB时分为10000片
467
+ if chunks, err = SplitFileByPartNum(fileSize, 10000); err != nil {
468
+ return
469
+ }
470
+ }
471
+ // 单个分片大小不能小于100KB
472
+ if chunks[0].Size < 100*utils.KB {
473
+ if chunks, err = SplitFileByPartSize(fileSize, 100*utils.KB); err != nil {
474
+ return
475
+ }
476
+ }
477
+ return
478
+ }
479
+
480
+ // SplitFileByPartNum splits big file into parts by the num of parts.
481
+ // Split the file with specified parts count, returns the split result when error is nil.
482
+ func SplitFileByPartNum(fileSize int64, chunkNum int) ([]oss.FileChunk, error) {
483
+ if chunkNum <= 0 || chunkNum > 10000 {
484
+ return nil, errors.New("chunkNum invalid")
485
+ }
486
+
487
+ if int64(chunkNum) > fileSize {
488
+ return nil, errors.New("oss: chunkNum invalid")
489
+ }
490
+
491
+ var chunks []oss.FileChunk
492
+ chunk := oss.FileChunk{}
493
+ chunkN := (int64)(chunkNum)
494
+ for i := int64(0); i < chunkN; i++ {
495
+ chunk.Number = int(i + 1)
496
+ chunk.Offset = i * (fileSize / chunkN)
497
+ if i == chunkN-1 {
498
+ chunk.Size = fileSize/chunkN + fileSize%chunkN
499
+ } else {
500
+ chunk.Size = fileSize / chunkN
501
+ }
502
+ chunks = append(chunks, chunk)
503
+ }
504
+
505
+ return chunks, nil
506
+ }
507
+
508
+ // SplitFileByPartSize splits big file into parts by the size of parts.
509
+ // Splits the file by the part size. Returns the FileChunk when error is nil.
510
+ func SplitFileByPartSize(fileSize int64, chunkSize int64) ([]oss.FileChunk, error) {
511
+ if chunkSize <= 0 {
512
+ return nil, errors.New("chunkSize invalid")
513
+ }
514
+
515
+ chunkN := fileSize / chunkSize
516
+ if chunkN >= 10000 {
517
+ return nil, errors.New("Too many parts, please increase part size")
518
+ }
519
+
520
+ var chunks []oss.FileChunk
521
+ chunk := oss.FileChunk{}
522
+ for i := int64(0); i < chunkN; i++ {
523
+ chunk.Number = int(i + 1)
524
+ chunk.Offset = i * chunkSize
525
+ chunk.Size = chunkSize
526
+ chunks = append(chunks, chunk)
527
+ }
528
+
529
+ if fileSize%chunkSize > 0 {
530
+ chunk.Number = len(chunks) + 1
531
+ chunk.Offset = int64(len(chunks)) * chunkSize
532
+ chunk.Size = fileSize % chunkSize
533
+ chunks = append(chunks, chunk)
534
+ }
535
+
536
+ return chunks, nil
537
+ }
drivers/115_share/driver.go ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115_share
2
+
3
+ import (
4
+ "context"
5
+
6
+ driver115 "github.com/SheltonZhu/115driver/pkg/driver"
7
+ "github.com/alist-org/alist/v3/internal/driver"
8
+ "github.com/alist-org/alist/v3/internal/errs"
9
+ "github.com/alist-org/alist/v3/internal/model"
10
+ "github.com/alist-org/alist/v3/pkg/utils"
11
+ "golang.org/x/time/rate"
12
+ )
13
+
14
+ type Pan115Share struct {
15
+ model.Storage
16
+ Addition
17
+ client *driver115.Pan115Client
18
+ limiter *rate.Limiter
19
+ }
20
+
21
+ func (d *Pan115Share) Config() driver.Config {
22
+ return config
23
+ }
24
+
25
+ func (d *Pan115Share) GetAddition() driver.Additional {
26
+ return &d.Addition
27
+ }
28
+
29
+ func (d *Pan115Share) Init(ctx context.Context) error {
30
+ if d.LimitRate > 0 {
31
+ d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)
32
+ }
33
+
34
+ return d.login()
35
+ }
36
+
37
+ func (d *Pan115Share) WaitLimit(ctx context.Context) error {
38
+ if d.limiter != nil {
39
+ return d.limiter.Wait(ctx)
40
+ }
41
+ return nil
42
+ }
43
+
44
+ func (d *Pan115Share) Drop(ctx context.Context) error {
45
+ return nil
46
+ }
47
+
48
+ func (d *Pan115Share) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
49
+ if err := d.WaitLimit(ctx); err != nil {
50
+ return nil, err
51
+ }
52
+
53
+ files := make([]driver115.ShareFile, 0)
54
+ fileResp, err := d.client.GetShareSnap(d.ShareCode, d.ReceiveCode, dir.GetID(), driver115.QueryLimit(int(d.PageSize)))
55
+ if err != nil {
56
+ return nil, err
57
+ }
58
+ files = append(files, fileResp.Data.List...)
59
+ total := fileResp.Data.Count
60
+ count := len(fileResp.Data.List)
61
+ for total > count {
62
+ fileResp, err := d.client.GetShareSnap(
63
+ d.ShareCode, d.ReceiveCode, dir.GetID(),
64
+ driver115.QueryLimit(int(d.PageSize)), driver115.QueryOffset(count),
65
+ )
66
+ if err != nil {
67
+ return nil, err
68
+ }
69
+ files = append(files, fileResp.Data.List...)
70
+ count += len(fileResp.Data.List)
71
+ }
72
+
73
+ return utils.SliceConvert(files, transFunc)
74
+ }
75
+
76
+ func (d *Pan115Share) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
77
+ if err := d.WaitLimit(ctx); err != nil {
78
+ return nil, err
79
+ }
80
+ downloadInfo, err := d.client.DownloadByShareCode(d.ShareCode, d.ReceiveCode, file.GetID())
81
+ if err != nil {
82
+ return nil, err
83
+ }
84
+
85
+ return &model.Link{URL: downloadInfo.URL.URL}, nil
86
+ }
87
+
88
+ func (d *Pan115Share) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
89
+ return errs.NotSupport
90
+ }
91
+
92
+ func (d *Pan115Share) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
93
+ return errs.NotSupport
94
+ }
95
+
96
+ func (d *Pan115Share) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
97
+ return errs.NotSupport
98
+ }
99
+
100
+ func (d *Pan115Share) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
101
+ return errs.NotSupport
102
+ }
103
+
104
+ func (d *Pan115Share) Remove(ctx context.Context, obj model.Obj) error {
105
+ return errs.NotSupport
106
+ }
107
+
108
+ func (d *Pan115Share) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
109
+ return errs.NotSupport
110
+ }
111
+
112
+ var _ driver.Driver = (*Pan115Share)(nil)
drivers/115_share/meta.go ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115_share
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/internal/driver"
5
+ "github.com/alist-org/alist/v3/internal/op"
6
+ )
7
+
8
+ type Addition struct {
9
+ Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
10
+ QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
11
+ QRCodeSource string `json:"qrcode_source" type:"select" options:"web,android,ios,tv,alipaymini,wechatmini,qandroid" default:"linux" help:"select the QR code device, default linux"`
12
+ PageSize int64 `json:"page_size" type:"number" default:"1000" help:"list api per page size of 115 driver"`
13
+ LimitRate float64 `json:"limit_rate" type:"float" default:"2" help:"limit all api request rate (1r/[limit_rate]s)"`
14
+ ShareCode string `json:"share_code" type:"text" required:"true" help:"share code of 115 share link"`
15
+ ReceiveCode string `json:"receive_code" type:"text" required:"true" help:"receive code of 115 share link"`
16
+ driver.RootID
17
+ }
18
+
19
+ var config = driver.Config{
20
+ Name: "115 Share",
21
+ DefaultRoot: "",
22
+ // OnlyProxy: true,
23
+ // OnlyLocal: true,
24
+ CheckStatus: false,
25
+ Alert: "",
26
+ NoOverwriteUpload: true,
27
+ NoUpload: true,
28
+ }
29
+
30
+ func init() {
31
+ op.RegisterDriver(func() driver.Driver {
32
+ return &Pan115Share{}
33
+ })
34
+ }
drivers/115_share/utils.go ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115_share
2
+
3
+ import (
4
+ "fmt"
5
+ "strconv"
6
+ "time"
7
+
8
+ driver115 "github.com/SheltonZhu/115driver/pkg/driver"
9
+ "github.com/alist-org/alist/v3/internal/model"
10
+ "github.com/alist-org/alist/v3/pkg/utils"
11
+ "github.com/pkg/errors"
12
+ )
13
+
14
+ var _ model.Obj = (*FileObj)(nil)
15
+
16
+ type FileObj struct {
17
+ Size int64
18
+ Sha1 string
19
+ Utm time.Time
20
+ FileName string
21
+ isDir bool
22
+ FileID string
23
+ }
24
+
25
+ func (f *FileObj) CreateTime() time.Time {
26
+ return f.Utm
27
+ }
28
+
29
+ func (f *FileObj) GetHash() utils.HashInfo {
30
+ return utils.NewHashInfo(utils.SHA1, f.Sha1)
31
+ }
32
+
33
+ func (f *FileObj) GetSize() int64 {
34
+ return f.Size
35
+ }
36
+
37
+ func (f *FileObj) GetName() string {
38
+ return f.FileName
39
+ }
40
+
41
+ func (f *FileObj) ModTime() time.Time {
42
+ return f.Utm
43
+ }
44
+
45
+ func (f *FileObj) IsDir() bool {
46
+ return f.isDir
47
+ }
48
+
49
+ func (f *FileObj) GetID() string {
50
+ return f.FileID
51
+ }
52
+
53
+ func (f *FileObj) GetPath() string {
54
+ return ""
55
+ }
56
+
57
+ func transFunc(sf driver115.ShareFile) (model.Obj, error) {
58
+ timeInt, err := strconv.ParseInt(sf.UpdateTime, 10, 64)
59
+ if err != nil {
60
+ return nil, err
61
+ }
62
+ var (
63
+ utm = time.Unix(timeInt, 0)
64
+ isDir = (sf.IsFile == 0)
65
+ fileID = string(sf.FileID)
66
+ )
67
+ if isDir {
68
+ fileID = string(sf.CategoryID)
69
+ }
70
+ return &FileObj{
71
+ Size: int64(sf.Size),
72
+ Sha1: sf.Sha1,
73
+ Utm: utm,
74
+ FileName: string(sf.FileName),
75
+ isDir: isDir,
76
+ FileID: fileID,
77
+ }, nil
78
+ }
79
+
80
+ var UserAgent = driver115.UA115Browser
81
+
82
+ func (d *Pan115Share) login() error {
83
+ var err error
84
+ opts := []driver115.Option{
85
+ driver115.UA(UserAgent),
86
+ }
87
+ d.client = driver115.New(opts...)
88
+ if _, err := d.client.GetShareSnap(d.ShareCode, d.ReceiveCode, ""); err != nil {
89
+ return errors.Wrap(err, "failed to get share snap")
90
+ }
91
+ cr := &driver115.Credential{}
92
+ if d.QRCodeToken != "" {
93
+ s := &driver115.QRCodeSession{
94
+ UID: d.QRCodeToken,
95
+ }
96
+ if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
97
+ return errors.Wrap(err, "failed to login by qrcode")
98
+ }
99
+ d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s;KID=%s", cr.UID, cr.CID, cr.SEID, cr.KID)
100
+ d.QRCodeToken = ""
101
+ } else if d.Cookie != "" {
102
+ if err = cr.FromCookie(d.Cookie); err != nil {
103
+ return errors.Wrap(err, "failed to login by cookies")
104
+ }
105
+ d.client.ImportCredential(cr)
106
+ } else {
107
+ return errors.New("missing cookie or qrcode account")
108
+ }
109
+
110
+ return d.client.LoginCheck()
111
+ }
drivers/123/driver.go ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123
2
+
3
+ import (
4
+ "context"
5
+ "crypto/md5"
6
+ "encoding/base64"
7
+ "encoding/hex"
8
+ "fmt"
9
+ "golang.org/x/time/rate"
10
+ "io"
11
+ "net/http"
12
+ "net/url"
13
+ "sync"
14
+ "time"
15
+
16
+ "github.com/alist-org/alist/v3/drivers/base"
17
+ "github.com/alist-org/alist/v3/internal/driver"
18
+ "github.com/alist-org/alist/v3/internal/errs"
19
+ "github.com/alist-org/alist/v3/internal/model"
20
+ "github.com/alist-org/alist/v3/pkg/utils"
21
+ "github.com/aws/aws-sdk-go/aws"
22
+ "github.com/aws/aws-sdk-go/aws/credentials"
23
+ "github.com/aws/aws-sdk-go/aws/session"
24
+ "github.com/aws/aws-sdk-go/service/s3/s3manager"
25
+ "github.com/go-resty/resty/v2"
26
+ log "github.com/sirupsen/logrus"
27
+ )
28
+
29
+ type Pan123 struct {
30
+ model.Storage
31
+ Addition
32
+ apiRateLimit sync.Map
33
+ }
34
+
35
+ func (d *Pan123) Config() driver.Config {
36
+ return config
37
+ }
38
+
39
+ func (d *Pan123) GetAddition() driver.Additional {
40
+ return &d.Addition
41
+ }
42
+
43
+ func (d *Pan123) Init(ctx context.Context) error {
44
+ _, err := d.request(UserInfo, http.MethodGet, nil, nil)
45
+ return err
46
+ }
47
+
48
+ func (d *Pan123) Drop(ctx context.Context) error {
49
+ _, _ = d.request(Logout, http.MethodPost, func(req *resty.Request) {
50
+ req.SetBody(base.Json{})
51
+ }, nil)
52
+ return nil
53
+ }
54
+
55
+ func (d *Pan123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
56
+ files, err := d.getFiles(ctx, dir.GetID(), dir.GetName())
57
+ if err != nil {
58
+ return nil, err
59
+ }
60
+ return utils.SliceConvert(files, func(src File) (model.Obj, error) {
61
+ return src, nil
62
+ })
63
+ }
64
+
65
+ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
66
+ if f, ok := file.(File); ok {
67
+ //var resp DownResp
68
+ var headers map[string]string
69
+ if !utils.IsLocalIPAddr(args.IP) {
70
+ headers = map[string]string{
71
+ //"X-Real-IP": "1.1.1.1",
72
+ "X-Forwarded-For": args.IP,
73
+ }
74
+ }
75
+ data := base.Json{
76
+ "driveId": 0,
77
+ "etag": f.Etag,
78
+ "fileId": f.FileId,
79
+ "fileName": f.FileName,
80
+ "s3keyFlag": f.S3KeyFlag,
81
+ "size": f.Size,
82
+ "type": f.Type,
83
+ }
84
+ resp, err := d.request(DownloadInfo, http.MethodPost, func(req *resty.Request) {
85
+
86
+ req.SetBody(data).SetHeaders(headers)
87
+ }, nil)
88
+ if err != nil {
89
+ return nil, err
90
+ }
91
+ downloadUrl := utils.Json.Get(resp, "data", "DownloadUrl").ToString()
92
+ u, err := url.Parse(downloadUrl)
93
+ if err != nil {
94
+ return nil, err
95
+ }
96
+ nu := u.Query().Get("params")
97
+ if nu != "" {
98
+ du, _ := base64.StdEncoding.DecodeString(nu)
99
+ u, err = url.Parse(string(du))
100
+ if err != nil {
101
+ return nil, err
102
+ }
103
+ }
104
+ u_ := u.String()
105
+ log.Debug("download url: ", u_)
106
+ res, err := base.NoRedirectClient.R().SetHeader("Referer", "https://www.123pan.com/").Get(u_)
107
+ if err != nil {
108
+ return nil, err
109
+ }
110
+ log.Debug(res.String())
111
+ link := model.Link{
112
+ URL: u_,
113
+ }
114
+ log.Debugln("res code: ", res.StatusCode())
115
+ if res.StatusCode() == 302 {
116
+ link.URL = res.Header().Get("location")
117
+ } else if res.StatusCode() < 300 {
118
+ link.URL = utils.Json.Get(res.Body(), "data", "redirect_url").ToString()
119
+ }
120
+ link.Header = http.Header{
121
+ "Referer": []string{"https://www.123pan.com/"},
122
+ }
123
+ return &link, nil
124
+ } else {
125
+ return nil, fmt.Errorf("can't convert obj")
126
+ }
127
+ }
128
+
129
+ func (d *Pan123) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
130
+ data := base.Json{
131
+ "driveId": 0,
132
+ "etag": "",
133
+ "fileName": dirName,
134
+ "parentFileId": parentDir.GetID(),
135
+ "size": 0,
136
+ "type": 1,
137
+ }
138
+ _, err := d.request(Mkdir, http.MethodPost, func(req *resty.Request) {
139
+ req.SetBody(data)
140
+ }, nil)
141
+ return err
142
+ }
143
+
144
+ func (d *Pan123) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
145
+ data := base.Json{
146
+ "fileIdList": []base.Json{{"FileId": srcObj.GetID()}},
147
+ "parentFileId": dstDir.GetID(),
148
+ }
149
+ _, err := d.request(Move, http.MethodPost, func(req *resty.Request) {
150
+ req.SetBody(data)
151
+ }, nil)
152
+ return err
153
+ }
154
+
155
+ func (d *Pan123) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
156
+ data := base.Json{
157
+ "driveId": 0,
158
+ "fileId": srcObj.GetID(),
159
+ "fileName": newName,
160
+ }
161
+ _, err := d.request(Rename, http.MethodPost, func(req *resty.Request) {
162
+ req.SetBody(data)
163
+ }, nil)
164
+ return err
165
+ }
166
+
167
+ func (d *Pan123) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
168
+ return errs.NotSupport
169
+ }
170
+
171
+ func (d *Pan123) Remove(ctx context.Context, obj model.Obj) error {
172
+ if f, ok := obj.(File); ok {
173
+ data := base.Json{
174
+ "driveId": 0,
175
+ "operation": true,
176
+ "fileTrashInfoList": []File{f},
177
+ }
178
+ _, err := d.request(Trash, http.MethodPost, func(req *resty.Request) {
179
+ req.SetBody(data)
180
+ }, nil)
181
+ return err
182
+ } else {
183
+ return fmt.Errorf("can't convert obj")
184
+ }
185
+ }
186
+
187
+ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
188
+ // const DEFAULT int64 = 10485760
189
+ h := md5.New()
190
+ // need to calculate md5 of the full content
191
+ tempFile, err := stream.CacheFullInTempFile()
192
+ if err != nil {
193
+ return err
194
+ }
195
+ defer func() {
196
+ _ = tempFile.Close()
197
+ }()
198
+ if _, err = utils.CopyWithBuffer(h, tempFile); err != nil {
199
+ return err
200
+ }
201
+ _, err = tempFile.Seek(0, io.SeekStart)
202
+ if err != nil {
203
+ return err
204
+ }
205
+ etag := hex.EncodeToString(h.Sum(nil))
206
+ data := base.Json{
207
+ "driveId": 0,
208
+ "duplicate": 2, // 2->覆盖 1->重命名 0->默认
209
+ "etag": etag,
210
+ "fileName": stream.GetName(),
211
+ "parentFileId": dstDir.GetID(),
212
+ "size": stream.GetSize(),
213
+ "type": 0,
214
+ }
215
+ var resp UploadResp
216
+ res, err := d.request(UploadRequest, http.MethodPost, func(req *resty.Request) {
217
+ req.SetBody(data).SetContext(ctx)
218
+ }, &resp)
219
+ if err != nil {
220
+ return err
221
+ }
222
+ log.Debugln("upload request res: ", string(res))
223
+ if resp.Data.Reuse || resp.Data.Key == "" {
224
+ return nil
225
+ }
226
+ if resp.Data.AccessKeyId == "" || resp.Data.SecretAccessKey == "" || resp.Data.SessionToken == "" {
227
+ err = d.newUpload(ctx, &resp, stream, tempFile, up)
228
+ return err
229
+ } else {
230
+ cfg := &aws.Config{
231
+ Credentials: credentials.NewStaticCredentials(resp.Data.AccessKeyId, resp.Data.SecretAccessKey, resp.Data.SessionToken),
232
+ Region: aws.String("123pan"),
233
+ Endpoint: aws.String(resp.Data.EndPoint),
234
+ S3ForcePathStyle: aws.Bool(true),
235
+ }
236
+ s, err := session.NewSession(cfg)
237
+ if err != nil {
238
+ return err
239
+ }
240
+ uploader := s3manager.NewUploader(s)
241
+ if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
242
+ uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)
243
+ }
244
+ input := &s3manager.UploadInput{
245
+ Bucket: &resp.Data.Bucket,
246
+ Key: &resp.Data.Key,
247
+ Body: tempFile,
248
+ }
249
+ _, err = uploader.UploadWithContext(ctx, input)
250
+ }
251
+ _, err = d.request(UploadComplete, http.MethodPost, func(req *resty.Request) {
252
+ req.SetBody(base.Json{
253
+ "fileId": resp.Data.FileId,
254
+ }).SetContext(ctx)
255
+ }, nil)
256
+ return err
257
+ }
258
+
259
+ func (d *Pan123) APIRateLimit(ctx context.Context, api string) error {
260
+ value, _ := d.apiRateLimit.LoadOrStore(api,
261
+ rate.NewLimiter(rate.Every(700*time.Millisecond), 1))
262
+ limiter := value.(*rate.Limiter)
263
+
264
+ return limiter.Wait(ctx)
265
+ }
266
+
267
+ var _ driver.Driver = (*Pan123)(nil)
drivers/123/meta.go ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/internal/driver"
5
+ "github.com/alist-org/alist/v3/internal/op"
6
+ )
7
+
8
+ type Addition struct {
9
+ Username string `json:"username" required:"true"`
10
+ Password string `json:"password" required:"true"`
11
+ driver.RootID
12
+ //OrderBy string `json:"order_by" type:"select" options:"file_id,file_name,size,update_at" default:"file_name"`
13
+ //OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
14
+ AccessToken string
15
+ }
16
+
17
+ var config = driver.Config{
18
+ Name: "123Pan",
19
+ DefaultRoot: "0",
20
+ LocalSort: true,
21
+ }
22
+
23
+ func init() {
24
+ op.RegisterDriver(func() driver.Driver {
25
+ return &Pan123{}
26
+ })
27
+ }
drivers/123/types.go ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/pkg/utils"
5
+ "net/url"
6
+ "path"
7
+ "strconv"
8
+ "strings"
9
+ "time"
10
+
11
+ "github.com/alist-org/alist/v3/internal/model"
12
+ )
13
+
14
+ type File struct {
15
+ FileName string `json:"FileName"`
16
+ Size int64 `json:"Size"`
17
+ UpdateAt time.Time `json:"UpdateAt"`
18
+ FileId int64 `json:"FileId"`
19
+ Type int `json:"Type"`
20
+ Etag string `json:"Etag"`
21
+ S3KeyFlag string `json:"S3KeyFlag"`
22
+ DownloadUrl string `json:"DownloadUrl"`
23
+ }
24
+
25
+ func (f File) CreateTime() time.Time {
26
+ return f.UpdateAt
27
+ }
28
+
29
+ func (f File) GetHash() utils.HashInfo {
30
+ return utils.HashInfo{}
31
+ }
32
+
33
+ func (f File) GetPath() string {
34
+ return ""
35
+ }
36
+
37
+ func (f File) GetSize() int64 {
38
+ return f.Size
39
+ }
40
+
41
+ func (f File) GetName() string {
42
+ return f.FileName
43
+ }
44
+
45
+ func (f File) ModTime() time.Time {
46
+ return f.UpdateAt
47
+ }
48
+
49
+ func (f File) IsDir() bool {
50
+ return f.Type == 1
51
+ }
52
+
53
+ func (f File) GetID() string {
54
+ return strconv.FormatInt(f.FileId, 10)
55
+ }
56
+
57
+ func (f File) Thumb() string {
58
+ if f.DownloadUrl == "" {
59
+ return ""
60
+ }
61
+ du, err := url.Parse(f.DownloadUrl)
62
+ if err != nil {
63
+ return ""
64
+ }
65
+ du.Path = strings.TrimSuffix(du.Path, "_24_24") + "_70_70"
66
+ query := du.Query()
67
+ query.Set("w", "70")
68
+ query.Set("h", "70")
69
+ if !query.Has("type") {
70
+ query.Set("type", strings.TrimPrefix(path.Base(f.FileName), "."))
71
+ }
72
+ if !query.Has("trade_key") {
73
+ query.Set("trade_key", "123pan-thumbnail")
74
+ }
75
+ du.RawQuery = query.Encode()
76
+ return du.String()
77
+ }
78
+
79
+ var _ model.Obj = (*File)(nil)
80
+ var _ model.Thumb = (*File)(nil)
81
+
82
+ //func (f File) Thumb() string {
83
+ //
84
+ //}
85
+ //var _ model.Thumb = (*File)(nil)
86
+
87
+ type Files struct {
88
+ //BaseResp
89
+ Data struct {
90
+ Next string `json:"Next"`
91
+ Total int `json:"Total"`
92
+ InfoList []File `json:"InfoList"`
93
+ } `json:"data"`
94
+ }
95
+
96
+ //type DownResp struct {
97
+ // //BaseResp
98
+ // Data struct {
99
+ // DownloadUrl string `json:"DownloadUrl"`
100
+ // } `json:"data"`
101
+ //}
102
+
103
+ type UploadResp struct {
104
+ //BaseResp
105
+ Data struct {
106
+ AccessKeyId string `json:"AccessKeyId"`
107
+ Bucket string `json:"Bucket"`
108
+ Key string `json:"Key"`
109
+ SecretAccessKey string `json:"SecretAccessKey"`
110
+ SessionToken string `json:"SessionToken"`
111
+ FileId int64 `json:"FileId"`
112
+ Reuse bool `json:"Reuse"`
113
+ EndPoint string `json:"EndPoint"`
114
+ StorageNode string `json:"StorageNode"`
115
+ UploadId string `json:"UploadId"`
116
+ } `json:"data"`
117
+ }
118
+
119
+ type S3PreSignedURLs struct {
120
+ Data struct {
121
+ PreSignedUrls map[string]string `json:"presignedUrls"`
122
+ } `json:"data"`
123
+ }
drivers/123/upload.go ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "io"
7
+ "math"
8
+ "net/http"
9
+ "strconv"
10
+
11
+ "github.com/alist-org/alist/v3/drivers/base"
12
+ "github.com/alist-org/alist/v3/internal/driver"
13
+ "github.com/alist-org/alist/v3/internal/model"
14
+ "github.com/alist-org/alist/v3/pkg/utils"
15
+ "github.com/go-resty/resty/v2"
16
+ )
17
+
18
+ func (d *Pan123) getS3PreSignedUrls(ctx context.Context, upReq *UploadResp, start, end int) (*S3PreSignedURLs, error) {
19
+ data := base.Json{
20
+ "bucket": upReq.Data.Bucket,
21
+ "key": upReq.Data.Key,
22
+ "partNumberEnd": end,
23
+ "partNumberStart": start,
24
+ "uploadId": upReq.Data.UploadId,
25
+ "StorageNode": upReq.Data.StorageNode,
26
+ }
27
+ var s3PreSignedUrls S3PreSignedURLs
28
+ _, err := d.request(S3PreSignedUrls, http.MethodPost, func(req *resty.Request) {
29
+ req.SetBody(data).SetContext(ctx)
30
+ }, &s3PreSignedUrls)
31
+ if err != nil {
32
+ return nil, err
33
+ }
34
+ return &s3PreSignedUrls, nil
35
+ }
36
+
37
+ func (d *Pan123) getS3Auth(ctx context.Context, upReq *UploadResp, start, end int) (*S3PreSignedURLs, error) {
38
+ data := base.Json{
39
+ "StorageNode": upReq.Data.StorageNode,
40
+ "bucket": upReq.Data.Bucket,
41
+ "key": upReq.Data.Key,
42
+ "partNumberEnd": end,
43
+ "partNumberStart": start,
44
+ "uploadId": upReq.Data.UploadId,
45
+ }
46
+ var s3PreSignedUrls S3PreSignedURLs
47
+ _, err := d.request(S3Auth, http.MethodPost, func(req *resty.Request) {
48
+ req.SetBody(data).SetContext(ctx)
49
+ }, &s3PreSignedUrls)
50
+ if err != nil {
51
+ return nil, err
52
+ }
53
+ return &s3PreSignedUrls, nil
54
+ }
55
+
56
+ func (d *Pan123) completeS3(ctx context.Context, upReq *UploadResp, file model.FileStreamer, isMultipart bool) error {
57
+ data := base.Json{
58
+ "StorageNode": upReq.Data.StorageNode,
59
+ "bucket": upReq.Data.Bucket,
60
+ "fileId": upReq.Data.FileId,
61
+ "fileSize": file.GetSize(),
62
+ "isMultipart": isMultipart,
63
+ "key": upReq.Data.Key,
64
+ "uploadId": upReq.Data.UploadId,
65
+ }
66
+ _, err := d.request(UploadCompleteV2, http.MethodPost, func(req *resty.Request) {
67
+ req.SetBody(data).SetContext(ctx)
68
+ }, nil)
69
+ return err
70
+ }
71
+
72
+ func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.FileStreamer, reader io.Reader, up driver.UpdateProgress) error {
73
+ chunkSize := int64(1024 * 1024 * 16)
74
+ // fetch s3 pre signed urls
75
+ chunkCount := int(math.Ceil(float64(file.GetSize()) / float64(chunkSize)))
76
+ // only 1 batch is allowed
77
+ isMultipart := chunkCount > 1
78
+ batchSize := 1
79
+ getS3UploadUrl := d.getS3Auth
80
+ if isMultipart {
81
+ batchSize = 10
82
+ getS3UploadUrl = d.getS3PreSignedUrls
83
+ }
84
+ for i := 1; i <= chunkCount; i += batchSize {
85
+ if utils.IsCanceled(ctx) {
86
+ return ctx.Err()
87
+ }
88
+ start := i
89
+ end := i + batchSize
90
+ if end > chunkCount+1 {
91
+ end = chunkCount + 1
92
+ }
93
+ s3PreSignedUrls, err := getS3UploadUrl(ctx, upReq, start, end)
94
+ if err != nil {
95
+ return err
96
+ }
97
+ // upload each chunk
98
+ for j := start; j < end; j++ {
99
+ if utils.IsCanceled(ctx) {
100
+ return ctx.Err()
101
+ }
102
+ curSize := chunkSize
103
+ if j == chunkCount {
104
+ curSize = file.GetSize() - (int64(chunkCount)-1)*chunkSize
105
+ }
106
+ err = d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, j, end, io.LimitReader(reader, chunkSize), curSize, false, getS3UploadUrl)
107
+ if err != nil {
108
+ return err
109
+ }
110
+ up(float64(j) * 100 / float64(chunkCount))
111
+ }
112
+ }
113
+ // complete s3 upload
114
+ return d.completeS3(ctx, upReq, file, chunkCount > 1)
115
+ }
116
+
117
+ func (d *Pan123) uploadS3Chunk(ctx context.Context, upReq *UploadResp, s3PreSignedUrls *S3PreSignedURLs, cur, end int, reader io.Reader, curSize int64, retry bool, getS3UploadUrl func(ctx context.Context, upReq *UploadResp, start int, end int) (*S3PreSignedURLs, error)) error {
118
+ uploadUrl := s3PreSignedUrls.Data.PreSignedUrls[strconv.Itoa(cur)]
119
+ if uploadUrl == "" {
120
+ return fmt.Errorf("upload url is empty, s3PreSignedUrls: %+v", s3PreSignedUrls)
121
+ }
122
+ req, err := http.NewRequest("PUT", uploadUrl, reader)
123
+ if err != nil {
124
+ return err
125
+ }
126
+ req = req.WithContext(ctx)
127
+ req.ContentLength = curSize
128
+ //req.Header.Set("Content-Length", strconv.FormatInt(curSize, 10))
129
+ res, err := base.HttpClient.Do(req)
130
+ if err != nil {
131
+ return err
132
+ }
133
+ defer res.Body.Close()
134
+ if res.StatusCode == http.StatusForbidden {
135
+ if retry {
136
+ return fmt.Errorf("upload s3 chunk %d failed, status code: %d", cur, res.StatusCode)
137
+ }
138
+ // refresh s3 pre signed urls
139
+ newS3PreSignedUrls, err := getS3UploadUrl(ctx, upReq, cur, end)
140
+ if err != nil {
141
+ return err
142
+ }
143
+ s3PreSignedUrls.Data.PreSignedUrls = newS3PreSignedUrls.Data.PreSignedUrls
144
+ // retry
145
+ return d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, cur, end, reader, curSize, true, getS3UploadUrl)
146
+ }
147
+ if res.StatusCode != http.StatusOK {
148
+ body, err := io.ReadAll(res.Body)
149
+ if err != nil {
150
+ return err
151
+ }
152
+ return fmt.Errorf("upload s3 chunk %d failed, status code: %d, body: %s", cur, res.StatusCode, body)
153
+ }
154
+ return nil
155
+ }
drivers/123/util.go ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "hash/crc32"
8
+ "math"
9
+ "math/rand"
10
+ "net/http"
11
+ "net/url"
12
+ "strconv"
13
+ "strings"
14
+ "time"
15
+
16
+ "github.com/alist-org/alist/v3/drivers/base"
17
+ "github.com/alist-org/alist/v3/pkg/utils"
18
+ "github.com/go-resty/resty/v2"
19
+ jsoniter "github.com/json-iterator/go"
20
+ log "github.com/sirupsen/logrus"
21
+ )
22
+
23
+ // do others that not defined in Driver interface
24
+
25
+ const (
26
+ Api = "https://www.123pan.com/api"
27
+ AApi = "https://www.123pan.com/a/api"
28
+ BApi = "https://www.123pan.com/b/api"
29
+ LoginApi = "https://login.123pan.com/api"
30
+ MainApi = BApi
31
+ SignIn = LoginApi + "/user/sign_in"
32
+ Logout = MainApi + "/user/logout"
33
+ UserInfo = MainApi + "/user/info"
34
+ FileList = MainApi + "/file/list/new"
35
+ DownloadInfo = MainApi + "/file/download_info"
36
+ Mkdir = MainApi + "/file/upload_request"
37
+ Move = MainApi + "/file/mod_pid"
38
+ Rename = MainApi + "/file/rename"
39
+ Trash = MainApi + "/file/trash"
40
+ UploadRequest = MainApi + "/file/upload_request"
41
+ UploadComplete = MainApi + "/file/upload_complete"
42
+ S3PreSignedUrls = MainApi + "/file/s3_repare_upload_parts_batch"
43
+ S3Auth = MainApi + "/file/s3_upload_object/auth"
44
+ UploadCompleteV2 = MainApi + "/file/upload_complete/v2"
45
+ S3Complete = MainApi + "/file/s3_complete_multipart_upload"
46
+ //AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
47
+ )
48
+
49
+ func signPath(path string, os string, version string) (k string, v string) {
50
+ table := []byte{'a', 'd', 'e', 'f', 'g', 'h', 'l', 'm', 'y', 'i', 'j', 'n', 'o', 'p', 'k', 'q', 'r', 's', 't', 'u', 'b', 'c', 'v', 'w', 's', 'z'}
51
+ random := fmt.Sprintf("%.f", math.Round(1e7*rand.Float64()))
52
+ now := time.Now().In(time.FixedZone("CST", 8*3600))
53
+ timestamp := fmt.Sprint(now.Unix())
54
+ nowStr := []byte(now.Format("200601021504"))
55
+ for i := 0; i < len(nowStr); i++ {
56
+ nowStr[i] = table[nowStr[i]-48]
57
+ }
58
+ timeSign := fmt.Sprint(crc32.ChecksumIEEE(nowStr))
59
+ data := strings.Join([]string{timestamp, random, path, os, version, timeSign}, "|")
60
+ dataSign := fmt.Sprint(crc32.ChecksumIEEE([]byte(data)))
61
+ return timeSign, strings.Join([]string{timestamp, random, dataSign}, "-")
62
+ }
63
+
64
+ func GetApi(rawUrl string) string {
65
+ u, _ := url.Parse(rawUrl)
66
+ query := u.Query()
67
+ query.Add(signPath(u.Path, "web", "3"))
68
+ u.RawQuery = query.Encode()
69
+ return u.String()
70
+ }
71
+
72
+ //func GetApi(url string) string {
73
+ // vm := js.New()
74
+ // vm.Set("url", url[22:])
75
+ // r, err := vm.RunString(`
76
+ // (function(e){
77
+ // function A(t, e) {
78
+ // e = 1 < arguments.length && void 0 !== e ? e : 10;
79
+ // for (var n = function() {
80
+ // for (var t = [], e = 0; e < 256; e++) {
81
+ // for (var n = e, r = 0; r < 8; r++)
82
+ // n = 1 & n ? 3988292384 ^ n >>> 1 : n >>> 1;
83
+ // t[e] = n
84
+ // }
85
+ // return t
86
+ // }(), r = function(t) {
87
+ // t = t.replace(/\\r\\n/g, "\\n");
88
+ // for (var e = "", n = 0; n < t.length; n++) {
89
+ // var r = t.charCodeAt(n);
90
+ // r < 128 ? e += String.fromCharCode(r) : e = 127 < r && r < 2048 ? (e += String.fromCharCode(r >> 6 | 192)) + String.fromCharCode(63 & r | 128) : (e = (e += String.fromCharCode(r >> 12 | 224)) + String.fromCharCode(r >> 6 & 63 | 128)) + String.fromCharCode(63 & r | 128)
91
+ // }
92
+ // return e
93
+ // }(t), a = -1, i = 0; i < r.length; i++)
94
+ // a = a >>> 8 ^ n[255 & (a ^ r.charCodeAt(i))];
95
+ // return (a = (-1 ^ a) >>> 0).toString(e)
96
+ // }
97
+ //
98
+ // function v(t) {
99
+ // return (v = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(t) {
100
+ // return typeof t
101
+ // }
102
+ // : function(t) {
103
+ // return t && "function" == typeof Symbol && t.constructor === Symbol && t !== Symbol.prototype ? "symbol" : typeof t
104
+ // }
105
+ // )(t)
106
+ // }
107
+ //
108
+ // for (p in a = Math.round(1e7 * Math.random()),
109
+ // o = Math.round(((new Date).getTime() + 60 * (new Date).getTimezoneOffset() * 1e3 + 288e5) / 1e3).toString(),
110
+ // m = ["a", "d", "e", "f", "g", "h", "l", "m", "y", "i", "j", "n", "o", "p", "k", "q", "r", "s", "t", "u", "b", "c", "v", "w", "s", "z"],
111
+ // u = function(t, e, n) {
112
+ // var r;
113
+ // n = 2 < arguments.length && void 0 !== n ? n : 8;
114
+ // return 0 === arguments.length ? null : (r = "object" === v(t) ? t : (10 === "".concat(t).length && (t = 1e3 * Number.parseInt(t)),
115
+ // new Date(t)),
116
+ // t += 6e4 * new Date(t).getTimezoneOffset(),
117
+ // {
118
+ // y: (r = new Date(t + 36e5 * n)).getFullYear(),
119
+ // m: r.getMonth() + 1 < 10 ? "0".concat(r.getMonth() + 1) : r.getMonth() + 1,
120
+ // d: r.getDate() < 10 ? "0".concat(r.getDate()) : r.getDate(),
121
+ // h: r.getHours() < 10 ? "0".concat(r.getHours()) : r.getHours(),
122
+ // f: r.getMinutes() < 10 ? "0".concat(r.getMinutes()) : r.getMinutes()
123
+ // })
124
+ // }(o),
125
+ // h = u.y,
126
+ // g = u.m,
127
+ // l = u.d,
128
+ // c = u.h,
129
+ // u = u.f,
130
+ // d = [h, g, l, c, u].join(""),
131
+ // f = [],
132
+ // d)
133
+ // f.push(m[Number(d[p])]);
134
+ // return h = A(f.join("")),
135
+ // g = A("".concat(o, "|").concat(a, "|").concat(e, "|").concat("web", "|").concat("3", "|").concat(h)),
136
+ // "".concat(h, "=").concat(o, "-").concat(a, "-").concat(g);
137
+ // })(url)
138
+ // `)
139
+ // if err != nil {
140
+ // fmt.Println(err)
141
+ // return url
142
+ // }
143
+ // v, _ := r.Export().(string)
144
+ // return url + "?" + v
145
+ //}
146
+
147
+ func (d *Pan123) login() error {
148
+ var body base.Json
149
+ if utils.IsEmailFormat(d.Username) {
150
+ body = base.Json{
151
+ "mail": d.Username,
152
+ "password": d.Password,
153
+ "type": 2,
154
+ }
155
+ } else {
156
+ body = base.Json{
157
+ "passport": d.Username,
158
+ "password": d.Password,
159
+ "remember": true,
160
+ }
161
+ }
162
+ res, err := base.RestyClient.R().
163
+ SetHeaders(map[string]string{
164
+ "origin": "https://www.123pan.com",
165
+ "referer": "https://www.123pan.com/",
166
+ "user-agent": "Dart/2.19(dart:io)-alist",
167
+ "platform": "web",
168
+ "app-version": "3",
169
+ //"user-agent": base.UserAgent,
170
+ }).
171
+ SetBody(body).Post(SignIn)
172
+ if err != nil {
173
+ return err
174
+ }
175
+ if utils.Json.Get(res.Body(), "code").ToInt() != 200 {
176
+ err = fmt.Errorf(utils.Json.Get(res.Body(), "message").ToString())
177
+ } else {
178
+ d.AccessToken = utils.Json.Get(res.Body(), "data", "token").ToString()
179
+ }
180
+ return err
181
+ }
182
+
183
+ //func authKey(reqUrl string) (*string, error) {
184
+ // reqURL, err := url.Parse(reqUrl)
185
+ // if err != nil {
186
+ // return nil, err
187
+ // }
188
+ //
189
+ // nowUnix := time.Now().Unix()
190
+ // random := rand.Intn(0x989680)
191
+ //
192
+ // p4 := fmt.Sprintf("%d|%d|%s|%s|%s|%s", nowUnix, random, reqURL.Path, "web", "3", AuthKeySalt)
193
+ // authKey := fmt.Sprintf("%d-%d-%x", nowUnix, random, md5.Sum([]byte(p4)))
194
+ // return &authKey, nil
195
+ //}
196
+
197
+ func (d *Pan123) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
198
+ req := base.RestyClient.R()
199
+ req.SetHeaders(map[string]string{
200
+ "origin": "https://www.123pan.com",
201
+ "referer": "https://www.123pan.com/",
202
+ "authorization": "Bearer " + d.AccessToken,
203
+ "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
204
+ "platform": "web",
205
+ "app-version": "3",
206
+ //"user-agent": base.UserAgent,
207
+ })
208
+ if callback != nil {
209
+ callback(req)
210
+ }
211
+ if resp != nil {
212
+ req.SetResult(resp)
213
+ }
214
+ //authKey, err := authKey(url)
215
+ //if err != nil {
216
+ // return nil, err
217
+ //}
218
+ //req.SetQueryParam("auth-key", *authKey)
219
+ res, err := req.Execute(method, GetApi(url))
220
+ if err != nil {
221
+ return nil, err
222
+ }
223
+ body := res.Body()
224
+ code := utils.Json.Get(body, "code").ToInt()
225
+ if code != 0 {
226
+ if code == 401 {
227
+ err := d.login()
228
+ if err != nil {
229
+ return nil, err
230
+ }
231
+ return d.request(url, method, callback, resp)
232
+ }
233
+ return nil, errors.New(jsoniter.Get(body, "message").ToString())
234
+ }
235
+ return body, nil
236
+ }
237
+
238
+ func (d *Pan123) getFiles(ctx context.Context, parentId string, name string) ([]File, error) {
239
+ page := 1
240
+ total := 0
241
+ res := make([]File, 0)
242
+ // 2024-02-06 fix concurrency by 123pan
243
+ for {
244
+ if err := d.APIRateLimit(ctx, FileList); err != nil {
245
+ return nil, err
246
+ }
247
+ var resp Files
248
+ query := map[string]string{
249
+ "driveId": "0",
250
+ "limit": "100",
251
+ "next": "0",
252
+ "orderBy": "file_id",
253
+ "orderDirection": "desc",
254
+ "parentFileId": parentId,
255
+ "trashed": "false",
256
+ "SearchData": "",
257
+ "Page": strconv.Itoa(page),
258
+ "OnlyLookAbnormalFile": "0",
259
+ "event": "homeListFile",
260
+ "operateType": "4",
261
+ "inDirectSpace": "false",
262
+ }
263
+ _res, err := d.request(FileList, http.MethodGet, func(req *resty.Request) {
264
+ req.SetQueryParams(query)
265
+ }, &resp)
266
+ if err != nil {
267
+ return nil, err
268
+ }
269
+ log.Debug(string(_res))
270
+ page++
271
+ res = append(res, resp.Data.InfoList...)
272
+ total = resp.Data.Total
273
+ if len(resp.Data.InfoList) == 0 || resp.Data.Next == "-1" {
274
+ break
275
+ }
276
+ }
277
+ if len(res) != total {
278
+ log.Warnf("incorrect file count from remote at %s: expected %d, got %d", name, total, len(res))
279
+ }
280
+ return res, nil
281
+ }
drivers/123_link/driver.go ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Link
2
+
3
+ import (
4
+ "context"
5
+ stdpath "path"
6
+ "time"
7
+
8
+ "github.com/alist-org/alist/v3/internal/driver"
9
+ "github.com/alist-org/alist/v3/internal/errs"
10
+ "github.com/alist-org/alist/v3/internal/model"
11
+ "github.com/alist-org/alist/v3/pkg/utils"
12
+ )
13
+
14
+ type Pan123Link struct {
15
+ model.Storage
16
+ Addition
17
+ root *Node
18
+ }
19
+
20
+ func (d *Pan123Link) Config() driver.Config {
21
+ return config
22
+ }
23
+
24
+ func (d *Pan123Link) GetAddition() driver.Additional {
25
+ return &d.Addition
26
+ }
27
+
28
+ func (d *Pan123Link) Init(ctx context.Context) error {
29
+ node, err := BuildTree(d.OriginURLs)
30
+ if err != nil {
31
+ return err
32
+ }
33
+ node.calSize()
34
+ d.root = node
35
+ return nil
36
+ }
37
+
38
+ func (d *Pan123Link) Drop(ctx context.Context) error {
39
+ return nil
40
+ }
41
+
42
+ func (d *Pan123Link) Get(ctx context.Context, path string) (model.Obj, error) {
43
+ node := GetNodeFromRootByPath(d.root, path)
44
+ return nodeToObj(node, path)
45
+ }
46
+
47
+ func (d *Pan123Link) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
48
+ node := GetNodeFromRootByPath(d.root, dir.GetPath())
49
+ if node == nil {
50
+ return nil, errs.ObjectNotFound
51
+ }
52
+ if node.isFile() {
53
+ return nil, errs.NotFolder
54
+ }
55
+ return utils.SliceConvert(node.Children, func(node *Node) (model.Obj, error) {
56
+ return nodeToObj(node, stdpath.Join(dir.GetPath(), node.Name))
57
+ })
58
+ }
59
+
60
+ func (d *Pan123Link) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
61
+ node := GetNodeFromRootByPath(d.root, file.GetPath())
62
+ if node == nil {
63
+ return nil, errs.ObjectNotFound
64
+ }
65
+ if node.isFile() {
66
+ signUrl, err := SignURL(node.Url, d.PrivateKey, d.UID, time.Duration(d.ValidDuration)*time.Minute)
67
+ if err != nil {
68
+ return nil, err
69
+ }
70
+ return &model.Link{
71
+ URL: signUrl,
72
+ }, nil
73
+ }
74
+ return nil, errs.NotFile
75
+ }
76
+
77
+ var _ driver.Driver = (*Pan123Link)(nil)
drivers/123_link/meta.go ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Link
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/internal/driver"
5
+ "github.com/alist-org/alist/v3/internal/op"
6
+ )
7
+
8
+ type Addition struct {
9
+ OriginURLs string `json:"origin_urls" type:"text" required:"true" default:"https://vip.123pan.com/29/folder/file.mp3" help:"structure:FolderName:\n [FileSize:][Modified:]Url"`
10
+ PrivateKey string `json:"private_key"`
11
+ UID uint64 `json:"uid" type:"number"`
12
+ ValidDuration int64 `json:"valid_duration" type:"number" default:"30" help:"minutes"`
13
+ }
14
+
15
+ var config = driver.Config{
16
+ Name: "123PanLink",
17
+ }
18
+
19
+ func init() {
20
+ op.RegisterDriver(func() driver.Driver {
21
+ return &Pan123Link{}
22
+ })
23
+ }
drivers/123_link/parse.go ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Link
2
+
3
+ import (
4
+ "fmt"
5
+ url2 "net/url"
6
+ stdpath "path"
7
+ "strconv"
8
+ "strings"
9
+ "time"
10
+ )
11
+
12
+ // build tree from text, text structure definition:
13
+ /**
14
+ * FolderName:
15
+ * [FileSize:][Modified:]Url
16
+ */
17
+ /**
18
+ * For example:
19
+ * folder1:
20
+ * name1:url1
21
+ * url2
22
+ * folder2:
23
+ * url3
24
+ * url4
25
+ * url5
26
+ * folder3:
27
+ * url6
28
+ * url7
29
+ * url8
30
+ */
31
+ // if there are no name, use the last segment of url as name
32
+ func BuildTree(text string) (*Node, error) {
33
+ lines := strings.Split(text, "\n")
34
+ var root = &Node{Level: -1, Name: "root"}
35
+ stack := []*Node{root}
36
+ for _, line := range lines {
37
+ // calculate indent
38
+ indent := 0
39
+ for i := 0; i < len(line); i++ {
40
+ if line[i] != ' ' {
41
+ break
42
+ }
43
+ indent++
44
+ }
45
+ // if indent is not a multiple of 2, it is an error
46
+ if indent%2 != 0 {
47
+ return nil, fmt.Errorf("the line '%s' is not a multiple of 2", line)
48
+ }
49
+ // calculate level
50
+ level := indent / 2
51
+ line = strings.TrimSpace(line[indent:])
52
+ // if the line is empty, skip
53
+ if line == "" {
54
+ continue
55
+ }
56
+ // if level isn't greater than the level of the top of the stack
57
+ // it is not the child of the top of the stack
58
+ for level <= stack[len(stack)-1].Level {
59
+ // pop the top of the stack
60
+ stack = stack[:len(stack)-1]
61
+ }
62
+ // if the line is a folder
63
+ if isFolder(line) {
64
+ // create a new node
65
+ node := &Node{
66
+ Level: level,
67
+ Name: strings.TrimSuffix(line, ":"),
68
+ }
69
+ // add the node to the top of the stack
70
+ stack[len(stack)-1].Children = append(stack[len(stack)-1].Children, node)
71
+ // push the node to the stack
72
+ stack = append(stack, node)
73
+ } else {
74
+ // if the line is a file
75
+ // create a new node
76
+ node, err := parseFileLine(line)
77
+ if err != nil {
78
+ return nil, err
79
+ }
80
+ node.Level = level
81
+ // add the node to the top of the stack
82
+ stack[len(stack)-1].Children = append(stack[len(stack)-1].Children, node)
83
+ }
84
+ }
85
+ return root, nil
86
+ }
87
+
88
+ func isFolder(line string) bool {
89
+ return strings.HasSuffix(line, ":")
90
+ }
91
+
92
+ // line definition:
93
+ // [FileSize:][Modified:]Url
94
+ func parseFileLine(line string) (*Node, error) {
95
+ // if there is no url, it is an error
96
+ if !strings.Contains(line, "http://") && !strings.Contains(line, "https://") {
97
+ return nil, fmt.Errorf("invalid line: %s, because url is required for file", line)
98
+ }
99
+ index := strings.Index(line, "http://")
100
+ if index == -1 {
101
+ index = strings.Index(line, "https://")
102
+ }
103
+ url := line[index:]
104
+ info := line[:index]
105
+ node := &Node{
106
+ Url: url,
107
+ }
108
+ name := stdpath.Base(url)
109
+ unescape, err := url2.PathUnescape(name)
110
+ if err == nil {
111
+ name = unescape
112
+ }
113
+ node.Name = name
114
+ if index > 0 {
115
+ if !strings.HasSuffix(info, ":") {
116
+ return nil, fmt.Errorf("invalid line: %s, because file info must end with ':'", line)
117
+ }
118
+ info = info[:len(info)-1]
119
+ if info == "" {
120
+ return nil, fmt.Errorf("invalid line: %s, because file name can't be empty", line)
121
+ }
122
+ infoParts := strings.Split(info, ":")
123
+ size, err := strconv.ParseInt(infoParts[0], 10, 64)
124
+ if err != nil {
125
+ return nil, fmt.Errorf("invalid line: %s, because file size must be an integer", line)
126
+ }
127
+ node.Size = size
128
+ if len(infoParts) > 1 {
129
+ modified, err := strconv.ParseInt(infoParts[1], 10, 64)
130
+ if err != nil {
131
+ return nil, fmt.Errorf("invalid line: %s, because file modified must be an unix timestamp", line)
132
+ }
133
+ node.Modified = modified
134
+ } else {
135
+ node.Modified = time.Now().Unix()
136
+ }
137
+ }
138
+ return node, nil
139
+ }
140
+
141
+ func splitPath(path string) []string {
142
+ if path == "/" {
143
+ return []string{"root"}
144
+ }
145
+ parts := strings.Split(path, "/")
146
+ parts[0] = "root"
147
+ return parts
148
+ }
149
+
150
+ func GetNodeFromRootByPath(root *Node, path string) *Node {
151
+ return root.getByPath(splitPath(path))
152
+ }
drivers/123_link/types.go ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Link
2
+
3
+ import (
4
+ "time"
5
+
6
+ "github.com/alist-org/alist/v3/internal/errs"
7
+ "github.com/alist-org/alist/v3/internal/model"
8
+ )
9
+
10
+ // Node is a node in the folder tree
11
+ type Node struct {
12
+ Url string
13
+ Name string
14
+ Level int
15
+ Modified int64
16
+ Size int64
17
+ Children []*Node
18
+ }
19
+
20
+ func (node *Node) getByPath(paths []string) *Node {
21
+ if len(paths) == 0 || node == nil {
22
+ return nil
23
+ }
24
+ if node.Name != paths[0] {
25
+ return nil
26
+ }
27
+ if len(paths) == 1 {
28
+ return node
29
+ }
30
+ for _, child := range node.Children {
31
+ tmp := child.getByPath(paths[1:])
32
+ if tmp != nil {
33
+ return tmp
34
+ }
35
+ }
36
+ return nil
37
+ }
38
+
39
+ func (node *Node) isFile() bool {
40
+ return node.Url != ""
41
+ }
42
+
43
+ func (node *Node) calSize() int64 {
44
+ if node.isFile() {
45
+ return node.Size
46
+ }
47
+ var size int64 = 0
48
+ for _, child := range node.Children {
49
+ size += child.calSize()
50
+ }
51
+ node.Size = size
52
+ return size
53
+ }
54
+
55
+ func nodeToObj(node *Node, path string) (model.Obj, error) {
56
+ if node == nil {
57
+ return nil, errs.ObjectNotFound
58
+ }
59
+ return &model.Object{
60
+ Name: node.Name,
61
+ Size: node.Size,
62
+ Modified: time.Unix(node.Modified, 0),
63
+ IsFolder: !node.isFile(),
64
+ Path: path,
65
+ }, nil
66
+ }
drivers/123_link/util.go ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Link
2
+
3
+ import (
4
+ "crypto/md5"
5
+ "fmt"
6
+ "math/rand"
7
+ "net/url"
8
+ "time"
9
+ )
10
+
11
+ func SignURL(originURL, privateKey string, uid uint64, validDuration time.Duration) (newURL string, err error) {
12
+ if privateKey == "" {
13
+ return originURL, nil
14
+ }
15
+ var (
16
+ ts = time.Now().Add(validDuration).Unix() // 有效时间戳
17
+ rInt = rand.Int() // 随机正整数
18
+ objURL *url.URL
19
+ )
20
+ objURL, err = url.Parse(originURL)
21
+ if err != nil {
22
+ return "", err
23
+ }
24
+ authKey := fmt.Sprintf("%d-%d-%d-%x", ts, rInt, uid, md5.Sum([]byte(fmt.Sprintf("%s-%d-%d-%d-%s",
25
+ objURL.Path, ts, rInt, uid, privateKey))))
26
+ v := objURL.Query()
27
+ v.Add("auth_key", authKey)
28
+ objURL.RawQuery = v.Encode()
29
+ return objURL.String(), nil
30
+ }
drivers/123_share/driver.go ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Share
2
+
3
+ import (
4
+ "context"
5
+ "encoding/base64"
6
+ "fmt"
7
+ "golang.org/x/time/rate"
8
+ "net/http"
9
+ "net/url"
10
+ "sync"
11
+ "time"
12
+
13
+ "github.com/alist-org/alist/v3/drivers/base"
14
+ "github.com/alist-org/alist/v3/internal/driver"
15
+ "github.com/alist-org/alist/v3/internal/errs"
16
+ "github.com/alist-org/alist/v3/internal/model"
17
+ "github.com/alist-org/alist/v3/pkg/utils"
18
+ "github.com/go-resty/resty/v2"
19
+ log "github.com/sirupsen/logrus"
20
+ )
21
+
22
+ type Pan123Share struct {
23
+ model.Storage
24
+ Addition
25
+ apiRateLimit sync.Map
26
+ }
27
+
28
+ func (d *Pan123Share) Config() driver.Config {
29
+ return config
30
+ }
31
+
32
+ func (d *Pan123Share) GetAddition() driver.Additional {
33
+ return &d.Addition
34
+ }
35
+
36
+ func (d *Pan123Share) Init(ctx context.Context) error {
37
+ // TODO login / refresh token
38
+ //op.MustSaveDriverStorage(d)
39
+ return nil
40
+ }
41
+
42
+ func (d *Pan123Share) Drop(ctx context.Context) error {
43
+ return nil
44
+ }
45
+
46
+ func (d *Pan123Share) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
47
+ // TODO return the files list, required
48
+ files, err := d.getFiles(ctx, dir.GetID())
49
+ if err != nil {
50
+ return nil, err
51
+ }
52
+ return utils.SliceConvert(files, func(src File) (model.Obj, error) {
53
+ return src, nil
54
+ })
55
+ }
56
+
57
+ func (d *Pan123Share) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
58
+ // TODO return link of file, required
59
+ if f, ok := file.(File); ok {
60
+ //var resp DownResp
61
+ var headers map[string]string
62
+ if !utils.IsLocalIPAddr(args.IP) {
63
+ headers = map[string]string{
64
+ //"X-Real-IP": "1.1.1.1",
65
+ "X-Forwarded-For": args.IP,
66
+ }
67
+ }
68
+ data := base.Json{
69
+ "shareKey": d.ShareKey,
70
+ "SharePwd": d.SharePwd,
71
+ "etag": f.Etag,
72
+ "fileId": f.FileId,
73
+ "s3keyFlag": f.S3KeyFlag,
74
+ "size": f.Size,
75
+ }
76
+ resp, err := d.request(DownloadInfo, http.MethodPost, func(req *resty.Request) {
77
+ req.SetBody(data).SetHeaders(headers)
78
+ }, nil)
79
+ if err != nil {
80
+ return nil, err
81
+ }
82
+ downloadUrl := utils.Json.Get(resp, "data", "DownloadURL").ToString()
83
+ u, err := url.Parse(downloadUrl)
84
+ if err != nil {
85
+ return nil, err
86
+ }
87
+ nu := u.Query().Get("params")
88
+ if nu != "" {
89
+ du, _ := base64.StdEncoding.DecodeString(nu)
90
+ u, err = url.Parse(string(du))
91
+ if err != nil {
92
+ return nil, err
93
+ }
94
+ }
95
+ u_ := u.String()
96
+ log.Debug("download url: ", u_)
97
+ res, err := base.NoRedirectClient.R().SetHeader("Referer", "https://www.123pan.com/").Get(u_)
98
+ if err != nil {
99
+ return nil, err
100
+ }
101
+ log.Debug(res.String())
102
+ link := model.Link{
103
+ URL: u_,
104
+ }
105
+ log.Debugln("res code: ", res.StatusCode())
106
+ if res.StatusCode() == 302 {
107
+ link.URL = res.Header().Get("location")
108
+ } else if res.StatusCode() < 300 {
109
+ link.URL = utils.Json.Get(res.Body(), "data", "redirect_url").ToString()
110
+ }
111
+ link.Header = http.Header{
112
+ "Referer": []string{"https://www.123pan.com/"},
113
+ }
114
+ return &link, nil
115
+ }
116
+ return nil, fmt.Errorf("can't convert obj")
117
+ }
118
+
119
+ func (d *Pan123Share) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
120
+ // TODO create folder, optional
121
+ return errs.NotSupport
122
+ }
123
+
124
+ func (d *Pan123Share) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
125
+ // TODO move obj, optional
126
+ return errs.NotSupport
127
+ }
128
+
129
+ func (d *Pan123Share) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
130
+ // TODO rename obj, optional
131
+ return errs.NotSupport
132
+ }
133
+
134
+ func (d *Pan123Share) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
135
+ // TODO copy obj, optional
136
+ return errs.NotSupport
137
+ }
138
+
139
+ func (d *Pan123Share) Remove(ctx context.Context, obj model.Obj) error {
140
+ // TODO remove obj, optional
141
+ return errs.NotSupport
142
+ }
143
+
144
+ func (d *Pan123Share) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
145
+ // TODO upload file, optional
146
+ return errs.NotSupport
147
+ }
148
+
149
+ //func (d *Pan123Share) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
150
+ // return nil, errs.NotSupport
151
+ //}
152
+
153
+ func (d *Pan123Share) APIRateLimit(ctx context.Context, api string) error {
154
+ value, _ := d.apiRateLimit.LoadOrStore(api,
155
+ rate.NewLimiter(rate.Every(700*time.Millisecond), 1))
156
+ limiter := value.(*rate.Limiter)
157
+
158
+ return limiter.Wait(ctx)
159
+ }
160
+
161
+ var _ driver.Driver = (*Pan123Share)(nil)
drivers/123_share/meta.go ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Share
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/internal/driver"
5
+ "github.com/alist-org/alist/v3/internal/op"
6
+ )
7
+
8
+ type Addition struct {
9
+ ShareKey string `json:"sharekey" required:"true"`
10
+ SharePwd string `json:"sharepassword"`
11
+ driver.RootID
12
+ //OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
13
+ //OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
14
+ AccessToken string `json:"accesstoken" type:"text"`
15
+ }
16
+
17
+ var config = driver.Config{
18
+ Name: "123PanShare",
19
+ LocalSort: true,
20
+ OnlyLocal: false,
21
+ OnlyProxy: false,
22
+ NoCache: false,
23
+ NoUpload: true,
24
+ NeedMs: false,
25
+ DefaultRoot: "0",
26
+ CheckStatus: false,
27
+ Alert: "",
28
+ NoOverwriteUpload: false,
29
+ }
30
+
31
+ func init() {
32
+ op.RegisterDriver(func() driver.Driver {
33
+ return &Pan123Share{}
34
+ })
35
+ }
drivers/123_share/types.go ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Share
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/pkg/utils"
5
+ "net/url"
6
+ "path"
7
+ "strconv"
8
+ "strings"
9
+ "time"
10
+
11
+ "github.com/alist-org/alist/v3/internal/model"
12
+ )
13
+
14
+ type File struct {
15
+ FileName string `json:"FileName"`
16
+ Size int64 `json:"Size"`
17
+ UpdateAt time.Time `json:"UpdateAt"`
18
+ FileId int64 `json:"FileId"`
19
+ Type int `json:"Type"`
20
+ Etag string `json:"Etag"`
21
+ S3KeyFlag string `json:"S3KeyFlag"`
22
+ DownloadUrl string `json:"DownloadUrl"`
23
+ }
24
+
25
+ func (f File) GetHash() utils.HashInfo {
26
+ return utils.HashInfo{}
27
+ }
28
+
29
+ func (f File) GetPath() string {
30
+ return ""
31
+ }
32
+
33
+ func (f File) GetSize() int64 {
34
+ return f.Size
35
+ }
36
+
37
+ func (f File) GetName() string {
38
+ return f.FileName
39
+ }
40
+
41
+ func (f File) ModTime() time.Time {
42
+ return f.UpdateAt
43
+ }
44
+ func (f File) CreateTime() time.Time {
45
+ return f.UpdateAt
46
+ }
47
+
48
+ func (f File) IsDir() bool {
49
+ return f.Type == 1
50
+ }
51
+
52
+ func (f File) GetID() string {
53
+ return strconv.FormatInt(f.FileId, 10)
54
+ }
55
+
56
+ func (f File) Thumb() string {
57
+ if f.DownloadUrl == "" {
58
+ return ""
59
+ }
60
+ du, err := url.Parse(f.DownloadUrl)
61
+ if err != nil {
62
+ return ""
63
+ }
64
+ du.Path = strings.TrimSuffix(du.Path, "_24_24") + "_70_70"
65
+ query := du.Query()
66
+ query.Set("w", "70")
67
+ query.Set("h", "70")
68
+ if !query.Has("type") {
69
+ query.Set("type", strings.TrimPrefix(path.Base(f.FileName), "."))
70
+ }
71
+ if !query.Has("trade_key") {
72
+ query.Set("trade_key", "123pan-thumbnail")
73
+ }
74
+ du.RawQuery = query.Encode()
75
+ return du.String()
76
+ }
77
+
78
+ var _ model.Obj = (*File)(nil)
79
+ var _ model.Thumb = (*File)(nil)
80
+
81
+ //func (f File) Thumb() string {
82
+ //
83
+ //}
84
+ //var _ model.Thumb = (*File)(nil)
85
+
86
+ type Files struct {
87
+ //BaseResp
88
+ Data struct {
89
+ InfoList []File `json:"InfoList"`
90
+ Next string `json:"Next"`
91
+ } `json:"data"`
92
+ }
93
+
94
+ //type DownResp struct {
95
+ // //BaseResp
96
+ // Data struct {
97
+ // DownloadUrl string `json:"DownloadUrl"`
98
+ // } `json:"data"`
99
+ //}
drivers/123_share/util.go ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Share
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "hash/crc32"
8
+ "math"
9
+ "math/rand"
10
+ "net/http"
11
+ "net/url"
12
+ "strconv"
13
+ "strings"
14
+ "time"
15
+
16
+ "github.com/alist-org/alist/v3/drivers/base"
17
+ "github.com/alist-org/alist/v3/pkg/utils"
18
+ "github.com/go-resty/resty/v2"
19
+ jsoniter "github.com/json-iterator/go"
20
+ )
21
+
22
+ const (
23
+ Api = "https://www.123pan.com/api"
24
+ AApi = "https://www.123pan.com/a/api"
25
+ BApi = "https://www.123pan.com/b/api"
26
+ MainApi = BApi
27
+ FileList = MainApi + "/share/get"
28
+ DownloadInfo = MainApi + "/share/download/info"
29
+ //AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
30
+ )
31
+
32
+ func signPath(path string, os string, version string) (k string, v string) {
33
+ table := []byte{'a', 'd', 'e', 'f', 'g', 'h', 'l', 'm', 'y', 'i', 'j', 'n', 'o', 'p', 'k', 'q', 'r', 's', 't', 'u', 'b', 'c', 'v', 'w', 's', 'z'}
34
+ random := fmt.Sprintf("%.f", math.Round(1e7*rand.Float64()))
35
+ now := time.Now().In(time.FixedZone("CST", 8*3600))
36
+ timestamp := fmt.Sprint(now.Unix())
37
+ nowStr := []byte(now.Format("200601021504"))
38
+ for i := 0; i < len(nowStr); i++ {
39
+ nowStr[i] = table[nowStr[i]-48]
40
+ }
41
+ timeSign := fmt.Sprint(crc32.ChecksumIEEE(nowStr))
42
+ data := strings.Join([]string{timestamp, random, path, os, version, timeSign}, "|")
43
+ dataSign := fmt.Sprint(crc32.ChecksumIEEE([]byte(data)))
44
+ return timeSign, strings.Join([]string{timestamp, random, dataSign}, "-")
45
+ }
46
+
47
+ func GetApi(rawUrl string) string {
48
+ u, _ := url.Parse(rawUrl)
49
+ query := u.Query()
50
+ query.Add(signPath(u.Path, "web", "3"))
51
+ u.RawQuery = query.Encode()
52
+ return u.String()
53
+ }
54
+
55
+ func (d *Pan123Share) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
56
+ req := base.RestyClient.R()
57
+ req.SetHeaders(map[string]string{
58
+ "origin": "https://www.123pan.com",
59
+ "referer": "https://www.123pan.com/",
60
+ "authorization": "Bearer " + d.AccessToken,
61
+ "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
62
+ "platform": "web",
63
+ "app-version": "3",
64
+ //"user-agent": base.UserAgent,
65
+ })
66
+ if callback != nil {
67
+ callback(req)
68
+ }
69
+ if resp != nil {
70
+ req.SetResult(resp)
71
+ }
72
+ res, err := req.Execute(method, GetApi(url))
73
+ if err != nil {
74
+ return nil, err
75
+ }
76
+ body := res.Body()
77
+ code := utils.Json.Get(body, "code").ToInt()
78
+ if code != 0 {
79
+ return nil, errors.New(jsoniter.Get(body, "message").ToString())
80
+ }
81
+ return body, nil
82
+ }
83
+
84
+ func (d *Pan123Share) getFiles(ctx context.Context, parentId string) ([]File, error) {
85
+ page := 1
86
+ res := make([]File, 0)
87
+ for {
88
+ if err := d.APIRateLimit(ctx, FileList); err != nil {
89
+ return nil, err
90
+ }
91
+ var resp Files
92
+ query := map[string]string{
93
+ "limit": "100",
94
+ "next": "0",
95
+ "orderBy": "file_id",
96
+ "orderDirection": "desc",
97
+ "parentFileId": parentId,
98
+ "Page": strconv.Itoa(page),
99
+ "shareKey": d.ShareKey,
100
+ "SharePwd": d.SharePwd,
101
+ }
102
+ _, err := d.request(FileList, http.MethodGet, func(req *resty.Request) {
103
+ req.SetQueryParams(query)
104
+ }, &resp)
105
+ if err != nil {
106
+ return nil, err
107
+ }
108
+ page++
109
+ res = append(res, resp.Data.InfoList...)
110
+ if len(resp.Data.InfoList) == 0 || resp.Data.Next == "-1" {
111
+ break
112
+ }
113
+ }
114
+ return res, nil
115
+ }
116
+
117
+ // do others that not defined in Driver interface
drivers/139/driver.go ADDED
@@ -0,0 +1,653 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _139
2
+
3
+ import (
4
+ "context"
5
+ "encoding/base64"
6
+ "fmt"
7
+ "io"
8
+ "net/http"
9
+ "strconv"
10
+ "strings"
11
+ "time"
12
+
13
+ "github.com/alist-org/alist/v3/drivers/base"
14
+ "github.com/alist-org/alist/v3/internal/driver"
15
+ "github.com/alist-org/alist/v3/internal/errs"
16
+ "github.com/alist-org/alist/v3/internal/model"
17
+ "github.com/alist-org/alist/v3/pkg/cron"
18
+ "github.com/alist-org/alist/v3/pkg/utils"
19
+ "github.com/google/uuid"
20
+ log "github.com/sirupsen/logrus"
21
+ )
22
+
23
+ type Yun139 struct {
24
+ model.Storage
25
+ Addition
26
+ cron *cron.Cron
27
+ Account string
28
+ }
29
+
30
+ func (d *Yun139) Config() driver.Config {
31
+ return config
32
+ }
33
+
34
+ func (d *Yun139) GetAddition() driver.Additional {
35
+ return &d.Addition
36
+ }
37
+
38
+ func (d *Yun139) Init(ctx context.Context) error {
39
+ if d.Authorization == "" {
40
+ return fmt.Errorf("authorization is empty")
41
+ }
42
+ d.cron = cron.NewCron(time.Hour * 24 * 7)
43
+ d.cron.Do(func() {
44
+ err := d.refreshToken()
45
+ if err != nil {
46
+ log.Errorf("%+v", err)
47
+ }
48
+ })
49
+ switch d.Addition.Type {
50
+ case MetaPersonalNew:
51
+ if len(d.Addition.RootFolderID) == 0 {
52
+ d.RootFolderID = "/"
53
+ }
54
+ return nil
55
+ case MetaPersonal:
56
+ if len(d.Addition.RootFolderID) == 0 {
57
+ d.RootFolderID = "root"
58
+ }
59
+ fallthrough
60
+ case MetaFamily:
61
+ decode, err := base64.StdEncoding.DecodeString(d.Authorization)
62
+ if err != nil {
63
+ return err
64
+ }
65
+ decodeStr := string(decode)
66
+ splits := strings.Split(decodeStr, ":")
67
+ if len(splits) < 2 {
68
+ return fmt.Errorf("authorization is invalid, splits < 2")
69
+ }
70
+ d.Account = splits[1]
71
+ _, err = d.post("/orchestration/personalCloud/user/v1.0/qryUserExternInfo", base.Json{
72
+ "qryUserExternInfoReq": base.Json{
73
+ "commonAccountInfo": base.Json{
74
+ "account": d.Account,
75
+ "accountType": 1,
76
+ },
77
+ },
78
+ }, nil)
79
+ return err
80
+ default:
81
+ return errs.NotImplement
82
+ }
83
+ }
84
+
85
+ func (d *Yun139) Drop(ctx context.Context) error {
86
+ if d.cron != nil {
87
+ d.cron.Stop()
88
+ }
89
+ return nil
90
+ }
91
+
92
+ func (d *Yun139) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
93
+ switch d.Addition.Type {
94
+ case MetaPersonalNew:
95
+ return d.personalGetFiles(dir.GetID())
96
+ case MetaPersonal:
97
+ return d.getFiles(dir.GetID())
98
+ case MetaFamily:
99
+ return d.familyGetFiles(dir.GetID())
100
+ default:
101
+ return nil, errs.NotImplement
102
+ }
103
+ }
104
+
105
+ func (d *Yun139) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
106
+ var url string
107
+ var err error
108
+ switch d.Addition.Type {
109
+ case MetaPersonalNew:
110
+ url, err = d.personalGetLink(file.GetID())
111
+ case MetaPersonal:
112
+ fallthrough
113
+ case MetaFamily:
114
+ url, err = d.getLink(file.GetID())
115
+ default:
116
+ return nil, errs.NotImplement
117
+ }
118
+ if err != nil {
119
+ return nil, err
120
+ }
121
+ return &model.Link{URL: url}, nil
122
+ }
123
+
124
+ func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
125
+ var err error
126
+ switch d.Addition.Type {
127
+ case MetaPersonalNew:
128
+ data := base.Json{
129
+ "parentFileId": parentDir.GetID(),
130
+ "name": dirName,
131
+ "description": "",
132
+ "type": "folder",
133
+ "fileRenameMode": "force_rename",
134
+ }
135
+ pathname := "/hcy/file/create"
136
+ _, err = d.personalPost(pathname, data, nil)
137
+ case MetaPersonal:
138
+ data := base.Json{
139
+ "createCatalogExtReq": base.Json{
140
+ "parentCatalogID": parentDir.GetID(),
141
+ "newCatalogName": dirName,
142
+ "commonAccountInfo": base.Json{
143
+ "account": d.Account,
144
+ "accountType": 1,
145
+ },
146
+ },
147
+ }
148
+ pathname := "/orchestration/personalCloud/catalog/v1.0/createCatalogExt"
149
+ _, err = d.post(pathname, data, nil)
150
+ case MetaFamily:
151
+ cataID := parentDir.GetID()
152
+ path := cataID
153
+ data := base.Json{
154
+ "cloudID": d.CloudID,
155
+ "commonAccountInfo": base.Json{
156
+ "account": d.Account,
157
+ "accountType": 1,
158
+ },
159
+ "docLibName": dirName,
160
+ "path": path,
161
+ }
162
+ pathname := "/orchestration/familyCloud-rebuild/cloudCatalog/v1.0/createCloudDoc"
163
+ _, err = d.post(pathname, data, nil)
164
+ default:
165
+ err = errs.NotImplement
166
+ }
167
+ return err
168
+ }
169
+
170
+ func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
171
+ switch d.Addition.Type {
172
+ case MetaPersonalNew:
173
+ data := base.Json{
174
+ "fileIds": []string{srcObj.GetID()},
175
+ "toParentFileId": dstDir.GetID(),
176
+ }
177
+ pathname := "/hcy/file/batchMove"
178
+ _, err := d.personalPost(pathname, data, nil)
179
+ if err != nil {
180
+ return nil, err
181
+ }
182
+ return srcObj, nil
183
+ case MetaPersonal:
184
+ var contentInfoList []string
185
+ var catalogInfoList []string
186
+ if srcObj.IsDir() {
187
+ catalogInfoList = append(catalogInfoList, srcObj.GetID())
188
+ } else {
189
+ contentInfoList = append(contentInfoList, srcObj.GetID())
190
+ }
191
+ data := base.Json{
192
+ "createBatchOprTaskReq": base.Json{
193
+ "taskType": 3,
194
+ "actionType": "304",
195
+ "taskInfo": base.Json{
196
+ "contentInfoList": contentInfoList,
197
+ "catalogInfoList": catalogInfoList,
198
+ "newCatalogID": dstDir.GetID(),
199
+ },
200
+ "commonAccountInfo": base.Json{
201
+ "account": d.Account,
202
+ "accountType": 1,
203
+ },
204
+ },
205
+ }
206
+ pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
207
+ _, err := d.post(pathname, data, nil)
208
+ if err != nil {
209
+ return nil, err
210
+ }
211
+ return srcObj, nil
212
+ default:
213
+ return nil, errs.NotImplement
214
+ }
215
+ }
216
+
217
+ func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
218
+ var err error
219
+ switch d.Addition.Type {
220
+ case MetaPersonalNew:
221
+ data := base.Json{
222
+ "fileId": srcObj.GetID(),
223
+ "name": newName,
224
+ "description": "",
225
+ }
226
+ pathname := "/hcy/file/update"
227
+ _, err = d.personalPost(pathname, data, nil)
228
+ case MetaPersonal:
229
+ var data base.Json
230
+ var pathname string
231
+ if srcObj.IsDir() {
232
+ data = base.Json{
233
+ "catalogID": srcObj.GetID(),
234
+ "catalogName": newName,
235
+ "commonAccountInfo": base.Json{
236
+ "account": d.Account,
237
+ "accountType": 1,
238
+ },
239
+ }
240
+ pathname = "/orchestration/personalCloud/catalog/v1.0/updateCatalogInfo"
241
+ } else {
242
+ data = base.Json{
243
+ "contentID": srcObj.GetID(),
244
+ "contentName": newName,
245
+ "commonAccountInfo": base.Json{
246
+ "account": d.Account,
247
+ "accountType": 1,
248
+ },
249
+ }
250
+ pathname = "/orchestration/personalCloud/content/v1.0/updateContentInfo"
251
+ }
252
+ _, err = d.post(pathname, data, nil)
253
+ default:
254
+ err = errs.NotImplement
255
+ }
256
+ return err
257
+ }
258
+
259
+ func (d *Yun139) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
260
+ var err error
261
+ switch d.Addition.Type {
262
+ case MetaPersonalNew:
263
+ data := base.Json{
264
+ "fileIds": []string{srcObj.GetID()},
265
+ "toParentFileId": dstDir.GetID(),
266
+ }
267
+ pathname := "/hcy/file/batchCopy"
268
+ _, err := d.personalPost(pathname, data, nil)
269
+ return err
270
+ case MetaPersonal:
271
+ var contentInfoList []string
272
+ var catalogInfoList []string
273
+ if srcObj.IsDir() {
274
+ catalogInfoList = append(catalogInfoList, srcObj.GetID())
275
+ } else {
276
+ contentInfoList = append(contentInfoList, srcObj.GetID())
277
+ }
278
+ data := base.Json{
279
+ "createBatchOprTaskReq": base.Json{
280
+ "taskType": 3,
281
+ "actionType": 309,
282
+ "taskInfo": base.Json{
283
+ "contentInfoList": contentInfoList,
284
+ "catalogInfoList": catalogInfoList,
285
+ "newCatalogID": dstDir.GetID(),
286
+ },
287
+ "commonAccountInfo": base.Json{
288
+ "account": d.Account,
289
+ "accountType": 1,
290
+ },
291
+ },
292
+ }
293
+ pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
294
+ _, err = d.post(pathname, data, nil)
295
+ default:
296
+ err = errs.NotImplement
297
+ }
298
+ return err
299
+ }
300
+
301
+ func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
302
+ switch d.Addition.Type {
303
+ case MetaPersonalNew:
304
+ data := base.Json{
305
+ "fileIds": []string{obj.GetID()},
306
+ }
307
+ pathname := "/hcy/recyclebin/batchTrash"
308
+ _, err := d.personalPost(pathname, data, nil)
309
+ return err
310
+ case MetaPersonal:
311
+ fallthrough
312
+ case MetaFamily:
313
+ return errs.NotImplement
314
+ log.Warn("==========================================")
315
+ var contentInfoList []string
316
+ var catalogInfoList []string
317
+ cataID := obj.GetID()
318
+ path := ""
319
+ if strings.Contains(cataID, "/") {
320
+ lastSlashIndex := strings.LastIndex(cataID, "/")
321
+ path = cataID[0:lastSlashIndex]
322
+ cataID = cataID[lastSlashIndex+1:]
323
+ }
324
+
325
+ if obj.IsDir() {
326
+ catalogInfoList = append(catalogInfoList, cataID)
327
+ } else {
328
+ contentInfoList = append(contentInfoList, cataID)
329
+ }
330
+ data := base.Json{
331
+ "createBatchOprTaskReq": base.Json{
332
+ "taskType": 2,
333
+ "actionType": 201,
334
+ "taskInfo": base.Json{
335
+ "newCatalogID": "",
336
+ "contentInfoList": contentInfoList,
337
+ "catalogInfoList": catalogInfoList,
338
+ },
339
+ "commonAccountInfo": base.Json{
340
+ "account": d.Account,
341
+ "accountType": 1,
342
+ },
343
+ },
344
+ }
345
+ pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
346
+ if d.isFamily() {
347
+ data = base.Json{
348
+ "taskType": 2,
349
+ "sourceCloudID": d.CloudID,
350
+ "sourceCatalogType": 1002,
351
+ "path": path,
352
+ "contentList": catalogInfoList,
353
+ "catalogList": contentInfoList,
354
+ "commonAccountInfo": base.Json{
355
+ "account": d.Account,
356
+ "accountType": 1,
357
+ },
358
+ }
359
+ pathname = "/orchestration/familyCloud-rebuild/batchOprTask/v1.0/createBatchOprTask"
360
+ }
361
+ _, err := d.post(pathname, data, nil)
362
+ return err
363
+ default:
364
+ return errs.NotImplement
365
+ }
366
+ }
367
+
368
+ const (
369
+ _ = iota //ignore first value by assigning to blank identifier
370
+ KB = 1 << (10 * iota)
371
+ MB
372
+ GB
373
+ TB
374
+ )
375
+
376
+ func getPartSize(size int64) int64 {
377
+ // 网盘对于分片数量存在上限
378
+ if size/GB > 30 {
379
+ return 512 * MB
380
+ }
381
+ return 350 * MB
382
+ }
383
+
384
+
385
+
386
+ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
387
+ switch d.Addition.Type {
388
+ case MetaPersonalNew:
389
+ var err error
390
+ fullHash := stream.GetHash().GetHash(utils.SHA256)
391
+ if len(fullHash) <= 0 {
392
+ tmpF, err := stream.CacheFullInTempFile()
393
+ if err != nil {
394
+ return err
395
+ }
396
+ fullHash, err = utils.HashFile(utils.SHA256, tmpF)
397
+ if err != nil {
398
+ return err
399
+ }
400
+ }
401
+
402
+ partInfos := []PartInfo{}
403
+ var partSize = getPartSize(stream.GetSize())
404
+ part := (stream.GetSize() + partSize - 1) / partSize
405
+ if part == 0 {
406
+ part = 1
407
+ }
408
+ for i := int64(0); i < part; i++ {
409
+ if utils.IsCanceled(ctx) {
410
+ return ctx.Err()
411
+ }
412
+
413
+ start := i * partSize
414
+ byteSize := stream.GetSize() - start
415
+ if byteSize > partSize {
416
+ byteSize = partSize
417
+ }
418
+ partNumber := i + 1
419
+ partInfo := PartInfo{
420
+ PartNumber: partNumber,
421
+ PartSize: byteSize,
422
+ ParallelHashCtx: ParallelHashCtx{
423
+ PartOffset: start,
424
+ },
425
+ }
426
+ partInfos = append(partInfos, partInfo)
427
+ }
428
+
429
+ // return errs.NotImplement
430
+ data := base.Json{
431
+ "contentHash": fullHash,
432
+ "contentHashAlgorithm": "SHA256",
433
+ "contentType": "application/octet-stream",
434
+ "parallelUpload": false,
435
+ "partInfos": partInfos,
436
+ "size": stream.GetSize(),
437
+ "parentFileId": dstDir.GetID(),
438
+ "name": stream.GetName(),
439
+ "type": "file",
440
+ "fileRenameMode": "auto_rename",
441
+ }
442
+ pathname := "/hcy/file/create"
443
+ var resp PersonalUploadResp
444
+ _, err = d.personalPost(pathname, data, &resp)
445
+ if err != nil {
446
+ return err
447
+ }
448
+
449
+ if resp.Data.Exist || resp.Data.RapidUpload {
450
+ return nil
451
+ }
452
+
453
+ // Progress
454
+ p := driver.NewProgress(stream.GetSize(), up)
455
+
456
+ // Update Progress
457
+ // r := io.TeeReader(stream, p)
458
+
459
+ for index, partInfo := range resp.Data.PartInfos {
460
+
461
+ int64Index := int64(index)
462
+ start := int64Index * partSize
463
+ byteSize := stream.GetSize() - start
464
+ if byteSize > partSize {
465
+ byteSize = partSize
466
+ }
467
+
468
+ retry := 2 // 只允许重试 2 次
469
+ for attempt := 0; attempt <= retry; attempt++ {
470
+ limitReader := io.LimitReader(stream, byteSize)
471
+ // Update Progress
472
+ r := io.TeeReader(limitReader, p)
473
+ req, err := http.NewRequest("PUT", partInfo.UploadUrl, r)
474
+ if err != nil {
475
+ return err
476
+ }
477
+ req = req.WithContext(ctx)
478
+ req.Header.Set("Content-Type", "application/octet-stream")
479
+ req.Header.Set("Content-Length", fmt.Sprint(byteSize))
480
+ req.Header.Set("Origin", "https://yun.139.com")
481
+ req.Header.Set("Referer", "https://yun.139.com/")
482
+ req.ContentLength = byteSize
483
+
484
+ res, err := base.HttpClient.Do(req)
485
+ if err != nil {
486
+ return err
487
+ }
488
+
489
+ _ = res.Body.Close()
490
+ log.Debugf("%+v", res)
491
+ if res.StatusCode != http.StatusOK {
492
+ if res.StatusCode == http.StatusRequestTimeout && attempt < retry{
493
+ log.Warn("服务器返回 408,尝试重试...")
494
+ continue
495
+ }else{
496
+ return fmt.Errorf("unexpected status code: %d", res.StatusCode)
497
+ }
498
+ }
499
+ break
500
+ }
501
+ }
502
+
503
+ data = base.Json{
504
+ "contentHash": fullHash,
505
+ "contentHashAlgorithm": "SHA256",
506
+ "fileId": resp.Data.FileId,
507
+ "uploadId": resp.Data.UploadId,
508
+ }
509
+ _, err = d.personalPost("/hcy/file/complete", data, nil)
510
+ if err != nil {
511
+ return err
512
+ }
513
+ return nil
514
+ case MetaPersonal:
515
+ fallthrough
516
+ case MetaFamily:
517
+ data := base.Json{
518
+ "manualRename": 2,
519
+ "operation": 0,
520
+ "fileCount": 1,
521
+ "totalSize": 0, // 去除上传大小限制
522
+ "uploadContentList": []base.Json{{
523
+ "contentName": stream.GetName(),
524
+ "contentSize": stream.GetSize(), // 去除上传大小限制
525
+ // "digest": "5a3231986ce7a6b46e408612d385bafa"
526
+ }},
527
+ "parentCatalogID": dstDir.GetID(),
528
+ "newCatalogName": "",
529
+ "commonAccountInfo": base.Json{
530
+ "account": d.Account,
531
+ "accountType": 1,
532
+ },
533
+ }
534
+ pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest"
535
+ if d.isFamily() {
536
+ cataID := dstDir.GetID()
537
+ path := cataID
538
+ seqNo, _ := uuid.NewUUID()
539
+ data = base.Json{
540
+ "cloudID": d.CloudID,
541
+ "path": path,
542
+ "operation": 0,
543
+ "cloudType": 1,
544
+ "catalogType": 3,
545
+ "manualRename": 2,
546
+ "fileCount": 1,
547
+ "totalSize": stream.GetSize(),
548
+ "uploadContentList": []base.Json{{
549
+ "contentName": stream.GetName(),
550
+ "contentSize": stream.GetSize(),
551
+ }},
552
+ "seqNo": seqNo,
553
+ "commonAccountInfo": base.Json{
554
+ "account": d.Account,
555
+ "accountType": 1,
556
+ },
557
+ }
558
+ pathname = "/orchestration/familyCloud-rebuild/content/v1.0/getFileUploadURL"
559
+ //return errs.NotImplement
560
+ }
561
+ var resp UploadResp
562
+ _, err := d.post(pathname, data, &resp)
563
+ if err != nil {
564
+ return err
565
+ }
566
+ // Progress
567
+ p := driver.NewProgress(stream.GetSize(), up)
568
+
569
+ var partSize = getPartSize(stream.GetSize())
570
+ //var partSize = stream.GetSize()
571
+ part := (stream.GetSize() + partSize - 1) / partSize
572
+ if part == 0 {
573
+ part = 1
574
+ }
575
+ for i := int64(0); i < part; i++ {
576
+ if utils.IsCanceled(ctx) {
577
+ return ctx.Err()
578
+ }
579
+
580
+ start := i * partSize
581
+ byteSize := stream.GetSize() - start
582
+ if byteSize > partSize {
583
+ byteSize = partSize
584
+ }
585
+
586
+ retry := 2 // 只允许重试 2次
587
+ for attempt := 0; attempt <= retry; attempt++ {
588
+ limitReader := io.LimitReader(stream, byteSize)
589
+ // Update Progress
590
+ r := io.TeeReader(limitReader, p)
591
+ req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, r)
592
+ if err != nil {
593
+ return err
594
+ }
595
+
596
+ req = req.WithContext(ctx)
597
+ req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName()))
598
+ req.Header.Set("contentSize", strconv.FormatInt(stream.GetSize(), 10))
599
+ req.Header.Set("range", fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1))
600
+ req.Header.Set("uploadtaskID", resp.Data.UploadResult.UploadTaskID)
601
+ req.Header.Set("rangeType", "0")
602
+ req.ContentLength = byteSize
603
+
604
+ res, err := base.HttpClient.Do(req)
605
+ if err != nil {
606
+ return err
607
+ }
608
+ _ = res.Body.Close()
609
+ log.Debugf("%+v", res)
610
+ if res.StatusCode != http.StatusOK {
611
+ if res.StatusCode == http.StatusRequestTimeout && attempt < retry {
612
+ log.Warn("服务器返回 408,尝试重试...")
613
+ continue
614
+ }else{
615
+ return fmt.Errorf("unexpected status code: %d", res.StatusCode)
616
+ }
617
+ }
618
+ break
619
+ }
620
+ }
621
+
622
+ return nil
623
+ default:
624
+ return errs.NotImplement
625
+ }
626
+ }
627
+
628
+ func (d *Yun139) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
629
+ switch d.Addition.Type {
630
+ case MetaPersonalNew:
631
+ var resp base.Json
632
+ var uri string
633
+ data := base.Json{
634
+ "category": "video",
635
+ "fileId": args.Obj.GetID(),
636
+ }
637
+ switch args.Method {
638
+ case "video_preview":
639
+ uri = "/hcy/videoPreview/getPreviewInfo"
640
+ default:
641
+ return nil, errs.NotSupport
642
+ }
643
+ _, err := d.personalPost(uri, data, &resp)
644
+ if err != nil {
645
+ return nil, err
646
+ }
647
+ return resp["data"], nil
648
+ default:
649
+ return nil, errs.NotImplement
650
+ }
651
+ }
652
+
653
+ var _ driver.Driver = (*Yun139)(nil)
drivers/139/meta.go ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _139
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/internal/driver"
5
+ "github.com/alist-org/alist/v3/internal/op"
6
+ )
7
+
8
+ type Addition struct {
9
+ //Account string `json:"account" required:"true"`
10
+ Authorization string `json:"authorization" type:"text" required:"true"`
11
+ driver.RootID
12
+ Type string `json:"type" type:"select" options:"personal,family,personal_new" default:"personal"`
13
+ CloudID string `json:"cloud_id"`
14
+ }
15
+
16
+ var config = driver.Config{
17
+ Name: "139Yun",
18
+ LocalSort: true,
19
+ }
20
+
21
+ func init() {
22
+ op.RegisterDriver(func() driver.Driver {
23
+ return &Yun139{}
24
+ })
25
+ }
drivers/139/types.go ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _139
2
+
3
+ import (
4
+ "encoding/xml"
5
+ )
6
+
7
+ const (
8
+ MetaPersonal string = "personal"
9
+ MetaFamily string = "family"
10
+ MetaPersonalNew string = "personal_new"
11
+ )
12
+
13
+ type BaseResp struct {
14
+ Success bool `json:"success"`
15
+ Code string `json:"code"`
16
+ Message string `json:"message"`
17
+ }
18
+
19
+ type Catalog struct {
20
+ CatalogID string `json:"catalogID"`
21
+ CatalogName string `json:"catalogName"`
22
+ //CatalogType int `json:"catalogType"`
23
+ CreateTime string `json:"createTime"`
24
+ UpdateTime string `json:"updateTime"`
25
+ //IsShared bool `json:"isShared"`
26
+ //CatalogLevel int `json:"catalogLevel"`
27
+ //ShareDoneeCount int `json:"shareDoneeCount"`
28
+ //OpenType int `json:"openType"`
29
+ //ParentCatalogID string `json:"parentCatalogId"`
30
+ //DirEtag int `json:"dirEtag"`
31
+ //Tombstoned int `json:"tombstoned"`
32
+ //ProxyID interface{} `json:"proxyID"`
33
+ //Moved int `json:"moved"`
34
+ //IsFixedDir int `json:"isFixedDir"`
35
+ //IsSynced interface{} `json:"isSynced"`
36
+ //Owner string `json:"owner"`
37
+ //Modifier interface{} `json:"modifier"`
38
+ //Path string `json:"path"`
39
+ //ShareType int `json:"shareType"`
40
+ //SoftLink interface{} `json:"softLink"`
41
+ //ExtProp1 interface{} `json:"extProp1"`
42
+ //ExtProp2 interface{} `json:"extProp2"`
43
+ //ExtProp3 interface{} `json:"extProp3"`
44
+ //ExtProp4 interface{} `json:"extProp4"`
45
+ //ExtProp5 interface{} `json:"extProp5"`
46
+ //ETagOprType int `json:"ETagOprType"`
47
+ }
48
+
49
+ type Content struct {
50
+ ContentID string `json:"contentID"`
51
+ ContentName string `json:"contentName"`
52
+ //ContentSuffix string `json:"contentSuffix"`
53
+ ContentSize int64 `json:"contentSize"`
54
+ //ContentDesc string `json:"contentDesc"`
55
+ //ContentType int `json:"contentType"`
56
+ //ContentOrigin int `json:"contentOrigin"`
57
+ UpdateTime string `json:"updateTime"`
58
+ //CommentCount int `json:"commentCount"`
59
+ ThumbnailURL string `json:"thumbnailURL"`
60
+ //BigthumbnailURL string `json:"bigthumbnailURL"`
61
+ //PresentURL string `json:"presentURL"`
62
+ //PresentLURL string `json:"presentLURL"`
63
+ //PresentHURL string `json:"presentHURL"`
64
+ //ContentTAGList interface{} `json:"contentTAGList"`
65
+ //ShareDoneeCount int `json:"shareDoneeCount"`
66
+ //Safestate int `json:"safestate"`
67
+ //Transferstate int `json:"transferstate"`
68
+ //IsFocusContent int `json:"isFocusContent"`
69
+ //UpdateShareTime interface{} `json:"updateShareTime"`
70
+ //UploadTime string `json:"uploadTime"`
71
+ //OpenType int `json:"openType"`
72
+ //AuditResult int `json:"auditResult"`
73
+ //ParentCatalogID string `json:"parentCatalogId"`
74
+ //Channel string `json:"channel"`
75
+ //GeoLocFlag string `json:"geoLocFlag"`
76
+ Digest string `json:"digest"`
77
+ //Version string `json:"version"`
78
+ //FileEtag string `json:"fileEtag"`
79
+ //FileVersion string `json:"fileVersion"`
80
+ //Tombstoned int `json:"tombstoned"`
81
+ //ProxyID string `json:"proxyID"`
82
+ //Moved int `json:"moved"`
83
+ //MidthumbnailURL string `json:"midthumbnailURL"`
84
+ //Owner string `json:"owner"`
85
+ //Modifier string `json:"modifier"`
86
+ //ShareType int `json:"shareType"`
87
+ //ExtInfo struct {
88
+ // Uploader string `json:"uploader"`
89
+ // Address string `json:"address"`
90
+ //} `json:"extInfo"`
91
+ //Exif struct {
92
+ // CreateTime string `json:"createTime"`
93
+ // Longitude interface{} `json:"longitude"`
94
+ // Latitude interface{} `json:"latitude"`
95
+ // LocalSaveTime interface{} `json:"localSaveTime"`
96
+ //} `json:"exif"`
97
+ //CollectionFlag interface{} `json:"collectionFlag"`
98
+ //TreeInfo interface{} `json:"treeInfo"`
99
+ //IsShared bool `json:"isShared"`
100
+ //ETagOprType int `json:"ETagOprType"`
101
+ }
102
+
103
+ type GetDiskResp struct {
104
+ BaseResp
105
+ Data struct {
106
+ Result struct {
107
+ ResultCode string `json:"resultCode"`
108
+ ResultDesc interface{} `json:"resultDesc"`
109
+ } `json:"result"`
110
+ GetDiskResult struct {
111
+ ParentCatalogID string `json:"parentCatalogID"`
112
+ NodeCount int `json:"nodeCount"`
113
+ CatalogList []Catalog `json:"catalogList"`
114
+ ContentList []Content `json:"contentList"`
115
+ IsCompleted int `json:"isCompleted"`
116
+ } `json:"getDiskResult"`
117
+ } `json:"data"`
118
+ }
119
+
120
+ type UploadResp struct {
121
+ BaseResp
122
+ Data struct {
123
+ Result struct {
124
+ ResultCode string `json:"resultCode"`
125
+ ResultDesc interface{} `json:"resultDesc"`
126
+ } `json:"result"`
127
+ UploadResult struct {
128
+ UploadTaskID string `json:"uploadTaskID"`
129
+ RedirectionURL string `json:"redirectionUrl"`
130
+ NewContentIDList []struct {
131
+ ContentID string `json:"contentID"`
132
+ ContentName string `json:"contentName"`
133
+ IsNeedUpload string `json:"isNeedUpload"`
134
+ FileEtag int64 `json:"fileEtag"`
135
+ FileVersion int64 `json:"fileVersion"`
136
+ OverridenFlag int `json:"overridenFlag"`
137
+ } `json:"newContentIDList"`
138
+ CatalogIDList interface{} `json:"catalogIDList"`
139
+ IsSlice interface{} `json:"isSlice"`
140
+ } `json:"uploadResult"`
141
+ } `json:"data"`
142
+ }
143
+
144
+ type CloudContent struct {
145
+ ContentID string `json:"contentID"`
146
+ //Modifier string `json:"modifier"`
147
+ //Nickname string `json:"nickname"`
148
+ //CloudNickName string `json:"cloudNickName"`
149
+ ContentName string `json:"contentName"`
150
+ //ContentType int `json:"contentType"`
151
+ //ContentSuffix string `json:"contentSuffix"`
152
+ ContentSize int64 `json:"contentSize"`
153
+ //ContentDesc string `json:"contentDesc"`
154
+ CreateTime string `json:"createTime"`
155
+ //Shottime interface{} `json:"shottime"`
156
+ LastUpdateTime string `json:"lastUpdateTime"`
157
+ ThumbnailURL string `json:"thumbnailURL"`
158
+ //MidthumbnailURL string `json:"midthumbnailURL"`
159
+ //BigthumbnailURL string `json:"bigthumbnailURL"`
160
+ //PresentURL string `json:"presentURL"`
161
+ //PresentLURL string `json:"presentLURL"`
162
+ //PresentHURL string `json:"presentHURL"`
163
+ //ParentCatalogID string `json:"parentCatalogID"`
164
+ //Uploader string `json:"uploader"`
165
+ //UploaderNickName string `json:"uploaderNickName"`
166
+ //TreeInfo interface{} `json:"treeInfo"`
167
+ //UpdateTime interface{} `json:"updateTime"`
168
+ //ExtInfo struct {
169
+ // Uploader string `json:"uploader"`
170
+ //} `json:"extInfo"`
171
+ //EtagOprType interface{} `json:"etagOprType"`
172
+ }
173
+
174
+ type CloudCatalog struct {
175
+ CatalogID string `json:"catalogID"`
176
+ CatalogName string `json:"catalogName"`
177
+ //CloudID string `json:"cloudID"`
178
+ CreateTime string `json:"createTime"`
179
+ LastUpdateTime string `json:"lastUpdateTime"`
180
+ //Creator string `json:"creator"`
181
+ //CreatorNickname string `json:"creatorNickname"`
182
+ }
183
+
184
+ type QueryContentListResp struct {
185
+ BaseResp
186
+ Data struct {
187
+ Result struct {
188
+ ResultCode string `json:"resultCode"`
189
+ ResultDesc string `json:"resultDesc"`
190
+ } `json:"result"`
191
+ Path string `json:"path"`
192
+ CloudContentList []CloudContent `json:"cloudContentList"`
193
+ CloudCatalogList []CloudCatalog `json:"cloudCatalogList"`
194
+ TotalCount int `json:"totalCount"`
195
+ RecallContent interface{} `json:"recallContent"`
196
+ } `json:"data"`
197
+ }
198
+
199
+ type PartInfo struct {
200
+ PartNumber int64 `json:"partNumber"`
201
+ PartSize int64 `json:"partSize"`
202
+ ParallelHashCtx ParallelHashCtx `json:"parallelHashCtx"`
203
+ }
204
+
205
+ type ParallelHashCtx struct {
206
+ PartOffset int64 `json:"partOffset"`
207
+ }
208
+
209
+ type PersonalThumbnail struct {
210
+ Style string `json:"style"`
211
+ Url string `json:"url"`
212
+ }
213
+
214
+ type PersonalFileItem struct {
215
+ FileId string `json:"fileId"`
216
+ Name string `json:"name"`
217
+ Size int64 `json:"size"`
218
+ Type string `json:"type"`
219
+ CreatedAt string `json:"createdAt"`
220
+ UpdatedAt string `json:"updatedAt"`
221
+ Thumbnails []PersonalThumbnail `json:"thumbnailUrls"`
222
+ }
223
+
224
+ type PersonalListResp struct {
225
+ BaseResp
226
+ Data struct {
227
+ Items []PersonalFileItem `json:"items"`
228
+ NextPageCursor string `json:"nextPageCursor"`
229
+ }
230
+ }
231
+
232
+ type PersonalPartInfo struct {
233
+ PartNumber int `json:"partNumber"`
234
+ UploadUrl string `json:"uploadUrl"`
235
+ }
236
+
237
+ type PersonalUploadResp struct {
238
+ BaseResp
239
+ Data struct {
240
+ FileId string `json:"fileId"`
241
+ PartInfos []PersonalPartInfo `json:"partInfos"`
242
+ Exist bool `json:"exist"`
243
+ RapidUpload bool `json:"rapidUpload"`
244
+ UploadId string `json:"uploadId"`
245
+ }
246
+ }
247
+
248
+ type RefreshTokenResp struct {
249
+ XMLName xml.Name `xml:"root"`
250
+ Return string `xml:"return"`
251
+ Token string `xml:"token"`
252
+ Expiretime int32 `xml:"expiretime"`
253
+ AccessToken string `xml:"accessToken"`
254
+ Desc string `xml:"desc"`
255
+ }
drivers/139/util.go ADDED
@@ -0,0 +1,438 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _139
2
+
3
+ import (
4
+ "encoding/base64"
5
+ "errors"
6
+ "fmt"
7
+ "net/http"
8
+ "net/url"
9
+ "sort"
10
+ "strconv"
11
+ "strings"
12
+ "time"
13
+
14
+ "github.com/alist-org/alist/v3/drivers/base"
15
+ "github.com/alist-org/alist/v3/internal/model"
16
+ "github.com/alist-org/alist/v3/internal/op"
17
+ "github.com/alist-org/alist/v3/pkg/utils"
18
+ "github.com/alist-org/alist/v3/pkg/utils/random"
19
+ "github.com/go-resty/resty/v2"
20
+ jsoniter "github.com/json-iterator/go"
21
+ log "github.com/sirupsen/logrus"
22
+ )
23
+
24
+ // do others that not defined in Driver interface
25
+ func (d *Yun139) isFamily() bool {
26
+ return d.Type == "family"
27
+ }
28
+
29
+ func encodeURIComponent(str string) string {
30
+ r := url.QueryEscape(str)
31
+ r = strings.Replace(r, "+", "%20", -1)
32
+ r = strings.Replace(r, "%21", "!", -1)
33
+ r = strings.Replace(r, "%27", "'", -1)
34
+ r = strings.Replace(r, "%28", "(", -1)
35
+ r = strings.Replace(r, "%29", ")", -1)
36
+ r = strings.Replace(r, "%2A", "*", -1)
37
+ return r
38
+ }
39
+
40
+ func calSign(body, ts, randStr string) string {
41
+ body = encodeURIComponent(body)
42
+ strs := strings.Split(body, "")
43
+ sort.Strings(strs)
44
+ body = strings.Join(strs, "")
45
+ body = base64.StdEncoding.EncodeToString([]byte(body))
46
+ res := utils.GetMD5EncodeStr(body) + utils.GetMD5EncodeStr(ts+":"+randStr)
47
+ res = strings.ToUpper(utils.GetMD5EncodeStr(res))
48
+ return res
49
+ }
50
+
51
+ func getTime(t string) time.Time {
52
+ stamp, _ := time.ParseInLocation("20060102150405", t, utils.CNLoc)
53
+ return stamp
54
+ }
55
+
56
+ func (d *Yun139) refreshToken() error {
57
+ url := "https://aas.caiyun.feixin.10086.cn:443/tellin/authTokenRefresh.do"
58
+ var resp RefreshTokenResp
59
+ decode, err := base64.StdEncoding.DecodeString(d.Authorization)
60
+ if err != nil {
61
+ return err
62
+ }
63
+ decodeStr := string(decode)
64
+ splits := strings.Split(decodeStr, ":")
65
+ reqBody := "<root><token>" + splits[2] + "</token><account>" + splits[1] + "</account><clienttype>656</clienttype></root>"
66
+ _, err = base.RestyClient.R().
67
+ ForceContentType("application/xml").
68
+ SetBody(reqBody).
69
+ SetResult(&resp).
70
+ Post(url)
71
+ if err != nil {
72
+ return err
73
+ }
74
+ if resp.Return != "0" {
75
+ return fmt.Errorf("failed to refresh token: %s", resp.Desc)
76
+ }
77
+ d.Authorization = base64.StdEncoding.EncodeToString([]byte(splits[0] + ":" + splits[1] + ":" + resp.Token))
78
+ op.MustSaveDriverStorage(d)
79
+ return nil
80
+ }
81
+
82
+ func (d *Yun139) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
83
+ url := "https://yun.139.com" + pathname
84
+ req := base.RestyClient.R()
85
+ randStr := random.String(16)
86
+ ts := time.Now().Format("2006-01-02 15:04:05")
87
+ if callback != nil {
88
+ callback(req)
89
+ }
90
+ body, err := utils.Json.Marshal(req.Body)
91
+ if err != nil {
92
+ return nil, err
93
+ }
94
+ sign := calSign(string(body), ts, randStr)
95
+ svcType := "1"
96
+ if d.isFamily() {
97
+ svcType = "2"
98
+ }
99
+ req.SetHeaders(map[string]string{
100
+ "Accept": "application/json, text/plain, */*",
101
+ "CMS-DEVICE": "default",
102
+ "Authorization": "Basic " + d.Authorization,
103
+ "mcloud-channel": "1000101",
104
+ "mcloud-client": "10701",
105
+ //"mcloud-route": "001",
106
+ "mcloud-sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign),
107
+ //"mcloud-skey":"",
108
+ "mcloud-version": "6.6.0",
109
+ "Origin": "https://yun.139.com",
110
+ "Referer": "https://yun.139.com/w/",
111
+ "x-DeviceInfo": "||9|6.6.0|chrome|95.0.4638.69|uwIy75obnsRPIwlJSd7D9GhUvFwG96ce||macos 10.15.2||zh-CN|||",
112
+ "x-huawei-channelSrc": "10000034",
113
+ "x-inner-ntwk": "2",
114
+ "x-m4c-caller": "PC",
115
+ "x-m4c-src": "10002",
116
+ "x-SvcType": svcType,
117
+ })
118
+
119
+ var e BaseResp
120
+ req.SetResult(&e)
121
+ res, err := req.Execute(method, url)
122
+ log.Debugln(res.String())
123
+ if !e.Success {
124
+ return nil, errors.New(e.Message)
125
+ }
126
+ if resp != nil {
127
+ err = utils.Json.Unmarshal(res.Body(), resp)
128
+ if err != nil {
129
+ return nil, err
130
+ }
131
+ }
132
+ return res.Body(), nil
133
+ }
134
+ func (d *Yun139) post(pathname string, data interface{}, resp interface{}) ([]byte, error) {
135
+ return d.request(pathname, http.MethodPost, func(req *resty.Request) {
136
+ req.SetBody(data)
137
+ }, resp)
138
+ }
139
+
140
+ func (d *Yun139) getFiles(catalogID string) ([]model.Obj, error) {
141
+ start := 0
142
+ limit := 100
143
+ files := make([]model.Obj, 0)
144
+ for {
145
+ data := base.Json{
146
+ "catalogID": catalogID,
147
+ "sortDirection": 1,
148
+ "startNumber": start + 1,
149
+ "endNumber": start + limit,
150
+ "filterType": 0,
151
+ "catalogSortType": 0,
152
+ "contentSortType": 0,
153
+ "commonAccountInfo": base.Json{
154
+ "account": d.Account,
155
+ "accountType": 1,
156
+ },
157
+ }
158
+ var resp GetDiskResp
159
+ _, err := d.post("/orchestration/personalCloud/catalog/v1.0/getDisk", data, &resp)
160
+ if err != nil {
161
+ return nil, err
162
+ }
163
+ for _, catalog := range resp.Data.GetDiskResult.CatalogList {
164
+ f := model.Object{
165
+ ID: catalog.CatalogID,
166
+ Name: catalog.CatalogName,
167
+ Size: 0,
168
+ Modified: getTime(catalog.UpdateTime),
169
+ Ctime: getTime(catalog.CreateTime),
170
+ IsFolder: true,
171
+ }
172
+ files = append(files, &f)
173
+ }
174
+ for _, content := range resp.Data.GetDiskResult.ContentList {
175
+ f := model.ObjThumb{
176
+ Object: model.Object{
177
+ ID: content.ContentID,
178
+ Name: content.ContentName,
179
+ Size: content.ContentSize,
180
+ Modified: getTime(content.UpdateTime),
181
+ HashInfo: utils.NewHashInfo(utils.MD5, content.Digest),
182
+ },
183
+ Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
184
+ //Thumbnail: content.BigthumbnailURL,
185
+ }
186
+ files = append(files, &f)
187
+ }
188
+ if start+limit >= resp.Data.GetDiskResult.NodeCount {
189
+ break
190
+ }
191
+ start += limit
192
+ }
193
+ return files, nil
194
+ }
195
+
196
+ func (d *Yun139) newJson(data map[string]interface{}) base.Json {
197
+ common := map[string]interface{}{
198
+ "catalogType": 3,
199
+ "cloudID": d.CloudID,
200
+ "cloudType": 1,
201
+ "commonAccountInfo": base.Json{
202
+ "account": d.Account,
203
+ "accountType": 1,
204
+ },
205
+ }
206
+ return utils.MergeMap(data, common)
207
+ }
208
+
209
+ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
210
+
211
+ if strings.Contains(catalogID, "/") {
212
+ lastSlashIndex := strings.LastIndex(catalogID, "/")
213
+ catalogID = catalogID[lastSlashIndex+1:]
214
+ }
215
+
216
+ pageNum := 1
217
+ files := make([]model.Obj, 0)
218
+ for {
219
+ data := d.newJson(base.Json{
220
+ "catalogID": catalogID,
221
+ "contentSortType": 0,
222
+ "pageInfo": base.Json{
223
+ "pageNum": pageNum,
224
+ "pageSize": 100,
225
+ },
226
+ "sortDirection": 1,
227
+ })
228
+ var resp QueryContentListResp
229
+ _, err := d.post("/orchestration/familyCloud/content/v1.0/queryContentList", data, &resp)
230
+ if err != nil {
231
+ return nil, err
232
+ }
233
+ for _, catalog := range resp.Data.CloudCatalogList {
234
+ f := model.Object{
235
+ ID: resp.Data.Path + "/" + catalog.CatalogID,
236
+ Name: catalog.CatalogName,
237
+ Size: 0,
238
+ IsFolder: true,
239
+ Modified: getTime(catalog.LastUpdateTime),
240
+ Ctime: getTime(catalog.CreateTime),
241
+ }
242
+ files = append(files, &f)
243
+ }
244
+ for _, content := range resp.Data.CloudContentList {
245
+ f := model.ObjThumb{
246
+ Object: model.Object{
247
+ ID: content.ContentID,
248
+ Name: content.ContentName,
249
+ Size: content.ContentSize,
250
+ Modified: getTime(content.LastUpdateTime),
251
+ Ctime: getTime(content.CreateTime),
252
+ },
253
+ Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
254
+ //Thumbnail: content.BigthumbnailURL,
255
+ }
256
+ files = append(files, &f)
257
+ }
258
+ if 100*pageNum > resp.Data.TotalCount {
259
+ break
260
+ }
261
+ pageNum++
262
+ }
263
+ return files, nil
264
+ }
265
+
266
+ func (d *Yun139) getLink(contentId string) (string, error) {
267
+ data := base.Json{
268
+ "appName": "",
269
+ "contentID": contentId,
270
+ "commonAccountInfo": base.Json{
271
+ "account": d.Account,
272
+ "accountType": 1,
273
+ },
274
+ }
275
+ res, err := d.post("/orchestration/personalCloud/uploadAndDownload/v1.0/downloadRequest",
276
+ data, nil)
277
+ if err != nil {
278
+ return "", err
279
+ }
280
+ return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
281
+ }
282
+
283
+ func unicode(str string) string {
284
+ textQuoted := strconv.QuoteToASCII(str)
285
+ textUnquoted := textQuoted[1 : len(textQuoted)-1]
286
+ return textUnquoted
287
+ }
288
+
289
+ func (d *Yun139) personalRequest(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
290
+ url := "https://personal-kd-njs.yun.139.com" + pathname
291
+ req := base.RestyClient.R()
292
+ randStr := random.String(16)
293
+ ts := time.Now().Format("2006-01-02 15:04:05")
294
+ if callback != nil {
295
+ callback(req)
296
+ }
297
+ body, err := utils.Json.Marshal(req.Body)
298
+ if err != nil {
299
+ return nil, err
300
+ }
301
+ sign := calSign(string(body), ts, randStr)
302
+ svcType := "1"
303
+ if d.isFamily() {
304
+ svcType = "2"
305
+ }
306
+ req.SetHeaders(map[string]string{
307
+ "Accept": "application/json, text/plain, */*",
308
+ "Authorization": "Basic " + d.Authorization,
309
+ "Caller": "web",
310
+ "Cms-Device": "default",
311
+ "Mcloud-Channel": "1000101",
312
+ "Mcloud-Client": "10701",
313
+ "Mcloud-Route": "001",
314
+ "Mcloud-Sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign),
315
+ "Mcloud-Version": "7.13.0",
316
+ "Origin": "https://yun.139.com",
317
+ "Referer": "https://yun.139.com/w/",
318
+ "x-DeviceInfo": "||9|7.13.0|chrome|120.0.0.0|||windows 10||zh-CN|||",
319
+ "x-huawei-channelSrc": "10000034",
320
+ "x-inner-ntwk": "2",
321
+ "x-m4c-caller": "PC",
322
+ "x-m4c-src": "10002",
323
+ "x-SvcType": svcType,
324
+ "X-Yun-Api-Version": "v1",
325
+ "X-Yun-App-Channel": "10000034",
326
+ "X-Yun-Channel-Source": "10000034",
327
+ "X-Yun-Client-Info": "||9|7.13.0|chrome|120.0.0.0|||windows 10||zh-CN|||dW5kZWZpbmVk||",
328
+ "X-Yun-Module-Type": "100",
329
+ "X-Yun-Svc-Type": "1",
330
+ })
331
+
332
+ var e BaseResp
333
+ req.SetResult(&e)
334
+ res, err := req.Execute(method, url)
335
+ if err != nil {
336
+ return nil, err
337
+ }
338
+ log.Debugln(res.String())
339
+ if !e.Success {
340
+ return nil, errors.New(e.Message)
341
+ }
342
+ if resp != nil {
343
+ err = utils.Json.Unmarshal(res.Body(), resp)
344
+ if err != nil {
345
+ return nil, err
346
+ }
347
+ }
348
+ return res.Body(), nil
349
+ }
350
+ func (d *Yun139) personalPost(pathname string, data interface{}, resp interface{}) ([]byte, error) {
351
+ return d.personalRequest(pathname, http.MethodPost, func(req *resty.Request) {
352
+ req.SetBody(data)
353
+ }, resp)
354
+ }
355
+
356
+ func getPersonalTime(t string) time.Time {
357
+ stamp, err := time.ParseInLocation("2006-01-02T15:04:05.999-07:00", t, utils.CNLoc)
358
+ if err != nil {
359
+ panic(err)
360
+ }
361
+ return stamp
362
+ }
363
+
364
+ func (d *Yun139) personalGetFiles(fileId string) ([]model.Obj, error) {
365
+ files := make([]model.Obj, 0)
366
+ nextPageCursor := ""
367
+ for {
368
+ data := base.Json{
369
+ "imageThumbnailStyleList": []string{"Small", "Large"},
370
+ "orderBy": "updated_at",
371
+ "orderDirection": "DESC",
372
+ "pageInfo": base.Json{
373
+ "pageCursor": nextPageCursor,
374
+ "pageSize": 100,
375
+ },
376
+ "parentFileId": fileId,
377
+ }
378
+ var resp PersonalListResp
379
+ _, err := d.personalPost("/hcy/file/list", data, &resp)
380
+ if err != nil {
381
+ return nil, err
382
+ }
383
+ nextPageCursor = resp.Data.NextPageCursor
384
+ for _, item := range resp.Data.Items {
385
+ var isFolder = (item.Type == "folder")
386
+ var f model.Obj
387
+ if isFolder {
388
+ f = &model.Object{
389
+ ID: item.FileId,
390
+ Name: item.Name,
391
+ Size: 0,
392
+ Modified: getPersonalTime(item.UpdatedAt),
393
+ Ctime: getPersonalTime(item.CreatedAt),
394
+ IsFolder: isFolder,
395
+ }
396
+ } else {
397
+ var Thumbnails = item.Thumbnails
398
+ var ThumbnailUrl string
399
+ if len(Thumbnails) > 0 {
400
+ ThumbnailUrl = Thumbnails[len(Thumbnails)-1].Url
401
+ }
402
+ f = &model.ObjThumb{
403
+ Object: model.Object{
404
+ ID: item.FileId,
405
+ Name: item.Name,
406
+ Size: item.Size,
407
+ Modified: getPersonalTime(item.UpdatedAt),
408
+ Ctime: getPersonalTime(item.CreatedAt),
409
+ IsFolder: isFolder,
410
+ },
411
+ Thumbnail: model.Thumbnail{Thumbnail: ThumbnailUrl},
412
+ }
413
+ }
414
+ files = append(files, f)
415
+ }
416
+ if len(nextPageCursor) == 0 {
417
+ break
418
+ }
419
+ }
420
+ return files, nil
421
+ }
422
+
423
+ func (d *Yun139) personalGetLink(fileId string) (string, error) {
424
+ data := base.Json{
425
+ "fileId": fileId,
426
+ }
427
+ res, err := d.personalPost("/hcy/file/getDownloadUrl",
428
+ data, nil)
429
+ if err != nil {
430
+ return "", err
431
+ }
432
+ var cdnUrl = jsoniter.Get(res, "data", "cdnUrl").ToString()
433
+ if cdnUrl != "" {
434
+ return cdnUrl, nil
435
+ } else {
436
+ return jsoniter.Get(res, "data", "url").ToString(), nil
437
+ }
438
+ }
drivers/189/driver.go ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _189
2
+
3
+ import (
4
+ "context"
5
+ "net/http"
6
+ "strings"
7
+
8
+ "github.com/alist-org/alist/v3/drivers/base"
9
+ "github.com/alist-org/alist/v3/internal/driver"
10
+ "github.com/alist-org/alist/v3/internal/model"
11
+ "github.com/alist-org/alist/v3/pkg/utils"
12
+ "github.com/go-resty/resty/v2"
13
+ log "github.com/sirupsen/logrus"
14
+ )
15
+
16
+ type Cloud189 struct {
17
+ model.Storage
18
+ Addition
19
+ client *resty.Client
20
+ rsa Rsa
21
+ sessionKey string
22
+ }
23
+
24
+ func (d *Cloud189) Config() driver.Config {
25
+ return config
26
+ }
27
+
28
+ func (d *Cloud189) GetAddition() driver.Additional {
29
+ return &d.Addition
30
+ }
31
+
32
+ func (d *Cloud189) Init(ctx context.Context) error {
33
+ d.client = base.NewRestyClient().
34
+ SetHeader("Referer", "https://cloud.189.cn/")
35
+ return d.newLogin()
36
+ }
37
+
38
+ func (d *Cloud189) Drop(ctx context.Context) error {
39
+ return nil
40
+ }
41
+
42
+ func (d *Cloud189) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
43
+ return d.getFiles(dir.GetID())
44
+ }
45
+
46
+ func (d *Cloud189) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
47
+ var resp DownResp
48
+ u := "https://cloud.189.cn/api/portal/getFileInfo.action"
49
+ _, err := d.request(u, http.MethodGet, func(req *resty.Request) {
50
+ req.SetQueryParam("fileId", file.GetID())
51
+ }, &resp)
52
+ if err != nil {
53
+ return nil, err
54
+ }
55
+ client := resty.NewWithClient(d.client.GetClient()).SetRedirectPolicy(
56
+ resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
57
+ return http.ErrUseLastResponse
58
+ }))
59
+ res, err := client.R().SetHeader("User-Agent", base.UserAgent).Get("https:" + resp.FileDownloadUrl)
60
+ if err != nil {
61
+ return nil, err
62
+ }
63
+ log.Debugln(res.Status())
64
+ log.Debugln(res.String())
65
+ link := model.Link{}
66
+ log.Debugln("first url:", resp.FileDownloadUrl)
67
+ if res.StatusCode() == 302 {
68
+ link.URL = res.Header().Get("location")
69
+ log.Debugln("second url:", link.URL)
70
+ _, _ = client.R().Get(link.URL)
71
+ if res.StatusCode() == 302 {
72
+ link.URL = res.Header().Get("location")
73
+ }
74
+ log.Debugln("third url:", link.URL)
75
+ } else {
76
+ link.URL = resp.FileDownloadUrl
77
+ }
78
+ link.URL = strings.Replace(link.URL, "http://", "https://", 1)
79
+ return &link, nil
80
+ }
81
+
82
+ func (d *Cloud189) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
83
+ form := map[string]string{
84
+ "parentFolderId": parentDir.GetID(),
85
+ "folderName": dirName,
86
+ }
87
+ _, err := d.request("https://cloud.189.cn/api/open/file/createFolder.action", http.MethodPost, func(req *resty.Request) {
88
+ req.SetFormData(form)
89
+ }, nil)
90
+ return err
91
+ }
92
+
93
+ func (d *Cloud189) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
94
+ isFolder := 0
95
+ if srcObj.IsDir() {
96
+ isFolder = 1
97
+ }
98
+ taskInfos := []base.Json{
99
+ {
100
+ "fileId": srcObj.GetID(),
101
+ "fileName": srcObj.GetName(),
102
+ "isFolder": isFolder,
103
+ },
104
+ }
105
+ taskInfosBytes, err := utils.Json.Marshal(taskInfos)
106
+ if err != nil {
107
+ return err
108
+ }
109
+ form := map[string]string{
110
+ "type": "MOVE",
111
+ "targetFolderId": dstDir.GetID(),
112
+ "taskInfos": string(taskInfosBytes),
113
+ }
114
+ _, err = d.request("https://cloud.189.cn/api/open/batch/createBatchTask.action", http.MethodPost, func(req *resty.Request) {
115
+ req.SetFormData(form)
116
+ }, nil)
117
+ return err
118
+ }
119
+
120
+ func (d *Cloud189) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
121
+ url := "https://cloud.189.cn/api/open/file/renameFile.action"
122
+ idKey := "fileId"
123
+ nameKey := "destFileName"
124
+ if srcObj.IsDir() {
125
+ url = "https://cloud.189.cn/api/open/file/renameFolder.action"
126
+ idKey = "folderId"
127
+ nameKey = "destFolderName"
128
+ }
129
+ form := map[string]string{
130
+ idKey: srcObj.GetID(),
131
+ nameKey: newName,
132
+ }
133
+ _, err := d.request(url, http.MethodPost, func(req *resty.Request) {
134
+ req.SetFormData(form)
135
+ }, nil)
136
+ return err
137
+ }
138
+
139
+ func (d *Cloud189) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
140
+ isFolder := 0
141
+ if srcObj.IsDir() {
142
+ isFolder = 1
143
+ }
144
+ taskInfos := []base.Json{
145
+ {
146
+ "fileId": srcObj.GetID(),
147
+ "fileName": srcObj.GetName(),
148
+ "isFolder": isFolder,
149
+ },
150
+ }
151
+ taskInfosBytes, err := utils.Json.Marshal(taskInfos)
152
+ if err != nil {
153
+ return err
154
+ }
155
+ form := map[string]string{
156
+ "type": "COPY",
157
+ "targetFolderId": dstDir.GetID(),
158
+ "taskInfos": string(taskInfosBytes),
159
+ }
160
+ _, err = d.request("https://cloud.189.cn/api/open/batch/createBatchTask.action", http.MethodPost, func(req *resty.Request) {
161
+ req.SetFormData(form)
162
+ }, nil)
163
+ return err
164
+ }
165
+
166
+ func (d *Cloud189) Remove(ctx context.Context, obj model.Obj) error {
167
+ isFolder := 0
168
+ if obj.IsDir() {
169
+ isFolder = 1
170
+ }
171
+ taskInfos := []base.Json{
172
+ {
173
+ "fileId": obj.GetID(),
174
+ "fileName": obj.GetName(),
175
+ "isFolder": isFolder,
176
+ },
177
+ }
178
+ taskInfosBytes, err := utils.Json.Marshal(taskInfos)
179
+ if err != nil {
180
+ return err
181
+ }
182
+ form := map[string]string{
183
+ "type": "DELETE",
184
+ "targetFolderId": "",
185
+ "taskInfos": string(taskInfosBytes),
186
+ }
187
+ _, err = d.request("https://cloud.189.cn/api/open/batch/createBatchTask.action", http.MethodPost, func(req *resty.Request) {
188
+ req.SetFormData(form)
189
+ }, nil)
190
+ return err
191
+ }
192
+
193
+ func (d *Cloud189) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
194
+ return d.newUpload(ctx, dstDir, stream, up)
195
+ }
196
+
197
+ var _ driver.Driver = (*Cloud189)(nil)
drivers/189/help.go ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _189
2
+
3
+ import (
4
+ "bytes"
5
+ "crypto/aes"
6
+ "crypto/hmac"
7
+ "crypto/md5"
8
+ "crypto/rand"
9
+ "crypto/rsa"
10
+ "crypto/sha1"
11
+ "crypto/x509"
12
+ "encoding/base64"
13
+ "encoding/hex"
14
+ "encoding/pem"
15
+ "fmt"
16
+ "net/url"
17
+ "regexp"
18
+ "strconv"
19
+ "strings"
20
+
21
+ myrand "github.com/alist-org/alist/v3/pkg/utils/random"
22
+ log "github.com/sirupsen/logrus"
23
+ )
24
+
25
+ func random() string {
26
+ return fmt.Sprintf("0.%17v", myrand.Rand.Int63n(100000000000000000))
27
+ }
28
+
29
+ func RsaEncode(origData []byte, j_rsakey string, hex bool) string {
30
+ publicKey := []byte("-----BEGIN PUBLIC KEY-----\n" + j_rsakey + "\n-----END PUBLIC KEY-----")
31
+ block, _ := pem.Decode(publicKey)
32
+ pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes)
33
+ pub := pubInterface.(*rsa.PublicKey)
34
+ b, err := rsa.EncryptPKCS1v15(rand.Reader, pub, origData)
35
+ if err != nil {
36
+ log.Errorf("err: %s", err.Error())
37
+ }
38
+ res := base64.StdEncoding.EncodeToString(b)
39
+ if hex {
40
+ return b64tohex(res)
41
+ }
42
+ return res
43
+ }
44
+
45
+ var b64map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
46
+
47
+ var BI_RM = "0123456789abcdefghijklmnopqrstuvwxyz"
48
+
49
+ func int2char(a int) string {
50
+ return strings.Split(BI_RM, "")[a]
51
+ }
52
+
53
+ func b64tohex(a string) string {
54
+ d := ""
55
+ e := 0
56
+ c := 0
57
+ for i := 0; i < len(a); i++ {
58
+ m := strings.Split(a, "")[i]
59
+ if m != "=" {
60
+ v := strings.Index(b64map, m)
61
+ if 0 == e {
62
+ e = 1
63
+ d += int2char(v >> 2)
64
+ c = 3 & v
65
+ } else if 1 == e {
66
+ e = 2
67
+ d += int2char(c<<2 | v>>4)
68
+ c = 15 & v
69
+ } else if 2 == e {
70
+ e = 3
71
+ d += int2char(c)
72
+ d += int2char(v >> 2)
73
+ c = 3 & v
74
+ } else {
75
+ e = 0
76
+ d += int2char(c<<2 | v>>4)
77
+ d += int2char(15 & v)
78
+ }
79
+ }
80
+ }
81
+ if e == 1 {
82
+ d += int2char(c << 2)
83
+ }
84
+ return d
85
+ }
86
+
87
+ func qs(form map[string]string) string {
88
+ f := make(url.Values)
89
+ for k, v := range form {
90
+ f.Set(k, v)
91
+ }
92
+ return EncodeParam(f)
93
+ //strList := make([]string, 0)
94
+ //for k, v := range form {
95
+ // strList = append(strList, fmt.Sprintf("%s=%s", k, url.QueryEscape(v)))
96
+ //}
97
+ //return strings.Join(strList, "&")
98
+ }
99
+
100
+ func EncodeParam(v url.Values) string {
101
+ if v == nil {
102
+ return ""
103
+ }
104
+ var buf strings.Builder
105
+ keys := make([]string, 0, len(v))
106
+ for k := range v {
107
+ keys = append(keys, k)
108
+ }
109
+ for _, k := range keys {
110
+ vs := v[k]
111
+ for _, v := range vs {
112
+ if buf.Len() > 0 {
113
+ buf.WriteByte('&')
114
+ }
115
+ buf.WriteString(k)
116
+ buf.WriteByte('=')
117
+ //if k == "fileName" {
118
+ // buf.WriteString(encode(v))
119
+ //} else {
120
+ buf.WriteString(v)
121
+ //}
122
+ }
123
+ }
124
+ return buf.String()
125
+ }
126
+
127
+ func encode(str string) string {
128
+ //str = strings.ReplaceAll(str, "%", "%25")
129
+ //str = strings.ReplaceAll(str, "&", "%26")
130
+ //str = strings.ReplaceAll(str, "+", "%2B")
131
+ //return str
132
+ return url.QueryEscape(str)
133
+ }
134
+
135
+ func AesEncrypt(data, key []byte) []byte {
136
+ block, _ := aes.NewCipher(key)
137
+ if block == nil {
138
+ return []byte{}
139
+ }
140
+ data = PKCS7Padding(data, block.BlockSize())
141
+ decrypted := make([]byte, len(data))
142
+ size := block.BlockSize()
143
+ for bs, be := 0, size; bs < len(data); bs, be = bs+size, be+size {
144
+ block.Encrypt(decrypted[bs:be], data[bs:be])
145
+ }
146
+ return decrypted
147
+ }
148
+
149
+ func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
150
+ padding := blockSize - len(ciphertext)%blockSize
151
+ padtext := bytes.Repeat([]byte{byte(padding)}, padding)
152
+ return append(ciphertext, padtext...)
153
+ }
154
+
155
+ func hmacSha1(data string, secret string) string {
156
+ h := hmac.New(sha1.New, []byte(secret))
157
+ h.Write([]byte(data))
158
+ return hex.EncodeToString(h.Sum(nil))
159
+ }
160
+
161
+ func getMd5(data []byte) []byte {
162
+ h := md5.New()
163
+ h.Write(data)
164
+ return h.Sum(nil)
165
+ }
166
+
167
+ func decodeURIComponent(str string) string {
168
+ r, _ := url.PathUnescape(str)
169
+ //r = strings.ReplaceAll(r, " ", "+")
170
+ return r
171
+ }
172
+
173
+ func Random(v string) string {
174
+ reg := regexp.MustCompilePOSIX("[xy]")
175
+ data := reg.ReplaceAllFunc([]byte(v), func(msg []byte) []byte {
176
+ var i int64
177
+ t := int64(16 * myrand.Rand.Float32())
178
+ if msg[0] == 120 {
179
+ i = t
180
+ } else {
181
+ i = 3&t | 8
182
+ }
183
+ return []byte(strconv.FormatInt(i, 16))
184
+ })
185
+ return string(data)
186
+ }
drivers/189/login.go ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _189
2
+
3
+ import (
4
+ "errors"
5
+ "strconv"
6
+
7
+ "github.com/alist-org/alist/v3/pkg/utils"
8
+ log "github.com/sirupsen/logrus"
9
+ )
10
+
11
+ type AppConf struct {
12
+ Data struct {
13
+ AccountType string `json:"accountType"`
14
+ AgreementCheck string `json:"agreementCheck"`
15
+ AppKey string `json:"appKey"`
16
+ ClientType int `json:"clientType"`
17
+ IsOauth2 bool `json:"isOauth2"`
18
+ LoginSort string `json:"loginSort"`
19
+ MailSuffix string `json:"mailSuffix"`
20
+ PageKey string `json:"pageKey"`
21
+ ParamId string `json:"paramId"`
22
+ RegReturnUrl string `json:"regReturnUrl"`
23
+ ReqId string `json:"reqId"`
24
+ ReturnUrl string `json:"returnUrl"`
25
+ ShowFeedback string `json:"showFeedback"`
26
+ ShowPwSaveName string `json:"showPwSaveName"`
27
+ ShowQrSaveName string `json:"showQrSaveName"`
28
+ ShowSmsSaveName string `json:"showSmsSaveName"`
29
+ Sso string `json:"sso"`
30
+ } `json:"data"`
31
+ Msg string `json:"msg"`
32
+ Result string `json:"result"`
33
+ }
34
+
35
+ type EncryptConf struct {
36
+ Result int `json:"result"`
37
+ Data struct {
38
+ UpSmsOn string `json:"upSmsOn"`
39
+ Pre string `json:"pre"`
40
+ PreDomain string `json:"preDomain"`
41
+ PubKey string `json:"pubKey"`
42
+ } `json:"data"`
43
+ }
44
+
45
+ func (d *Cloud189) newLogin() error {
46
+ url := "https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action"
47
+ res, err := d.client.R().Get(url)
48
+ if err != nil {
49
+ return err
50
+ }
51
+ // Is logged in
52
+ redirectURL := res.RawResponse.Request.URL
53
+ if redirectURL.String() == "https://cloud.189.cn/web/main" {
54
+ return nil
55
+ }
56
+ lt := redirectURL.Query().Get("lt")
57
+ reqId := redirectURL.Query().Get("reqId")
58
+ appId := redirectURL.Query().Get("appId")
59
+ headers := map[string]string{
60
+ "lt": lt,
61
+ "reqid": reqId,
62
+ "referer": redirectURL.String(),
63
+ "origin": "https://open.e.189.cn",
64
+ }
65
+ // get app Conf
66
+ var appConf AppConf
67
+ res, err = d.client.R().SetHeaders(headers).SetFormData(map[string]string{
68
+ "version": "2.0",
69
+ "appKey": appId,
70
+ }).SetResult(&appConf).Post("https://open.e.189.cn/api/logbox/oauth2/appConf.do")
71
+ if err != nil {
72
+ return err
73
+ }
74
+ log.Debugf("189 AppConf resp body: %s", res.String())
75
+ if appConf.Result != "0" {
76
+ return errors.New(appConf.Msg)
77
+ }
78
+ // get encrypt conf
79
+ var encryptConf EncryptConf
80
+ res, err = d.client.R().SetHeaders(headers).SetFormData(map[string]string{
81
+ "appId": appId,
82
+ }).Post("https://open.e.189.cn/api/logbox/config/encryptConf.do")
83
+ if err != nil {
84
+ return err
85
+ }
86
+ err = utils.Json.Unmarshal(res.Body(), &encryptConf)
87
+ if err != nil {
88
+ return err
89
+ }
90
+ log.Debugf("189 EncryptConf resp body: %s\n%+v", res.String(), encryptConf)
91
+ if encryptConf.Result != 0 {
92
+ return errors.New("get EncryptConf error:" + res.String())
93
+ }
94
+ // TODO: getUUID? needcaptcha
95
+ // login
96
+ loginData := map[string]string{
97
+ "version": "v2.0",
98
+ "apToken": "",
99
+ "appKey": appId,
100
+ "accountType": appConf.Data.AccountType,
101
+ "userName": encryptConf.Data.Pre + RsaEncode([]byte(d.Username), encryptConf.Data.PubKey, true),
102
+ "epd": encryptConf.Data.Pre + RsaEncode([]byte(d.Password), encryptConf.Data.PubKey, true),
103
+ "captchaType": "",
104
+ "validateCode": "",
105
+ "smsValidateCode": "",
106
+ "captchaToken": "",
107
+ "returnUrl": appConf.Data.ReturnUrl,
108
+ "mailSuffix": appConf.Data.MailSuffix,
109
+ "dynamicCheck": "FALSE",
110
+ "clientType": strconv.Itoa(appConf.Data.ClientType),
111
+ "cb_SaveName": "3",
112
+ "isOauth2": strconv.FormatBool(appConf.Data.IsOauth2),
113
+ "state": "",
114
+ "paramId": appConf.Data.ParamId,
115
+ }
116
+ res, err = d.client.R().SetHeaders(headers).SetFormData(loginData).Post("https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do")
117
+ if err != nil {
118
+ return err
119
+ }
120
+ log.Debugf("189 login resp body: %s", res.String())
121
+ loginResult := utils.Json.Get(res.Body(), "result").ToInt()
122
+ if loginResult != 0 {
123
+ return errors.New(utils.Json.Get(res.Body(), "msg").ToString())
124
+ }
125
+ return nil
126
+ }
drivers/189/meta.go ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _189
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/internal/driver"
5
+ "github.com/alist-org/alist/v3/internal/op"
6
+ )
7
+
8
+ type Addition struct {
9
+ Username string `json:"username" required:"true"`
10
+ Password string `json:"password" required:"true"`
11
+ Cookie string `json:"cookie" help:"Fill in the cookie if need captcha"`
12
+ driver.RootID
13
+ }
14
+
15
+ var config = driver.Config{
16
+ Name: "189Cloud",
17
+ LocalSort: true,
18
+ DefaultRoot: "-11",
19
+ Alert: `info|You can try to use 189PC driver if this driver does not work.`,
20
+ }
21
+
22
+ func init() {
23
+ op.RegisterDriver(func() driver.Driver {
24
+ return &Cloud189{}
25
+ })
26
+ }
drivers/189/types.go ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _189
2
+
3
+ type LoginResp struct {
4
+ Msg string `json:"msg"`
5
+ Result int `json:"result"`
6
+ ToUrl string `json:"toUrl"`
7
+ }
8
+
9
+ type Error struct {
10
+ ErrorCode string `json:"errorCode"`
11
+ ErrorMsg string `json:"errorMsg"`
12
+ }
13
+
14
+ type File struct {
15
+ Id int64 `json:"id"`
16
+ LastOpTime string `json:"lastOpTime"`
17
+ Name string `json:"name"`
18
+ Size int64 `json:"size"`
19
+ Icon struct {
20
+ SmallUrl string `json:"smallUrl"`
21
+ //LargeUrl string `json:"largeUrl"`
22
+ } `json:"icon"`
23
+ Url string `json:"url"`
24
+ }
25
+
26
+ type Folder struct {
27
+ Id int64 `json:"id"`
28
+ LastOpTime string `json:"lastOpTime"`
29
+ Name string `json:"name"`
30
+ }
31
+
32
+ type Files struct {
33
+ ResCode int `json:"res_code"`
34
+ ResMessage string `json:"res_message"`
35
+ FileListAO struct {
36
+ Count int `json:"count"`
37
+ FileList []File `json:"fileList"`
38
+ FolderList []Folder `json:"folderList"`
39
+ } `json:"fileListAO"`
40
+ }
41
+
42
+ type UploadUrlsResp struct {
43
+ Code string `json:"code"`
44
+ UploadUrls map[string]Part `json:"uploadUrls"`
45
+ }
46
+
47
+ type Part struct {
48
+ RequestURL string `json:"requestURL"`
49
+ RequestHeader string `json:"requestHeader"`
50
+ }
51
+
52
+ type Rsa struct {
53
+ Expire int64 `json:"expire"`
54
+ PkId string `json:"pkId"`
55
+ PubKey string `json:"pubKey"`
56
+ }
57
+
58
+ type Down struct {
59
+ ResCode int `json:"res_code"`
60
+ ResMessage string `json:"res_message"`
61
+ FileDownloadUrl string `json:"fileDownloadUrl"`
62
+ }
63
+
64
+ type DownResp struct {
65
+ ResCode int `json:"res_code"`
66
+ ResMessage string `json:"res_message"`
67
+ FileDownloadUrl string `json:"downloadUrl"`
68
+ }
drivers/189/util.go ADDED
@@ -0,0 +1,398 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _189
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "crypto/md5"
7
+ "encoding/base64"
8
+ "encoding/hex"
9
+ "errors"
10
+ "fmt"
11
+ "io"
12
+ "math"
13
+ "net/http"
14
+ "strconv"
15
+ "strings"
16
+ "time"
17
+
18
+ "github.com/alist-org/alist/v3/drivers/base"
19
+ "github.com/alist-org/alist/v3/internal/driver"
20
+ "github.com/alist-org/alist/v3/internal/model"
21
+ "github.com/alist-org/alist/v3/pkg/utils"
22
+ myrand "github.com/alist-org/alist/v3/pkg/utils/random"
23
+ "github.com/go-resty/resty/v2"
24
+ jsoniter "github.com/json-iterator/go"
25
+ log "github.com/sirupsen/logrus"
26
+ )
27
+
28
+ // do others that not defined in Driver interface
29
+
30
+ //func (d *Cloud189) login() error {
31
+ // url := "https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action"
32
+ // b := ""
33
+ // lt := ""
34
+ // ltText := regexp.MustCompile(`lt = "(.+?)"`)
35
+ // var res *resty.Response
36
+ // var err error
37
+ // for i := 0; i < 3; i++ {
38
+ // res, err = d.client.R().Get(url)
39
+ // if err != nil {
40
+ // return err
41
+ // }
42
+ // // 已经登陆
43
+ // if res.RawResponse.Request.URL.String() == "https://cloud.189.cn/web/main" {
44
+ // return nil
45
+ // }
46
+ // b = res.String()
47
+ // ltTextArr := ltText.FindStringSubmatch(b)
48
+ // if len(ltTextArr) > 0 {
49
+ // lt = ltTextArr[1]
50
+ // break
51
+ // } else {
52
+ // <-time.After(time.Second)
53
+ // }
54
+ // }
55
+ // if lt == "" {
56
+ // return fmt.Errorf("get page: %s \nstatus: %d \nrequest url: %s\nredirect url: %s",
57
+ // b, res.StatusCode(), res.RawResponse.Request.URL.String(), res.Header().Get("location"))
58
+ // }
59
+ // captchaToken := regexp.MustCompile(`captchaToken' value='(.+?)'`).FindStringSubmatch(b)[1]
60
+ // returnUrl := regexp.MustCompile(`returnUrl = '(.+?)'`).FindStringSubmatch(b)[1]
61
+ // paramId := regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(b)[1]
62
+ // //reqId := regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(b)[1]
63
+ // jRsakey := regexp.MustCompile(`j_rsaKey" value="(\S+)"`).FindStringSubmatch(b)[1]
64
+ // vCodeID := regexp.MustCompile(`picCaptcha\.do\?token\=([A-Za-z0-9\&\=]+)`).FindStringSubmatch(b)[1]
65
+ // vCodeRS := ""
66
+ // if vCodeID != "" {
67
+ // // need ValidateCode
68
+ // log.Debugf("try to identify verification codes")
69
+ // timeStamp := strconv.FormatInt(time.Now().UnixNano()/1e6, 10)
70
+ // u := "https://open.e.189.cn/api/logbox/oauth2/picCaptcha.do?token=" + vCodeID + timeStamp
71
+ // imgRes, err := d.client.R().SetHeaders(map[string]string{
72
+ // "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/76.0",
73
+ // "Referer": "https://open.e.189.cn/api/logbox/oauth2/unifyAccountLogin.do",
74
+ // "Sec-Fetch-Dest": "image",
75
+ // "Sec-Fetch-Mode": "no-cors",
76
+ // "Sec-Fetch-Site": "same-origin",
77
+ // }).Get(u)
78
+ // if err != nil {
79
+ // return err
80
+ // }
81
+ // // Enter the verification code manually
82
+ // //err = message.GetMessenger().WaitSend(message.Message{
83
+ // // Type: "image",
84
+ // // Content: "data:image/png;base64," + base64.StdEncoding.EncodeToString(imgRes.Body()),
85
+ // //}, 10)
86
+ // //if err != nil {
87
+ // // return err
88
+ // //}
89
+ // //vCodeRS, err = message.GetMessenger().WaitReceive(30)
90
+ // // use ocr api
91
+ // vRes, err := base.RestyClient.R().SetMultipartField(
92
+ // "image", "validateCode.png", "image/png", bytes.NewReader(imgRes.Body())).
93
+ // Post(setting.GetStr(conf.OcrApi))
94
+ // if err != nil {
95
+ // return err
96
+ // }
97
+ // if jsoniter.Get(vRes.Body(), "status").ToInt() != 200 {
98
+ // return errors.New("ocr error:" + jsoniter.Get(vRes.Body(), "msg").ToString())
99
+ // }
100
+ // vCodeRS = jsoniter.Get(vRes.Body(), "result").ToString()
101
+ // log.Debugln("code: ", vCodeRS)
102
+ // }
103
+ // userRsa := RsaEncode([]byte(d.Username), jRsakey, true)
104
+ // passwordRsa := RsaEncode([]byte(d.Password), jRsakey, true)
105
+ // url = "https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do"
106
+ // var loginResp LoginResp
107
+ // res, err = d.client.R().
108
+ // SetHeaders(map[string]string{
109
+ // "lt": lt,
110
+ // "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
111
+ // "Referer": "https://open.e.189.cn/",
112
+ // "accept": "application/json;charset=UTF-8",
113
+ // }).SetFormData(map[string]string{
114
+ // "appKey": "cloud",
115
+ // "accountType": "01",
116
+ // "userName": "{RSA}" + userRsa,
117
+ // "password": "{RSA}" + passwordRsa,
118
+ // "validateCode": vCodeRS,
119
+ // "captchaToken": captchaToken,
120
+ // "returnUrl": returnUrl,
121
+ // "mailSuffix": "@pan.cn",
122
+ // "paramId": paramId,
123
+ // "clientType": "10010",
124
+ // "dynamicCheck": "FALSE",
125
+ // "cb_SaveName": "1",
126
+ // "isOauth2": "false",
127
+ // }).Post(url)
128
+ // if err != nil {
129
+ // return err
130
+ // }
131
+ // err = utils.Json.Unmarshal(res.Body(), &loginResp)
132
+ // if err != nil {
133
+ // log.Error(err.Error())
134
+ // return err
135
+ // }
136
+ // if loginResp.Result != 0 {
137
+ // return fmt.Errorf(loginResp.Msg)
138
+ // }
139
+ // _, err = d.client.R().Get(loginResp.ToUrl)
140
+ // return err
141
+ //}
142
+
143
+ func (d *Cloud189) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
144
+ var e Error
145
+ req := d.client.R().SetError(&e).
146
+ SetHeader("Accept", "application/json;charset=UTF-8").
147
+ SetQueryParams(map[string]string{
148
+ "noCache": random(),
149
+ })
150
+ if callback != nil {
151
+ callback(req)
152
+ }
153
+ if resp != nil {
154
+ req.SetResult(resp)
155
+ }
156
+ res, err := req.Execute(method, url)
157
+ if err != nil {
158
+ return nil, err
159
+ }
160
+ //log.Debug(res.String())
161
+ if e.ErrorCode != "" {
162
+ if e.ErrorCode == "InvalidSessionKey" {
163
+ err = d.newLogin()
164
+ if err != nil {
165
+ return nil, err
166
+ }
167
+ return d.request(url, method, callback, resp)
168
+ }
169
+ }
170
+ if jsoniter.Get(res.Body(), "res_code").ToInt() != 0 {
171
+ err = errors.New(jsoniter.Get(res.Body(), "res_message").ToString())
172
+ }
173
+ return res.Body(), err
174
+ }
175
+
176
+ func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {
177
+ res := make([]model.Obj, 0)
178
+ pageNum := 1
179
+ for {
180
+ var resp Files
181
+ _, err := d.request("https://cloud.189.cn/api/open/file/listFiles.action", http.MethodGet, func(req *resty.Request) {
182
+ req.SetQueryParams(map[string]string{
183
+ //"noCache": random(),
184
+ "pageSize": "60",
185
+ "pageNum": strconv.Itoa(pageNum),
186
+ "mediaType": "0",
187
+ "folderId": fileId,
188
+ "iconOption": "5",
189
+ "orderBy": "lastOpTime", //account.OrderBy
190
+ "descending": "true", //account.OrderDirection
191
+ })
192
+ }, &resp)
193
+ if err != nil {
194
+ return nil, err
195
+ }
196
+ if resp.FileListAO.Count == 0 {
197
+ break
198
+ }
199
+ for _, folder := range resp.FileListAO.FolderList {
200
+ lastOpTime := utils.MustParseCNTime(folder.LastOpTime)
201
+ res = append(res, &model.Object{
202
+ ID: strconv.FormatInt(folder.Id, 10),
203
+ Name: folder.Name,
204
+ Modified: lastOpTime,
205
+ IsFolder: true,
206
+ })
207
+ }
208
+ for _, file := range resp.FileListAO.FileList {
209
+ lastOpTime := utils.MustParseCNTime(file.LastOpTime)
210
+ res = append(res, &model.ObjThumb{
211
+ Object: model.Object{
212
+ ID: strconv.FormatInt(file.Id, 10),
213
+ Name: file.Name,
214
+ Modified: lastOpTime,
215
+ Size: file.Size,
216
+ },
217
+ Thumbnail: model.Thumbnail{Thumbnail: file.Icon.SmallUrl},
218
+ })
219
+ }
220
+ pageNum++
221
+ }
222
+ return res, nil
223
+ }
224
+
225
+ func (d *Cloud189) oldUpload(dstDir model.Obj, file model.FileStreamer) error {
226
+ res, err := d.client.R().SetMultipartFormData(map[string]string{
227
+ "parentId": dstDir.GetID(),
228
+ "sessionKey": "??",
229
+ "opertype": "1",
230
+ "fname": file.GetName(),
231
+ }).SetMultipartField("Filedata", file.GetName(), file.GetMimetype(), file).Post("https://hb02.upload.cloud.189.cn/v1/DCIWebUploadAction")
232
+ if err != nil {
233
+ return err
234
+ }
235
+ if utils.Json.Get(res.Body(), "MD5").ToString() != "" {
236
+ return nil
237
+ }
238
+ log.Debugf(res.String())
239
+ return errors.New(res.String())
240
+ }
241
+
242
+ func (d *Cloud189) getSessionKey() (string, error) {
243
+ resp, err := d.request("https://cloud.189.cn/v2/getUserBriefInfo.action", http.MethodGet, nil, nil)
244
+ if err != nil {
245
+ return "", err
246
+ }
247
+ sessionKey := utils.Json.Get(resp, "sessionKey").ToString()
248
+ return sessionKey, nil
249
+ }
250
+
251
+ func (d *Cloud189) getResKey() (string, string, error) {
252
+ now := time.Now().UnixMilli()
253
+ if d.rsa.Expire > now {
254
+ return d.rsa.PubKey, d.rsa.PkId, nil
255
+ }
256
+ resp, err := d.request("https://cloud.189.cn/api/security/generateRsaKey.action", http.MethodGet, nil, nil)
257
+ if err != nil {
258
+ return "", "", err
259
+ }
260
+ pubKey, pkId := utils.Json.Get(resp, "pubKey").ToString(), utils.Json.Get(resp, "pkId").ToString()
261
+ d.rsa.PubKey, d.rsa.PkId = pubKey, pkId
262
+ d.rsa.Expire = utils.Json.Get(resp, "expire").ToInt64()
263
+ return pubKey, pkId, nil
264
+ }
265
+
266
+ func (d *Cloud189) uploadRequest(uri string, form map[string]string, resp interface{}) ([]byte, error) {
267
+ c := strconv.FormatInt(time.Now().UnixMilli(), 10)
268
+ r := Random("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx")
269
+ l := Random("xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx")
270
+ l = l[0 : 16+int(16*myrand.Rand.Float32())]
271
+
272
+ e := qs(form)
273
+ data := AesEncrypt([]byte(e), []byte(l[0:16]))
274
+ h := hex.EncodeToString(data)
275
+
276
+ sessionKey := d.sessionKey
277
+ signature := hmacSha1(fmt.Sprintf("SessionKey=%s&Operate=GET&RequestURI=%s&Date=%s&params=%s", sessionKey, uri, c, h), l)
278
+
279
+ pubKey, pkId, err := d.getResKey()
280
+ if err != nil {
281
+ return nil, err
282
+ }
283
+ b := RsaEncode([]byte(l), pubKey, false)
284
+ req := d.client.R().SetHeaders(map[string]string{
285
+ "accept": "application/json;charset=UTF-8",
286
+ "SessionKey": sessionKey,
287
+ "Signature": signature,
288
+ "X-Request-Date": c,
289
+ "X-Request-ID": r,
290
+ "EncryptionText": b,
291
+ "PkId": pkId,
292
+ })
293
+ if resp != nil {
294
+ req.SetResult(resp)
295
+ }
296
+ res, err := req.Get("https://upload.cloud.189.cn" + uri + "?params=" + h)
297
+ if err != nil {
298
+ return nil, err
299
+ }
300
+ data = res.Body()
301
+ if utils.Json.Get(data, "code").ToString() != "SUCCESS" {
302
+ return nil, errors.New(uri + "---" + jsoniter.Get(data, "msg").ToString())
303
+ }
304
+ return data, nil
305
+ }
306
+
307
+ func (d *Cloud189) newUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
308
+ sessionKey, err := d.getSessionKey()
309
+ if err != nil {
310
+ return err
311
+ }
312
+ d.sessionKey = sessionKey
313
+ const DEFAULT int64 = 10485760
314
+ var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
315
+
316
+ res, err := d.uploadRequest("/person/initMultiUpload", map[string]string{
317
+ "parentFolderId": dstDir.GetID(),
318
+ "fileName": encode(file.GetName()),
319
+ "fileSize": strconv.FormatInt(file.GetSize(), 10),
320
+ "sliceSize": strconv.FormatInt(DEFAULT, 10),
321
+ "lazyCheck": "1",
322
+ }, nil)
323
+ if err != nil {
324
+ return err
325
+ }
326
+ uploadFileId := jsoniter.Get(res, "data", "uploadFileId").ToString()
327
+ //_, err = d.uploadRequest("/person/getUploadedPartsInfo", map[string]string{
328
+ // "uploadFileId": uploadFileId,
329
+ //}, nil)
330
+ var finish int64 = 0
331
+ var i int64
332
+ var byteSize int64
333
+ md5s := make([]string, 0)
334
+ md5Sum := md5.New()
335
+ for i = 1; i <= count; i++ {
336
+ if utils.IsCanceled(ctx) {
337
+ return ctx.Err()
338
+ }
339
+ byteSize = file.GetSize() - finish
340
+ if DEFAULT < byteSize {
341
+ byteSize = DEFAULT
342
+ }
343
+ //log.Debugf("%d,%d", byteSize, finish)
344
+ byteData := make([]byte, byteSize)
345
+ n, err := io.ReadFull(file, byteData)
346
+ //log.Debug(err, n)
347
+ if err != nil {
348
+ return err
349
+ }
350
+ finish += int64(n)
351
+ md5Bytes := getMd5(byteData)
352
+ md5Hex := hex.EncodeToString(md5Bytes)
353
+ md5Base64 := base64.StdEncoding.EncodeToString(md5Bytes)
354
+ md5s = append(md5s, strings.ToUpper(md5Hex))
355
+ md5Sum.Write(byteData)
356
+ var resp UploadUrlsResp
357
+ res, err = d.uploadRequest("/person/getMultiUploadUrls", map[string]string{
358
+ "partInfo": fmt.Sprintf("%s-%s", strconv.FormatInt(i, 10), md5Base64),
359
+ "uploadFileId": uploadFileId,
360
+ }, &resp)
361
+ if err != nil {
362
+ return err
363
+ }
364
+ uploadData := resp.UploadUrls["partNumber_"+strconv.FormatInt(i, 10)]
365
+ log.Debugf("uploadData: %+v", uploadData)
366
+ requestURL := uploadData.RequestURL
367
+ uploadHeaders := strings.Split(decodeURIComponent(uploadData.RequestHeader), "&")
368
+ req, err := http.NewRequest(http.MethodPut, requestURL, bytes.NewReader(byteData))
369
+ if err != nil {
370
+ return err
371
+ }
372
+ req = req.WithContext(ctx)
373
+ for _, v := range uploadHeaders {
374
+ i := strings.Index(v, "=")
375
+ req.Header.Set(v[0:i], v[i+1:])
376
+ }
377
+ r, err := base.HttpClient.Do(req)
378
+ log.Debugf("%+v %+v", r, r.Request.Header)
379
+ r.Body.Close()
380
+ if err != nil {
381
+ return err
382
+ }
383
+ up(float64(i) * 100 / float64(count))
384
+ }
385
+ fileMd5 := hex.EncodeToString(md5Sum.Sum(nil))
386
+ sliceMd5 := fileMd5
387
+ if file.GetSize() > DEFAULT {
388
+ sliceMd5 = utils.GetMD5EncodeStr(strings.Join(md5s, "\n"))
389
+ }
390
+ res, err = d.uploadRequest("/person/commitMultiUploadFile", map[string]string{
391
+ "uploadFileId": uploadFileId,
392
+ "fileMd5": fileMd5,
393
+ "sliceMd5": sliceMd5,
394
+ "lazyCheck": "1",
395
+ "opertype": "3",
396
+ }, nil)
397
+ return err
398
+ }
drivers/189pc/driver.go ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _189pc
2
+
3
+ import (
4
+ "container/ring"
5
+ "context"
6
+ "net/http"
7
+ "strconv"
8
+ "strings"
9
+ "time"
10
+
11
+ "github.com/alist-org/alist/v3/drivers/base"
12
+ "github.com/alist-org/alist/v3/internal/driver"
13
+ "github.com/alist-org/alist/v3/internal/errs"
14
+ "github.com/alist-org/alist/v3/internal/model"
15
+ "github.com/alist-org/alist/v3/pkg/utils"
16
+ "github.com/go-resty/resty/v2"
17
+ )
18
+
19
+ type Cloud189PC struct {
20
+ model.Storage
21
+ Addition
22
+
23
+ identity string
24
+
25
+ client *resty.Client
26
+
27
+ loginParam *LoginParam
28
+ tokenInfo *AppSessionResp
29
+
30
+ uploadThread int
31
+
32
+ familyTransferFolder *ring.Ring
33
+ cleanFamilyTransferFile func()
34
+
35
+ storageConfig driver.Config
36
+ }
37
+
38
+ func (y *Cloud189PC) Config() driver.Config {
39
+ if y.storageConfig.Name == "" {
40
+ y.storageConfig = config
41
+ }
42
+ return y.storageConfig
43
+ }
44
+
45
+ func (y *Cloud189PC) GetAddition() driver.Additional {
46
+ return &y.Addition
47
+ }
48
+
49
+ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
50
+ // 兼容旧上传接口
51
+ y.storageConfig.NoOverwriteUpload = y.isFamily() && (y.Addition.RapidUpload || y.Addition.UploadMethod == "old")
52
+
53
+ // 处理个人云和家庭云参数
54
+ if y.isFamily() && y.RootFolderID == "-11" {
55
+ y.RootFolderID = ""
56
+ }
57
+ if !y.isFamily() && y.RootFolderID == "" {
58
+ y.RootFolderID = "-11"
59
+ }
60
+
61
+ // 限制上传线程数
62
+ y.uploadThread, _ = strconv.Atoi(y.UploadThread)
63
+ if y.uploadThread < 1 || y.uploadThread > 32 {
64
+ y.uploadThread, y.UploadThread = 3, "3"
65
+ }
66
+
67
+ // 初始化请求客户端
68
+ if y.client == nil {
69
+ y.client = base.NewRestyClient().SetHeaders(map[string]string{
70
+ "Accept": "application/json;charset=UTF-8",
71
+ "Referer": WEB_URL,
72
+ })
73
+ }
74
+
75
+ // 避免重复登陆
76
+ identity := utils.GetMD5EncodeStr(y.Username + y.Password)
77
+ if !y.isLogin() || y.identity != identity {
78
+ y.identity = identity
79
+ if err = y.login(); err != nil {
80
+ return
81
+ }
82
+ }
83
+
84
+ // 处理家庭云ID
85
+ if y.FamilyID == "" {
86
+ if y.FamilyID, err = y.getFamilyID(); err != nil {
87
+ return err
88
+ }
89
+ }
90
+
91
+ // 创建中转文件夹,防止重名文件
92
+ if y.FamilyTransfer {
93
+ if y.familyTransferFolder, err = y.createFamilyTransferFolder(32); err != nil {
94
+ return err
95
+ }
96
+ }
97
+
98
+ y.cleanFamilyTransferFile = utils.NewThrottle2(time.Minute, func() {
99
+ if err := y.cleanFamilyTransfer(context.TODO()); err != nil {
100
+ utils.Log.Errorf("cleanFamilyTransferFolderError:%s", err)
101
+ }
102
+ })
103
+ return
104
+ }
105
+
106
+ func (y *Cloud189PC) Drop(ctx context.Context) error {
107
+ return nil
108
+ }
109
+
110
+ func (y *Cloud189PC) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
111
+ return y.getFiles(ctx, dir.GetID(), y.isFamily())
112
+ }
113
+
114
+ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
115
+ var downloadUrl struct {
116
+ URL string `json:"fileDownloadUrl"`
117
+ }
118
+
119
+ isFamily := y.isFamily()
120
+ fullUrl := API_URL
121
+ if isFamily {
122
+ fullUrl += "/family/file"
123
+ }
124
+ fullUrl += "/getFileDownloadUrl.action"
125
+
126
+ _, err := y.get(fullUrl, func(r *resty.Request) {
127
+ r.SetContext(ctx)
128
+ r.SetQueryParam("fileId", file.GetID())
129
+ if isFamily {
130
+ r.SetQueryParams(map[string]string{
131
+ "familyId": y.FamilyID,
132
+ })
133
+ } else {
134
+ r.SetQueryParams(map[string]string{
135
+ "dt": "3",
136
+ "flag": "1",
137
+ })
138
+ }
139
+ }, &downloadUrl, isFamily)
140
+ if err != nil {
141
+ return nil, err
142
+ }
143
+
144
+ // 重定向获取真实链接
145
+ downloadUrl.URL = strings.Replace(strings.ReplaceAll(downloadUrl.URL, "&amp;", "&"), "http://", "https://", 1)
146
+ res, err := base.NoRedirectClient.R().SetContext(ctx).SetDoNotParseResponse(true).Get(downloadUrl.URL)
147
+ if err != nil {
148
+ return nil, err
149
+ }
150
+ defer res.RawBody().Close()
151
+ if res.StatusCode() == 302 {
152
+ downloadUrl.URL = res.Header().Get("location")
153
+ }
154
+
155
+ like := &model.Link{
156
+ URL: downloadUrl.URL,
157
+ Header: http.Header{
158
+ "User-Agent": []string{base.UserAgent},
159
+ },
160
+ }
161
+ /*
162
+ // 获取链接有效时常
163
+ strs := regexp.MustCompile(`(?i)expire[^=]*=([0-9]*)`).FindStringSubmatch(downloadUrl.URL)
164
+ if len(strs) == 2 {
165
+ timestamp, err := strconv.ParseInt(strs[1], 10, 64)
166
+ if err == nil {
167
+ expired := time.Duration(timestamp-time.Now().Unix()) * time.Second
168
+ like.Expiration = &expired
169
+ }
170
+ }
171
+ */
172
+ return like, nil
173
+ }
174
+
175
+ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
176
+ isFamily := y.isFamily()
177
+ fullUrl := API_URL
178
+ if isFamily {
179
+ fullUrl += "/family/file"
180
+ }
181
+ fullUrl += "/createFolder.action"
182
+
183
+ var newFolder Cloud189Folder
184
+ _, err := y.post(fullUrl, func(req *resty.Request) {
185
+ req.SetContext(ctx)
186
+ req.SetQueryParams(map[string]string{
187
+ "folderName": dirName,
188
+ "relativePath": "",
189
+ })
190
+ if isFamily {
191
+ req.SetQueryParams(map[string]string{
192
+ "familyId": y.FamilyID,
193
+ "parentId": parentDir.GetID(),
194
+ })
195
+ } else {
196
+ req.SetQueryParams(map[string]string{
197
+ "parentFolderId": parentDir.GetID(),
198
+ })
199
+ }
200
+ }, &newFolder, isFamily)
201
+ if err != nil {
202
+ return nil, err
203
+ }
204
+ return &newFolder, nil
205
+ }
206
+
207
+ func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
208
+ isFamily := y.isFamily()
209
+ other := map[string]string{"targetFileName": dstDir.GetName()}
210
+
211
+ resp, err := y.CreateBatchTask("MOVE", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
212
+ FileId: srcObj.GetID(),
213
+ FileName: srcObj.GetName(),
214
+ IsFolder: BoolToNumber(srcObj.IsDir()),
215
+ })
216
+ if err != nil {
217
+ return nil, err
218
+ }
219
+ if err = y.WaitBatchTask("MOVE", resp.TaskID, time.Millisecond*400); err != nil {
220
+ return nil, err
221
+ }
222
+ return srcObj, nil
223
+ }
224
+
225
+ func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
226
+ isFamily := y.isFamily()
227
+ queryParam := make(map[string]string)
228
+ fullUrl := API_URL
229
+ method := http.MethodPost
230
+ if isFamily {
231
+ fullUrl += "/family/file"
232
+ method = http.MethodGet
233
+ queryParam["familyId"] = y.FamilyID
234
+ }
235
+
236
+ var newObj model.Obj
237
+ switch f := srcObj.(type) {
238
+ case *Cloud189File:
239
+ fullUrl += "/renameFile.action"
240
+ queryParam["fileId"] = srcObj.GetID()
241
+ queryParam["destFileName"] = newName
242
+ newObj = &Cloud189File{Icon: f.Icon} // 复用预览
243
+ case *Cloud189Folder:
244
+ fullUrl += "/renameFolder.action"
245
+ queryParam["folderId"] = srcObj.GetID()
246
+ queryParam["destFolderName"] = newName
247
+ newObj = &Cloud189Folder{}
248
+ default:
249
+ return nil, errs.NotSupport
250
+ }
251
+
252
+ _, err := y.request(fullUrl, method, func(req *resty.Request) {
253
+ req.SetContext(ctx).SetQueryParams(queryParam)
254
+ }, nil, newObj, isFamily)
255
+ if err != nil {
256
+ return nil, err
257
+ }
258
+ return newObj, nil
259
+ }
260
+
261
+ func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
262
+ isFamily := y.isFamily()
263
+ other := map[string]string{"targetFileName": dstDir.GetName()}
264
+
265
+ resp, err := y.CreateBatchTask("COPY", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
266
+ FileId: srcObj.GetID(),
267
+ FileName: srcObj.GetName(),
268
+ IsFolder: BoolToNumber(srcObj.IsDir()),
269
+ })
270
+
271
+ if err != nil {
272
+ return err
273
+ }
274
+ return y.WaitBatchTask("COPY", resp.TaskID, time.Second)
275
+ }
276
+
277
+ func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
278
+ isFamily := y.isFamily()
279
+
280
+ resp, err := y.CreateBatchTask("DELETE", IF(isFamily, y.FamilyID, ""), "", nil, BatchTaskInfo{
281
+ FileId: obj.GetID(),
282
+ FileName: obj.GetName(),
283
+ IsFolder: BoolToNumber(obj.IsDir()),
284
+ })
285
+ if err != nil {
286
+ return err
287
+ }
288
+ // 批量任务数量限制,过快会导致无法删除
289
+ return y.WaitBatchTask("DELETE", resp.TaskID, time.Millisecond*200)
290
+ }
291
+
292
+ func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (newObj model.Obj, err error) {
293
+ overwrite := true
294
+ isFamily := y.isFamily()
295
+
296
+ // 响应时间长,按需启用
297
+ if y.Addition.RapidUpload && !stream.IsForceStreamUpload() {
298
+ if newObj, err := y.RapidUpload(ctx, dstDir, stream, isFamily, overwrite); err == nil {
299
+ return newObj, nil
300
+ }
301
+ }
302
+
303
+ uploadMethod := y.UploadMethod
304
+ if stream.IsForceStreamUpload() {
305
+ uploadMethod = "stream"
306
+ }
307
+
308
+ // 旧版上传家庭云也有限制
309
+ if uploadMethod == "old" {
310
+ return y.OldUpload(ctx, dstDir, stream, up, isFamily, overwrite)
311
+ }
312
+
313
+ // 开启家庭云转存
314
+ if !isFamily && y.FamilyTransfer {
315
+ // 修改上传目标为家庭云文件夹
316
+ transferDstDir := dstDir
317
+ dstDir = (y.familyTransferFolder.Value).(*Cloud189Folder)
318
+ y.familyTransferFolder = y.familyTransferFolder.Next()
319
+
320
+ isFamily = true
321
+ overwrite = false
322
+
323
+ defer func() {
324
+ if newObj != nil {
325
+ // 批量任务有概率删不掉
326
+ y.cleanFamilyTransferFile()
327
+
328
+ // 转存家庭云文件到个人云
329
+ err = y.SaveFamilyFileToPersonCloud(context.TODO(), y.FamilyID, newObj, transferDstDir, true)
330
+
331
+ task := BatchTaskInfo{
332
+ FileId: newObj.GetID(),
333
+ FileName: newObj.GetName(),
334
+ IsFolder: BoolToNumber(newObj.IsDir()),
335
+ }
336
+
337
+ // 删除源文件
338
+ if resp, err := y.CreateBatchTask("DELETE", y.FamilyID, "", nil, task); err == nil {
339
+ y.WaitBatchTask("DELETE", resp.TaskID, time.Second)
340
+ // 永久删除
341
+ if resp, err := y.CreateBatchTask("CLEAR_RECYCLE", y.FamilyID, "", nil, task); err == nil {
342
+ y.WaitBatchTask("CLEAR_RECYCLE", resp.TaskID, time.Second)
343
+ }
344
+ }
345
+ newObj = nil
346
+ }
347
+ }()
348
+ }
349
+
350
+ switch uploadMethod {
351
+ case "rapid":
352
+ return y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)
353
+ case "stream":
354
+ if stream.GetSize() == 0 {
355
+ return y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)
356
+ }
357
+ fallthrough
358
+ default:
359
+ return y.StreamUpload(ctx, dstDir, stream, up, isFamily, overwrite)
360
+ }
361
+ }
drivers/189pc/help.go ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _189pc
2
+
3
+ import (
4
+ "bytes"
5
+ "crypto/aes"
6
+ "crypto/hmac"
7
+ "crypto/rand"
8
+ "crypto/rsa"
9
+ "crypto/sha1"
10
+ "crypto/x509"
11
+ "encoding/hex"
12
+ "encoding/pem"
13
+ "encoding/xml"
14
+ "fmt"
15
+ "math"
16
+ "net/http"
17
+ "regexp"
18
+ "strings"
19
+ "time"
20
+
21
+ "github.com/alist-org/alist/v3/pkg/utils/random"
22
+ )
23
+
24
+ func clientSuffix() map[string]string {
25
+ rand := random.Rand
26
+ return map[string]string{
27
+ "clientType": PC,
28
+ "version": VERSION,
29
+ "channelId": CHANNEL_ID,
30
+ "rand": fmt.Sprintf("%d_%d", rand.Int63n(1e5), rand.Int63n(1e10)),
31
+ }
32
+ }
33
+
34
+ // 带params的SignatureOfHmac HMAC签名
35
+ func signatureOfHmac(sessionSecret, sessionKey, operate, fullUrl, dateOfGmt, param string) string {
36
+ urlpath := regexp.MustCompile(`://[^/]+((/[^/\s?#]+)*)`).FindStringSubmatch(fullUrl)[1]
37
+ mac := hmac.New(sha1.New, []byte(sessionSecret))
38
+ data := fmt.Sprintf("SessionKey=%s&Operate=%s&RequestURI=%s&Date=%s", sessionKey, operate, urlpath, dateOfGmt)
39
+ if param != "" {
40
+ data += fmt.Sprintf("&params=%s", param)
41
+ }
42
+ mac.Write([]byte(data))
43
+ return strings.ToUpper(hex.EncodeToString(mac.Sum(nil)))
44
+ }
45
+
46
+ // RAS 加密用户名密码
47
+ func RsaEncrypt(publicKey, origData string) string {
48
+ block, _ := pem.Decode([]byte(publicKey))
49
+ pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes)
50
+ data, _ := rsa.EncryptPKCS1v15(rand.Reader, pubInterface.(*rsa.PublicKey), []byte(origData))
51
+ return strings.ToUpper(hex.EncodeToString(data))
52
+ }
53
+
54
+ // aes 加密params
55
+ func AesECBEncrypt(data, key string) string {
56
+ block, _ := aes.NewCipher([]byte(key))
57
+ paddingData := PKCS7Padding([]byte(data), block.BlockSize())
58
+ decrypted := make([]byte, len(paddingData))
59
+ size := block.BlockSize()
60
+ for src, dst := paddingData, decrypted; len(src) > 0; src, dst = src[size:], dst[size:] {
61
+ block.Encrypt(dst[:size], src[:size])
62
+ }
63
+ return strings.ToUpper(hex.EncodeToString(decrypted))
64
+ }
65
+
66
+ func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
67
+ padding := blockSize - len(ciphertext)%blockSize
68
+ padtext := bytes.Repeat([]byte{byte(padding)}, padding)
69
+ return append(ciphertext, padtext...)
70
+ }
71
+
72
+ // 获取http规范的时间
73
+ func getHttpDateStr() string {
74
+ return time.Now().UTC().Format(http.TimeFormat)
75
+ }
76
+
77
+ // 时间戳
78
+ func timestamp() int64 {
79
+ return time.Now().UTC().UnixNano() / 1e6
80
+ }
81
+
82
+ func MustParseTime(str string) *time.Time {
83
+ lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05 -07", str+" +08", time.Local)
84
+ return &lastOpTime
85
+ }
86
+
87
+ type Time time.Time
88
+
89
+ func (t *Time) UnmarshalJSON(b []byte) error { return t.Unmarshal(b) }
90
+ func (t *Time) UnmarshalXML(e *xml.Decoder, ee xml.StartElement) error {
91
+ b, err := e.Token()
92
+ if err != nil {
93
+ return err
94
+ }
95
+ if b, ok := b.(xml.CharData); ok {
96
+ if err = t.Unmarshal(b); err != nil {
97
+ return err
98
+ }
99
+ }
100
+ return e.Skip()
101
+ }
102
+ func (t *Time) Unmarshal(b []byte) error {
103
+ bs := strings.Trim(string(b), "\"")
104
+ var v time.Time
105
+ var err error
106
+ for _, f := range []string{"2006-01-02 15:04:05 -07", "Jan 2, 2006 15:04:05 PM -07"} {
107
+ v, err = time.ParseInLocation(f, bs+" +08", time.Local)
108
+ if err == nil {
109
+ break
110
+ }
111
+ }
112
+ *t = Time(v)
113
+ return err
114
+ }
115
+
116
+ type String string
117
+
118
+ func (t *String) UnmarshalJSON(b []byte) error { return t.Unmarshal(b) }
119
+ func (t *String) UnmarshalXML(e *xml.Decoder, ee xml.StartElement) error {
120
+ b, err := e.Token()
121
+ if err != nil {
122
+ return err
123
+ }
124
+ if b, ok := b.(xml.CharData); ok {
125
+ if err = t.Unmarshal(b); err != nil {
126
+ return err
127
+ }
128
+ }
129
+ return e.Skip()
130
+ }
131
+ func (s *String) Unmarshal(b []byte) error {
132
+ *s = String(bytes.Trim(b, "\""))
133
+ return nil
134
+ }
135
+
136
+ func toFamilyOrderBy(o string) string {
137
+ switch o {
138
+ case "filename":
139
+ return "1"
140
+ case "filesize":
141
+ return "2"
142
+ case "lastOpTime":
143
+ return "3"
144
+ default:
145
+ return "1"
146
+ }
147
+ }
148
+
149
+ func toDesc(o string) string {
150
+ switch o {
151
+ case "desc":
152
+ return "true"
153
+ case "asc":
154
+ fallthrough
155
+ default:
156
+ return "false"
157
+ }
158
+ }
159
+
160
+ func ParseHttpHeader(str string) map[string]string {
161
+ header := make(map[string]string)
162
+ for _, value := range strings.Split(str, "&") {
163
+ if k, v, found := strings.Cut(value, "="); found {
164
+ header[k] = v
165
+ }
166
+ }
167
+ return header
168
+ }
169
+
170
+ func MustString(str string, err error) string {
171
+ return str
172
+ }
173
+
174
+ func BoolToNumber(b bool) int {
175
+ if b {
176
+ return 1
177
+ }
178
+ return 0
179
+ }
180
+
181
+ // 计算分片大小
182
+ // 对分片数量有限制
183
+ // 10MIB 20 MIB 999片
184
+ // 50MIB 60MIB 70MIB 80MIB ∞MIB 1999片
185
+ func partSize(size int64) int64 {
186
+ const DEFAULT = 1024 * 1024 * 10 // 10MIB
187
+ if size > DEFAULT*2*999 {
188
+ return int64(math.Max(math.Ceil((float64(size)/1999) /*=单个切片大小*/ /float64(DEFAULT)) /*=倍率*/, 5) * DEFAULT)
189
+ }
190
+ if size > DEFAULT*999 {
191
+ return DEFAULT * 2 // 20MIB
192
+ }
193
+ return DEFAULT
194
+ }
195
+
196
+ func isBool(bs ...bool) bool {
197
+ for _, b := range bs {
198
+ if b {
199
+ return true
200
+ }
201
+ }
202
+ return false
203
+ }
204
+
205
+ func IF[V any](o bool, t V, f V) V {
206
+ if o {
207
+ return t
208
+ }
209
+ return f
210
+ }
drivers/189pc/meta.go ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _189pc
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/internal/driver"
5
+ "github.com/alist-org/alist/v3/internal/op"
6
+ )
7
+
8
+ type Addition struct {
9
+ Username string `json:"username" required:"true"`
10
+ Password string `json:"password" required:"true"`
11
+ VCode string `json:"validate_code"`
12
+ driver.RootID
13
+ OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
14
+ OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
15
+ Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
16
+ FamilyID string `json:"family_id"`
17
+ UploadMethod string `json:"upload_method" type:"select" options:"stream,rapid,old" default:"stream"`
18
+ UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
19
+ FamilyTransfer bool `json:"family_transfer"`
20
+ RapidUpload bool `json:"rapid_upload"`
21
+ NoUseOcr bool `json:"no_use_ocr"`
22
+ }
23
+
24
+ var config = driver.Config{
25
+ Name: "189CloudPC",
26
+ DefaultRoot: "-11",
27
+ CheckStatus: true,
28
+ }
29
+
30
+ func init() {
31
+ op.RegisterDriver(func() driver.Driver {
32
+ return &Cloud189PC{}
33
+ })
34
+ }
drivers/189pc/types.go ADDED
@@ -0,0 +1,398 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _189pc
2
+
3
+ import (
4
+ "encoding/xml"
5
+ "fmt"
6
+ "sort"
7
+ "strings"
8
+ "time"
9
+
10
+ "github.com/alist-org/alist/v3/pkg/utils"
11
+ )
12
+
13
+ // 居然有四种返回方式
14
+ type RespErr struct {
15
+ ResCode any `json:"res_code"` // int or string
16
+ ResMessage string `json:"res_message"`
17
+
18
+ Error_ string `json:"error"`
19
+
20
+ XMLName xml.Name `xml:"error"`
21
+ Code string `json:"code" xml:"code"`
22
+ Message string `json:"message" xml:"message"`
23
+ Msg string `json:"msg"`
24
+
25
+ ErrorCode string `json:"errorCode"`
26
+ ErrorMsg string `json:"errorMsg"`
27
+ }
28
+
29
+ func (e *RespErr) HasError() bool {
30
+ switch v := e.ResCode.(type) {
31
+ case int, int64, int32:
32
+ return v != 0
33
+ case string:
34
+ return e.ResCode != ""
35
+ }
36
+ return (e.Code != "" && e.Code != "SUCCESS") || e.ErrorCode != "" || e.Error_ != ""
37
+ }
38
+
39
+ func (e *RespErr) Error() string {
40
+ switch v := e.ResCode.(type) {
41
+ case int, int64, int32:
42
+ if v != 0 {
43
+ return fmt.Sprintf("res_code: %d ,res_msg: %s", v, e.ResMessage)
44
+ }
45
+ case string:
46
+ if e.ResCode != "" {
47
+ return fmt.Sprintf("res_code: %s ,res_msg: %s", e.ResCode, e.ResMessage)
48
+ }
49
+ }
50
+
51
+ if e.Code != "" && e.Code != "SUCCESS" {
52
+ if e.Msg != "" {
53
+ return fmt.Sprintf("code: %s ,msg: %s", e.Code, e.Msg)
54
+ }
55
+ if e.Message != "" {
56
+ return fmt.Sprintf("code: %s ,msg: %s", e.Code, e.Message)
57
+ }
58
+ return "code: " + e.Code
59
+ }
60
+
61
+ if e.ErrorCode != "" {
62
+ return fmt.Sprintf("err_code: %s ,err_msg: %s", e.ErrorCode, e.ErrorMsg)
63
+ }
64
+
65
+ if e.Error_ != "" {
66
+ return fmt.Sprintf("error: %s ,message: %s", e.ErrorCode, e.Message)
67
+ }
68
+ return ""
69
+ }
70
+
71
+ // 登陆需要的参数
72
+ type LoginParam struct {
73
+ // 加密后的用户名和密码
74
+ RsaUsername string
75
+ RsaPassword string
76
+
77
+ // rsa密钥
78
+ jRsaKey string
79
+
80
+ // 请求头参数
81
+ Lt string
82
+ ReqId string
83
+
84
+ // 表单参数
85
+ ParamId string
86
+
87
+ // 验证码
88
+ CaptchaToken string
89
+ }
90
+
91
+ // 登陆加密相关
92
+ type EncryptConfResp struct {
93
+ Result int `json:"result"`
94
+ Data struct {
95
+ UpSmsOn string `json:"upSmsOn"`
96
+ Pre string `json:"pre"`
97
+ PreDomain string `json:"preDomain"`
98
+ PubKey string `json:"pubKey"`
99
+ } `json:"data"`
100
+ }
101
+
102
+ type LoginResp struct {
103
+ Msg string `json:"msg"`
104
+ Result int `json:"result"`
105
+ ToUrl string `json:"toUrl"`
106
+ }
107
+
108
+ // 刷新session返回
109
+ type UserSessionResp struct {
110
+ ResCode int `json:"res_code"`
111
+ ResMessage string `json:"res_message"`
112
+
113
+ LoginName string `json:"loginName"`
114
+
115
+ KeepAlive int `json:"keepAlive"`
116
+ GetFileDiffSpan int `json:"getFileDiffSpan"`
117
+ GetUserInfoSpan int `json:"getUserInfoSpan"`
118
+
119
+ // 个人云
120
+ SessionKey string `json:"sessionKey"`
121
+ SessionSecret string `json:"sessionSecret"`
122
+ // 家庭云
123
+ FamilySessionKey string `json:"familySessionKey"`
124
+ FamilySessionSecret string `json:"familySessionSecret"`
125
+ }
126
+
127
+ // 登录返回
128
+ type AppSessionResp struct {
129
+ UserSessionResp
130
+
131
+ IsSaveName string `json:"isSaveName"`
132
+
133
+ // 会话刷新Token
134
+ AccessToken string `json:"accessToken"`
135
+ //Token刷新
136
+ RefreshToken string `json:"refreshToken"`
137
+ }
138
+
139
+ // 家庭云账户
140
+ type FamilyInfoListResp struct {
141
+ FamilyInfoResp []FamilyInfoResp `json:"familyInfoResp"`
142
+ }
143
+ type FamilyInfoResp struct {
144
+ Count int `json:"count"`
145
+ CreateTime string `json:"createTime"`
146
+ FamilyID int64 `json:"familyId"`
147
+ RemarkName string `json:"remarkName"`
148
+ Type int `json:"type"`
149
+ UseFlag int `json:"useFlag"`
150
+ UserRole int `json:"userRole"`
151
+ }
152
+
153
+ /*文件部分*/
154
+ // 文件
155
+ type Cloud189File struct {
156
+ ID String `json:"id"`
157
+ Name string `json:"name"`
158
+ Size int64 `json:"size"`
159
+ Md5 string `json:"md5"`
160
+
161
+ LastOpTime Time `json:"lastOpTime"`
162
+ CreateDate Time `json:"createDate"`
163
+ Icon struct {
164
+ //iconOption 5
165
+ SmallUrl string `json:"smallUrl"`
166
+ LargeUrl string `json:"largeUrl"`
167
+
168
+ // iconOption 10
169
+ Max600 string `json:"max600"`
170
+ MediumURL string `json:"mediumUrl"`
171
+ } `json:"icon"`
172
+
173
+ // Orientation int64 `json:"orientation"`
174
+ // FileCata int64 `json:"fileCata"`
175
+ // MediaType int `json:"mediaType"`
176
+ // Rev string `json:"rev"`
177
+ // StarLabel int64 `json:"starLabel"`
178
+ }
179
+
180
+ func (c *Cloud189File) CreateTime() time.Time {
181
+ return time.Time(c.CreateDate)
182
+ }
183
+
184
+ func (c *Cloud189File) GetHash() utils.HashInfo {
185
+ return utils.NewHashInfo(utils.MD5, c.Md5)
186
+ }
187
+
188
+ func (c *Cloud189File) GetSize() int64 { return c.Size }
189
+ func (c *Cloud189File) GetName() string { return c.Name }
190
+ func (c *Cloud189File) ModTime() time.Time { return time.Time(c.LastOpTime) }
191
+ func (c *Cloud189File) IsDir() bool { return false }
192
+ func (c *Cloud189File) GetID() string { return string(c.ID) }
193
+ func (c *Cloud189File) GetPath() string { return "" }
194
+ func (c *Cloud189File) Thumb() string { return c.Icon.SmallUrl }
195
+
196
+ // 文件夹
197
+ type Cloud189Folder struct {
198
+ ID String `json:"id"`
199
+ ParentID int64 `json:"parentId"`
200
+ Name string `json:"name"`
201
+
202
+ LastOpTime Time `json:"lastOpTime"`
203
+ CreateDate Time `json:"createDate"`
204
+
205
+ // FileListSize int64 `json:"fileListSize"`
206
+ // FileCount int64 `json:"fileCount"`
207
+ // FileCata int64 `json:"fileCata"`
208
+ // Rev string `json:"rev"`
209
+ // StarLabel int64 `json:"starLabel"`
210
+ }
211
+
212
+ func (c *Cloud189Folder) CreateTime() time.Time {
213
+ return time.Time(c.CreateDate)
214
+ }
215
+
216
+ func (c *Cloud189Folder) GetHash() utils.HashInfo {
217
+ return utils.HashInfo{}
218
+ }
219
+
220
+ func (c *Cloud189Folder) GetSize() int64 { return 0 }
221
+ func (c *Cloud189Folder) GetName() string { return c.Name }
222
+ func (c *Cloud189Folder) ModTime() time.Time { return time.Time(c.LastOpTime) }
223
+ func (c *Cloud189Folder) IsDir() bool { return true }
224
+ func (c *Cloud189Folder) GetID() string { return string(c.ID) }
225
+ func (c *Cloud189Folder) GetPath() string { return "" }
226
+
227
+ type Cloud189FilesResp struct {
228
+ //ResCode int `json:"res_code"`
229
+ //ResMessage string `json:"res_message"`
230
+ FileListAO struct {
231
+ Count int `json:"count"`
232
+ FileList []Cloud189File `json:"fileList"`
233
+ FolderList []Cloud189Folder `json:"folderList"`
234
+ } `json:"fileListAO"`
235
+ }
236
+
237
+ // TaskInfo 任务信息
238
+ type BatchTaskInfo struct {
239
+ // FileId 文件ID
240
+ FileId string `json:"fileId"`
241
+ // FileName 文件名
242
+ FileName string `json:"fileName"`
243
+ // IsFolder 是否是文件夹,0-否,1-是
244
+ IsFolder int `json:"isFolder"`
245
+ // SrcParentId 文件所在父目录ID
246
+ SrcParentId string `json:"srcParentId,omitempty"`
247
+
248
+ /* 冲突管理 */
249
+ // 1 -> 跳过 2 -> 保留 3 -> 覆盖
250
+ DealWay int `json:"dealWay,omitempty"`
251
+ IsConflict int `json:"isConflict,omitempty"`
252
+ }
253
+
254
+ /* 上传部分 */
255
+ type InitMultiUploadResp struct {
256
+ //Code string `json:"code"`
257
+ Data struct {
258
+ UploadType int `json:"uploadType"`
259
+ UploadHost string `json:"uploadHost"`
260
+ UploadFileID string `json:"uploadFileId"`
261
+ FileDataExists int `json:"fileDataExists"`
262
+ } `json:"data"`
263
+ }
264
+ type UploadUrlsResp struct {
265
+ Code string `json:"code"`
266
+ Data map[string]UploadUrlsData `json:"uploadUrls"`
267
+ }
268
+ type UploadUrlsData struct {
269
+ RequestURL string `json:"requestURL"`
270
+ RequestHeader string `json:"requestHeader"`
271
+ }
272
+
273
+ type UploadUrlInfo struct {
274
+ PartNumber int
275
+ Headers map[string]string
276
+ UploadUrlsData
277
+ }
278
+
279
+ type UploadProgress struct {
280
+ UploadInfo InitMultiUploadResp
281
+ UploadParts []string
282
+ }
283
+
284
+ /* 第二种上传方式 */
285
+ type CreateUploadFileResp struct {
286
+ // 上传文件请求ID
287
+ UploadFileId int64 `json:"uploadFileId"`
288
+ // 上传文件数据的URL路径
289
+ FileUploadUrl string `json:"fileUploadUrl"`
290
+ // 上传文件完成后确认路径
291
+ FileCommitUrl string `json:"fileCommitUrl"`
292
+ // 文件是否已存在云盘中,0-未存在,1-已存在
293
+ FileDataExists int `json:"fileDataExists"`
294
+ }
295
+
296
+ type GetUploadFileStatusResp struct {
297
+ CreateUploadFileResp
298
+
299
+ // 已上传的大小
300
+ DataSize int64 `json:"dataSize"`
301
+ Size int64 `json:"size"`
302
+ }
303
+
304
+ func (r *GetUploadFileStatusResp) GetSize() int64 {
305
+ return r.DataSize + r.Size
306
+ }
307
+
308
+ type CommitMultiUploadFileResp struct {
309
+ File struct {
310
+ UserFileID String `json:"userFileId"`
311
+ FileName string `json:"fileName"`
312
+ FileSize int64 `json:"fileSize"`
313
+ FileMd5 string `json:"fileMd5"`
314
+ CreateDate Time `json:"createDate"`
315
+ } `json:"file"`
316
+ }
317
+
318
+ func (f *CommitMultiUploadFileResp) toFile() *Cloud189File {
319
+ return &Cloud189File{
320
+ ID: f.File.UserFileID,
321
+ Name: f.File.FileName,
322
+ Size: f.File.FileSize,
323
+ Md5: f.File.FileMd5,
324
+ LastOpTime: f.File.CreateDate,
325
+ CreateDate: f.File.CreateDate,
326
+ }
327
+ }
328
+
329
+ type OldCommitUploadFileResp struct {
330
+ XMLName xml.Name `xml:"file"`
331
+ ID String `xml:"id"`
332
+ Name string `xml:"name"`
333
+ Size int64 `xml:"size"`
334
+ Md5 string `xml:"md5"`
335
+ CreateDate Time `xml:"createDate"`
336
+ }
337
+
338
+ func (f *OldCommitUploadFileResp) toFile() *Cloud189File {
339
+ return &Cloud189File{
340
+ ID: f.ID,
341
+ Name: f.Name,
342
+ Size: f.Size,
343
+ Md5: f.Md5,
344
+ CreateDate: f.CreateDate,
345
+ LastOpTime: f.CreateDate,
346
+ }
347
+ }
348
+
349
+ type CreateBatchTaskResp struct {
350
+ TaskID string `json:"taskId"`
351
+ }
352
+
353
+ type BatchTaskStateResp struct {
354
+ FailedCount int `json:"failedCount"`
355
+ Process int `json:"process"`
356
+ SkipCount int `json:"skipCount"`
357
+ SubTaskCount int `json:"subTaskCount"`
358
+ SuccessedCount int `json:"successedCount"`
359
+ SuccessedFileIDList []int64 `json:"successedFileIdList"`
360
+ TaskID string `json:"taskId"`
361
+ TaskStatus int `json:"taskStatus"` //1 初始化 2 存在冲突 3 执行中,4 完成
362
+ }
363
+
364
+ type BatchTaskConflictTaskInfoResp struct {
365
+ SessionKey string `json:"sessionKey"`
366
+ TargetFolderID int `json:"targetFolderId"`
367
+ TaskID string `json:"taskId"`
368
+ TaskInfos []BatchTaskInfo
369
+ TaskType int `json:"taskType"`
370
+ }
371
+
372
+ /* query 加密参数*/
373
+ type Params map[string]string
374
+
375
+ func (p Params) Set(k, v string) {
376
+ p[k] = v
377
+ }
378
+
379
+ func (p Params) Encode() string {
380
+ if p == nil {
381
+ return ""
382
+ }
383
+ var buf strings.Builder
384
+ keys := make([]string, 0, len(p))
385
+ for k := range p {
386
+ keys = append(keys, k)
387
+ }
388
+ sort.Strings(keys)
389
+ for i := range keys {
390
+ if buf.Len() > 0 {
391
+ buf.WriteByte('&')
392
+ }
393
+ buf.WriteString(keys[i])
394
+ buf.WriteByte('=')
395
+ buf.WriteString(p[keys[i]])
396
+ }
397
+ return buf.String()
398
+ }
drivers/189pc/utils.go ADDED
@@ -0,0 +1,1144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _189pc
2
+
3
+ import (
4
+ "bytes"
5
+ "container/ring"
6
+ "context"
7
+ "crypto/md5"
8
+ "encoding/base64"
9
+ "encoding/hex"
10
+ "encoding/xml"
11
+ "fmt"
12
+ "io"
13
+ "math"
14
+ "net/http"
15
+ "net/http/cookiejar"
16
+ "net/url"
17
+ "regexp"
18
+ "sort"
19
+ "strconv"
20
+ "strings"
21
+ "time"
22
+
23
+ "github.com/alist-org/alist/v3/drivers/base"
24
+ "github.com/alist-org/alist/v3/internal/conf"
25
+ "github.com/alist-org/alist/v3/internal/driver"
26
+ "github.com/alist-org/alist/v3/internal/model"
27
+ "github.com/alist-org/alist/v3/internal/op"
28
+ "github.com/alist-org/alist/v3/internal/setting"
29
+ "github.com/alist-org/alist/v3/pkg/errgroup"
30
+ "github.com/alist-org/alist/v3/pkg/utils"
31
+
32
+ "github.com/avast/retry-go"
33
+ "github.com/go-resty/resty/v2"
34
+ "github.com/google/uuid"
35
+ jsoniter "github.com/json-iterator/go"
36
+ "github.com/pkg/errors"
37
+ )
38
+
39
+ const (
40
+ ACCOUNT_TYPE = "02"
41
+ APP_ID = "8025431004"
42
+ CLIENT_TYPE = "10020"
43
+ VERSION = "6.2"
44
+
45
+ WEB_URL = "https://cloud.189.cn"
46
+ AUTH_URL = "https://open.e.189.cn"
47
+ API_URL = "https://api.cloud.189.cn"
48
+ UPLOAD_URL = "https://upload.cloud.189.cn"
49
+
50
+ RETURN_URL = "https://m.cloud.189.cn/zhuanti/2020/loginErrorPc/index.html"
51
+
52
+ PC = "TELEPC"
53
+ MAC = "TELEMAC"
54
+
55
+ CHANNEL_ID = "web_cloud.189.cn"
56
+ )
57
+
58
+ func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool) map[string]string {
59
+ dateOfGmt := getHttpDateStr()
60
+ sessionKey := y.tokenInfo.SessionKey
61
+ sessionSecret := y.tokenInfo.SessionSecret
62
+ if isFamily {
63
+ sessionKey = y.tokenInfo.FamilySessionKey
64
+ sessionSecret = y.tokenInfo.FamilySessionSecret
65
+ }
66
+
67
+ header := map[string]string{
68
+ "Date": dateOfGmt,
69
+ "SessionKey": sessionKey,
70
+ "X-Request-ID": uuid.NewString(),
71
+ "Signature": signatureOfHmac(sessionSecret, sessionKey, method, url, dateOfGmt, params),
72
+ }
73
+ return header
74
+ }
75
+
76
+ func (y *Cloud189PC) EncryptParams(params Params, isFamily bool) string {
77
+ sessionSecret := y.tokenInfo.SessionSecret
78
+ if isFamily {
79
+ sessionSecret = y.tokenInfo.FamilySessionSecret
80
+ }
81
+ if params != nil {
82
+ return AesECBEncrypt(params.Encode(), sessionSecret[:16])
83
+ }
84
+ return ""
85
+ }
86
+
87
+ func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}, isFamily ...bool) ([]byte, error) {
88
+ req := y.client.R().SetQueryParams(clientSuffix())
89
+
90
+ // 设置params
91
+ paramsData := y.EncryptParams(params, isBool(isFamily...))
92
+ if paramsData != "" {
93
+ req.SetQueryParam("params", paramsData)
94
+ }
95
+
96
+ // Signature
97
+ req.SetHeaders(y.SignatureHeader(url, method, paramsData, isBool(isFamily...)))
98
+
99
+ var erron RespErr
100
+ req.SetError(&erron)
101
+
102
+ if callback != nil {
103
+ callback(req)
104
+ }
105
+ if resp != nil {
106
+ req.SetResult(resp)
107
+ }
108
+ res, err := req.Execute(method, url)
109
+ if err != nil {
110
+ return nil, err
111
+ }
112
+
113
+ if strings.Contains(res.String(), "userSessionBO is null") {
114
+ if err = y.refreshSession(); err != nil {
115
+ return nil, err
116
+ }
117
+ return y.request(url, method, callback, params, resp, isFamily...)
118
+ }
119
+
120
+ // if erron.ErrorCode == "InvalidSessionKey" || erron.Code == "InvalidSessionKey" {
121
+ if strings.Contains(res.String(), "InvalidSessionKey") {
122
+ if err = y.refreshSession(); err != nil {
123
+ return nil, err
124
+ }
125
+ return y.request(url, method, callback, params, resp, isFamily...)
126
+ }
127
+
128
+ // 处理错误
129
+ if erron.HasError() {
130
+ return nil, &erron
131
+ }
132
+ return res.Body(), nil
133
+ }
134
+
135
+ func (y *Cloud189PC) get(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {
136
+ return y.request(url, http.MethodGet, callback, nil, resp, isFamily...)
137
+ }
138
+
139
+ func (y *Cloud189PC) post(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {
140
+ return y.request(url, http.MethodPost, callback, nil, resp, isFamily...)
141
+ }
142
+
143
+ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]string, sign bool, file io.Reader, isFamily bool) ([]byte, error) {
144
+ req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, file)
145
+ if err != nil {
146
+ return nil, err
147
+ }
148
+
149
+ query := req.URL.Query()
150
+ for key, value := range clientSuffix() {
151
+ query.Add(key, value)
152
+ }
153
+ req.URL.RawQuery = query.Encode()
154
+
155
+ for key, value := range headers {
156
+ req.Header.Add(key, value)
157
+ }
158
+
159
+ if sign {
160
+ for key, value := range y.SignatureHeader(url, http.MethodPut, "", isFamily) {
161
+ req.Header.Add(key, value)
162
+ }
163
+ }
164
+
165
+ resp, err := base.HttpClient.Do(req)
166
+ if err != nil {
167
+ return nil, err
168
+ }
169
+ defer resp.Body.Close()
170
+
171
+ body, err := io.ReadAll(resp.Body)
172
+ if err != nil {
173
+ return nil, err
174
+ }
175
+
176
+ var erron RespErr
177
+ jsoniter.Unmarshal(body, &erron)
178
+ xml.Unmarshal(body, &erron)
179
+ if erron.HasError() {
180
+ return nil, &erron
181
+ }
182
+ if resp.StatusCode != http.StatusOK {
183
+ return nil, errors.Errorf("put fail,err:%s", string(body))
184
+ }
185
+ return body, nil
186
+ }
187
+ func (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool) ([]model.Obj, error) {
188
+ fullUrl := API_URL
189
+ if isFamily {
190
+ fullUrl += "/family/file"
191
+ }
192
+ fullUrl += "/listFiles.action"
193
+
194
+ res := make([]model.Obj, 0, 130)
195
+ for pageNum := 1; ; pageNum++ {
196
+ var resp Cloud189FilesResp
197
+ _, err := y.get(fullUrl, func(r *resty.Request) {
198
+ r.SetContext(ctx)
199
+ r.SetQueryParams(map[string]string{
200
+ "folderId": fileId,
201
+ "fileType": "0",
202
+ "mediaAttr": "0",
203
+ "iconOption": "5",
204
+ "pageNum": fmt.Sprint(pageNum),
205
+ "pageSize": "130",
206
+ })
207
+ if isFamily {
208
+ r.SetQueryParams(map[string]string{
209
+ "familyId": y.FamilyID,
210
+ "orderBy": toFamilyOrderBy(y.OrderBy),
211
+ "descending": toDesc(y.OrderDirection),
212
+ })
213
+ } else {
214
+ r.SetQueryParams(map[string]string{
215
+ "recursive": "0",
216
+ "orderBy": y.OrderBy,
217
+ "descending": toDesc(y.OrderDirection),
218
+ })
219
+ }
220
+ }, &resp, isFamily)
221
+ if err != nil {
222
+ return nil, err
223
+ }
224
+ // 获取完毕跳出
225
+ if resp.FileListAO.Count == 0 {
226
+ break
227
+ }
228
+
229
+ for i := 0; i < len(resp.FileListAO.FolderList); i++ {
230
+ res = append(res, &resp.FileListAO.FolderList[i])
231
+ }
232
+ for i := 0; i < len(resp.FileListAO.FileList); i++ {
233
+ res = append(res, &resp.FileListAO.FileList[i])
234
+ }
235
+ }
236
+ return res, nil
237
+ }
238
+
239
+ func (y *Cloud189PC) login() (err error) {
240
+ // 初始化登陆所需参数
241
+ if y.loginParam == nil {
242
+ if err = y.initLoginParam(); err != nil {
243
+ // 验证码也通过错误返回
244
+ return err
245
+ }
246
+ }
247
+ defer func() {
248
+ // 销毁验证码
249
+ y.VCode = ""
250
+ // 销毁登陆参数
251
+ y.loginParam = nil
252
+ // 遇到错误,重新加载登陆参数(刷新验证码)
253
+ if err != nil && y.NoUseOcr {
254
+ if err1 := y.initLoginParam(); err1 != nil {
255
+ err = fmt.Errorf("err1: %s \nerr2: %s", err, err1)
256
+ }
257
+ }
258
+ }()
259
+
260
+ param := y.loginParam
261
+ var loginresp LoginResp
262
+ _, err = y.client.R().
263
+ ForceContentType("application/json;charset=UTF-8").SetResult(&loginresp).
264
+ SetHeaders(map[string]string{
265
+ "REQID": param.ReqId,
266
+ "lt": param.Lt,
267
+ }).
268
+ SetFormData(map[string]string{
269
+ "appKey": APP_ID,
270
+ "accountType": ACCOUNT_TYPE,
271
+ "userName": param.RsaUsername,
272
+ "password": param.RsaPassword,
273
+ "validateCode": y.VCode,
274
+ "captchaToken": param.CaptchaToken,
275
+ "returnUrl": RETURN_URL,
276
+ // "mailSuffix": "@189.cn",
277
+ "dynamicCheck": "FALSE",
278
+ "clientType": CLIENT_TYPE,
279
+ "cb_SaveName": "1",
280
+ "isOauth2": "false",
281
+ "state": "",
282
+ "paramId": param.ParamId,
283
+ }).
284
+ Post(AUTH_URL + "/api/logbox/oauth2/loginSubmit.do")
285
+ if err != nil {
286
+ return err
287
+ }
288
+ if loginresp.ToUrl == "" {
289
+ return fmt.Errorf("login failed,No toUrl obtained, msg: %s", loginresp.Msg)
290
+ }
291
+
292
+ // 获取Session
293
+ var erron RespErr
294
+ var tokenInfo AppSessionResp
295
+ _, err = y.client.R().
296
+ SetResult(&tokenInfo).SetError(&erron).
297
+ SetQueryParams(clientSuffix()).
298
+ SetQueryParam("redirectURL", url.QueryEscape(loginresp.ToUrl)).
299
+ Post(API_URL + "/getSessionForPC.action")
300
+ if err != nil {
301
+ return
302
+ }
303
+
304
+ if erron.HasError() {
305
+ return &erron
306
+ }
307
+ if tokenInfo.ResCode != 0 {
308
+ err = fmt.Errorf(tokenInfo.ResMessage)
309
+ return
310
+ }
311
+ y.tokenInfo = &tokenInfo
312
+ return
313
+ }
314
+
315
+ /* 初始化登陆需要的参数
316
+ * 如果遇到验证码返回错误
317
+ */
318
+ func (y *Cloud189PC) initLoginParam() error {
319
+ // 清除cookie
320
+ jar, _ := cookiejar.New(nil)
321
+ y.client.SetCookieJar(jar)
322
+
323
+ res, err := y.client.R().
324
+ SetQueryParams(map[string]string{
325
+ "appId": APP_ID,
326
+ "clientType": CLIENT_TYPE,
327
+ "returnURL": RETURN_URL,
328
+ "timeStamp": fmt.Sprint(timestamp()),
329
+ }).
330
+ Get(WEB_URL + "/api/portal/unifyLoginForPC.action")
331
+ if err != nil {
332
+ return err
333
+ }
334
+
335
+ param := LoginParam{
336
+ CaptchaToken: regexp.MustCompile(`'captchaToken' value='(.+?)'`).FindStringSubmatch(res.String())[1],
337
+ Lt: regexp.MustCompile(`lt = "(.+?)"`).FindStringSubmatch(res.String())[1],
338
+ ParamId: regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(res.String())[1],
339
+ ReqId: regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(res.String())[1],
340
+ // jRsaKey: regexp.MustCompile(`"j_rsaKey" value="(.+?)"`).FindStringSubmatch(res.String())[1],
341
+ }
342
+
343
+ // 获取rsa公钥
344
+ var encryptConf EncryptConfResp
345
+ _, err = y.client.R().
346
+ ForceContentType("application/json;charset=UTF-8").SetResult(&encryptConf).
347
+ SetFormData(map[string]string{"appId": APP_ID}).
348
+ Post(AUTH_URL + "/api/logbox/config/encryptConf.do")
349
+ if err != nil {
350
+ return err
351
+ }
352
+
353
+ param.jRsaKey = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", encryptConf.Data.PubKey)
354
+ param.RsaUsername = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Username)
355
+ param.RsaPassword = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Password)
356
+ y.loginParam = &param
357
+
358
+ // 判断是否需要验证码
359
+ resp, err := y.client.R().
360
+ SetHeader("REQID", param.ReqId).
361
+ SetFormData(map[string]string{
362
+ "appKey": APP_ID,
363
+ "accountType": ACCOUNT_TYPE,
364
+ "userName": param.RsaUsername,
365
+ }).Post(AUTH_URL + "/api/logbox/oauth2/needcaptcha.do")
366
+ if err != nil {
367
+ return err
368
+ }
369
+ if resp.String() == "0" {
370
+ return nil
371
+ }
372
+
373
+ // 拉取验证码
374
+ imgRes, err := y.client.R().
375
+ SetQueryParams(map[string]string{
376
+ "token": param.CaptchaToken,
377
+ "REQID": param.ReqId,
378
+ "rnd": fmt.Sprint(timestamp()),
379
+ }).
380
+ Get(AUTH_URL + "/api/logbox/oauth2/picCaptcha.do")
381
+ if err != nil {
382
+ return fmt.Errorf("failed to obtain verification code")
383
+ }
384
+ if imgRes.Size() > 20 {
385
+ if setting.GetStr(conf.OcrApi) != "" && !y.NoUseOcr {
386
+ vRes, err := base.RestyClient.R().
387
+ SetMultipartField("image", "validateCode.png", "image/png", bytes.NewReader(imgRes.Body())).
388
+ Post(setting.GetStr(conf.OcrApi))
389
+ if err != nil {
390
+ return err
391
+ }
392
+ if jsoniter.Get(vRes.Body(), "status").ToInt() == 200 {
393
+ y.VCode = jsoniter.Get(vRes.Body(), "result").ToString()
394
+ return nil
395
+ }
396
+ }
397
+
398
+ // 返回验证码图片给前端
399
+ return fmt.Errorf(`need img validate code: <img src="data:image/png;base64,%s"/>`, base64.StdEncoding.EncodeToString(imgRes.Body()))
400
+ }
401
+ return nil
402
+ }
403
+
404
+ // 刷新会话
405
+ func (y *Cloud189PC) refreshSession() (err error) {
406
+ var erron RespErr
407
+ var userSessionResp UserSessionResp
408
+ _, err = y.client.R().
409
+ SetResult(&userSessionResp).SetError(&erron).
410
+ SetQueryParams(clientSuffix()).
411
+ SetQueryParams(map[string]string{
412
+ "appId": APP_ID,
413
+ "accessToken": y.tokenInfo.AccessToken,
414
+ }).
415
+ SetHeader("X-Request-ID", uuid.NewString()).
416
+ Get(API_URL + "/getSessionForPC.action")
417
+ if err != nil {
418
+ return err
419
+ }
420
+
421
+ // 错误影响正常访问,下线该储存
422
+ defer func() {
423
+ if err != nil {
424
+ y.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
425
+ op.MustSaveDriverStorage(y)
426
+ }
427
+ }()
428
+
429
+ if erron.HasError() {
430
+ if erron.ResCode == "UserInvalidOpenToken" {
431
+ if err = y.login(); err != nil {
432
+ return err
433
+ }
434
+ }
435
+ return &erron
436
+ }
437
+ y.tokenInfo.UserSessionResp = userSessionResp
438
+ return
439
+ }
440
+
441
+ // 普通上传
442
+ // 无法上传大小为0的文件
443
+ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
444
+ var sliceSize = partSize(file.GetSize())
445
+ count := int(math.Ceil(float64(file.GetSize()) / float64(sliceSize)))
446
+ lastPartSize := file.GetSize() % sliceSize
447
+ if file.GetSize() > 0 && lastPartSize == 0 {
448
+ lastPartSize = sliceSize
449
+ }
450
+
451
+ params := Params{
452
+ "parentFolderId": dstDir.GetID(),
453
+ "fileName": url.QueryEscape(file.GetName()),
454
+ "fileSize": fmt.Sprint(file.GetSize()),
455
+ "sliceSize": fmt.Sprint(sliceSize),
456
+ "lazyCheck": "1",
457
+ }
458
+
459
+ fullUrl := UPLOAD_URL
460
+ if isFamily {
461
+ params.Set("familyId", y.FamilyID)
462
+ fullUrl += "/family"
463
+ } else {
464
+ //params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
465
+ fullUrl += "/person"
466
+ }
467
+
468
+ // 初始化上传
469
+ var initMultiUpload InitMultiUploadResp
470
+ _, err := y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
471
+ req.SetContext(ctx)
472
+ }, params, &initMultiUpload, isFamily)
473
+ if err != nil {
474
+ return nil, err
475
+ }
476
+
477
+ threadG, upCtx := errgroup.NewGroupWithContext(ctx, y.uploadThread,
478
+ retry.Attempts(3),
479
+ retry.Delay(time.Second),
480
+ retry.DelayType(retry.BackOffDelay))
481
+
482
+ fileMd5 := md5.New()
483
+ silceMd5 := md5.New()
484
+ silceMd5Hexs := make([]string, 0, count)
485
+
486
+ for i := 1; i <= count; i++ {
487
+ if utils.IsCanceled(upCtx) {
488
+ break
489
+ }
490
+
491
+ byteData := make([]byte, sliceSize)
492
+ if i == count {
493
+ byteData = byteData[:lastPartSize]
494
+ }
495
+
496
+ // 读取块
497
+ silceMd5.Reset()
498
+ if _, err := io.ReadFull(io.TeeReader(file, io.MultiWriter(fileMd5, silceMd5)), byteData); err != io.EOF && err != nil {
499
+ return nil, err
500
+ }
501
+
502
+ // 计算块md5并进行hex和base64编码
503
+ md5Bytes := silceMd5.Sum(nil)
504
+ silceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Bytes)))
505
+ partInfo := fmt.Sprintf("%d-%s", i, base64.StdEncoding.EncodeToString(md5Bytes))
506
+
507
+ threadG.Go(func(ctx context.Context) error {
508
+ uploadUrls, err := y.GetMultiUploadUrls(ctx, isFamily, initMultiUpload.Data.UploadFileID, partInfo)
509
+ if err != nil {
510
+ return err
511
+ }
512
+
513
+ // step.4 上传切片
514
+ uploadUrl := uploadUrls[0]
515
+ _, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, bytes.NewReader(byteData), isFamily)
516
+ if err != nil {
517
+ return err
518
+ }
519
+ up(float64(threadG.Success()) * 100 / float64(count))
520
+ return nil
521
+ })
522
+ }
523
+ if err = threadG.Wait(); err != nil {
524
+ return nil, err
525
+ }
526
+
527
+ fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
528
+ sliceMd5Hex := fileMd5Hex
529
+ if file.GetSize() > sliceSize {
530
+ sliceMd5Hex = strings.ToUpper(utils.GetMD5EncodeStr(strings.Join(silceMd5Hexs, "\n")))
531
+ }
532
+
533
+ // 提交上传
534
+ var resp CommitMultiUploadFileResp
535
+ _, err = y.request(fullUrl+"/commitMultiUploadFile", http.MethodGet,
536
+ func(req *resty.Request) {
537
+ req.SetContext(ctx)
538
+ }, Params{
539
+ "uploadFileId": initMultiUpload.Data.UploadFileID,
540
+ "fileMd5": fileMd5Hex,
541
+ "sliceMd5": sliceMd5Hex,
542
+ "lazyCheck": "1",
543
+ "isLog": "0",
544
+ "opertype": IF(overwrite, "3", "1"),
545
+ }, &resp, isFamily)
546
+ if err != nil {
547
+ return nil, err
548
+ }
549
+ return resp.toFile(), nil
550
+ }
551
+
552
+ func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, isFamily bool, overwrite bool) (model.Obj, error) {
553
+ fileMd5 := stream.GetHash().GetHash(utils.MD5)
554
+ if len(fileMd5) < utils.MD5.Width {
555
+ return nil, errors.New("invalid hash")
556
+ }
557
+
558
+ uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, stream.GetName(), fmt.Sprint(stream.GetSize()), isFamily)
559
+ if err != nil {
560
+ return nil, err
561
+ }
562
+
563
+ if uploadInfo.FileDataExists != 1 {
564
+ return nil, errors.New("rapid upload fail")
565
+ }
566
+
567
+ return y.OldUploadCommit(ctx, uploadInfo.FileCommitUrl, uploadInfo.UploadFileId, isFamily, overwrite)
568
+ }
569
+
570
+ // 快传
571
+ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
572
+ tempFile, err := file.CacheFullInTempFile()
573
+ if err != nil {
574
+ return nil, err
575
+ }
576
+
577
+ var sliceSize = partSize(file.GetSize())
578
+ count := int(math.Ceil(float64(file.GetSize()) / float64(sliceSize)))
579
+ lastSliceSize := file.GetSize() % sliceSize
580
+ if file.GetSize() > 0 && lastSliceSize == 0 {
581
+ lastSliceSize = sliceSize
582
+ }
583
+
584
+ //step.1 优先计算所需信息
585
+ byteSize := sliceSize
586
+ fileMd5 := md5.New()
587
+ silceMd5 := md5.New()
588
+ silceMd5Hexs := make([]string, 0, count)
589
+ partInfos := make([]string, 0, count)
590
+ for i := 1; i <= count; i++ {
591
+ if utils.IsCanceled(ctx) {
592
+ return nil, ctx.Err()
593
+ }
594
+
595
+ if i == count {
596
+ byteSize = lastSliceSize
597
+ }
598
+
599
+ silceMd5.Reset()
600
+ if _, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5, silceMd5), tempFile, byteSize); err != nil && err != io.EOF {
601
+ return nil, err
602
+ }
603
+ md5Byte := silceMd5.Sum(nil)
604
+ silceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Byte)))
605
+ partInfos = append(partInfos, fmt.Sprint(i, "-", base64.StdEncoding.EncodeToString(md5Byte)))
606
+ }
607
+
608
+ fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
609
+ sliceMd5Hex := fileMd5Hex
610
+ if file.GetSize() > sliceSize {
611
+ sliceMd5Hex = strings.ToUpper(utils.GetMD5EncodeStr(strings.Join(silceMd5Hexs, "\n")))
612
+ }
613
+
614
+ fullUrl := UPLOAD_URL
615
+ if isFamily {
616
+ fullUrl += "/family"
617
+ } else {
618
+ //params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
619
+ fullUrl += "/person"
620
+ }
621
+
622
+ // 尝试恢复进度
623
+ uploadProgress, ok := base.GetUploadProgress[*UploadProgress](y, y.tokenInfo.SessionKey, fileMd5Hex)
624
+ if !ok {
625
+ //step.2 预上传
626
+ params := Params{
627
+ "parentFolderId": dstDir.GetID(),
628
+ "fileName": url.QueryEscape(file.GetName()),
629
+ "fileSize": fmt.Sprint(file.GetSize()),
630
+ "fileMd5": fileMd5Hex,
631
+ "sliceSize": fmt.Sprint(sliceSize),
632
+ "sliceMd5": sliceMd5Hex,
633
+ }
634
+ if isFamily {
635
+ params.Set("familyId", y.FamilyID)
636
+ }
637
+ var uploadInfo InitMultiUploadResp
638
+ _, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
639
+ req.SetContext(ctx)
640
+ }, params, &uploadInfo, isFamily)
641
+ if err != nil {
642
+ return nil, err
643
+ }
644
+ uploadProgress = &UploadProgress{
645
+ UploadInfo: uploadInfo,
646
+ UploadParts: partInfos,
647
+ }
648
+ }
649
+
650
+ uploadInfo := uploadProgress.UploadInfo.Data
651
+ // 网盘中不存在该文件,开始上传
652
+ if uploadInfo.FileDataExists != 1 {
653
+ threadG, upCtx := errgroup.NewGroupWithContext(ctx, y.uploadThread,
654
+ retry.Attempts(3),
655
+ retry.Delay(time.Second),
656
+ retry.DelayType(retry.BackOffDelay))
657
+ for i, uploadPart := range uploadProgress.UploadParts {
658
+ if utils.IsCanceled(upCtx) {
659
+ break
660
+ }
661
+
662
+ i, uploadPart := i, uploadPart
663
+ threadG.Go(func(ctx context.Context) error {
664
+ // step.3 获取上传链接
665
+ uploadUrls, err := y.GetMultiUploadUrls(ctx, isFamily, uploadInfo.UploadFileID, uploadPart)
666
+ if err != nil {
667
+ return err
668
+ }
669
+ uploadUrl := uploadUrls[0]
670
+
671
+ byteSize, offset := sliceSize, int64(uploadUrl.PartNumber-1)*sliceSize
672
+ if uploadUrl.PartNumber == count {
673
+ byteSize = lastSliceSize
674
+ }
675
+
676
+ // step.4 上传切片
677
+ _, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, io.NewSectionReader(tempFile, offset, byteSize), isFamily)
678
+ if err != nil {
679
+ return err
680
+ }
681
+
682
+ up(float64(threadG.Success()) * 100 / float64(len(uploadUrls)))
683
+ uploadProgress.UploadParts[i] = ""
684
+ return nil
685
+ })
686
+ }
687
+ if err = threadG.Wait(); err != nil {
688
+ if errors.Is(err, context.Canceled) {
689
+ uploadProgress.UploadParts = utils.SliceFilter(uploadProgress.UploadParts, func(s string) bool { return s != "" })
690
+ base.SaveUploadProgress(y, uploadProgress, y.tokenInfo.SessionKey, fileMd5Hex)
691
+ }
692
+ return nil, err
693
+ }
694
+ }
695
+
696
+ // step.5 提交
697
+ var resp CommitMultiUploadFileResp
698
+ _, err = y.request(fullUrl+"/commitMultiUploadFile", http.MethodGet,
699
+ func(req *resty.Request) {
700
+ req.SetContext(ctx)
701
+ }, Params{
702
+ "uploadFileId": uploadInfo.UploadFileID,
703
+ "isLog": "0",
704
+ "opertype": IF(overwrite, "3", "1"),
705
+ }, &resp, isFamily)
706
+ if err != nil {
707
+ return nil, err
708
+ }
709
+ return resp.toFile(), nil
710
+ }
711
+
712
+ // 获取上传切片信息
713
+ // 对http body有大小限制,分片信息太多会出错
714
+ func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, isFamily bool, uploadFileId string, partInfo ...string) ([]UploadUrlInfo, error) {
715
+ fullUrl := UPLOAD_URL
716
+ if isFamily {
717
+ fullUrl += "/family"
718
+ } else {
719
+ fullUrl += "/person"
720
+ }
721
+
722
+ var uploadUrlsResp UploadUrlsResp
723
+ _, err := y.request(fullUrl+"/getMultiUploadUrls", http.MethodGet,
724
+ func(req *resty.Request) {
725
+ req.SetContext(ctx)
726
+ }, Params{
727
+ "uploadFileId": uploadFileId,
728
+ "partInfo": strings.Join(partInfo, ","),
729
+ }, &uploadUrlsResp, isFamily)
730
+ if err != nil {
731
+ return nil, err
732
+ }
733
+ uploadUrls := uploadUrlsResp.Data
734
+
735
+ if len(uploadUrls) != len(partInfo) {
736
+ return nil, fmt.Errorf("uploadUrls get error, due to get length %d, real length %d", len(partInfo), len(uploadUrls))
737
+ }
738
+
739
+ uploadUrlInfos := make([]UploadUrlInfo, 0, len(uploadUrls))
740
+ for k, uploadUrl := range uploadUrls {
741
+ partNumber, err := strconv.Atoi(strings.TrimPrefix(k, "partNumber_"))
742
+ if err != nil {
743
+ return nil, err
744
+ }
745
+ uploadUrlInfos = append(uploadUrlInfos, UploadUrlInfo{
746
+ PartNumber: partNumber,
747
+ Headers: ParseHttpHeader(uploadUrl.RequestHeader),
748
+ UploadUrlsData: uploadUrl,
749
+ })
750
+ }
751
+ sort.Slice(uploadUrlInfos, func(i, j int) bool {
752
+ return uploadUrlInfos[i].PartNumber < uploadUrlInfos[j].PartNumber
753
+ })
754
+ return uploadUrlInfos, nil
755
+ }
756
+
757
+ // 旧版本上传,家庭云不支持覆盖
758
+ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
759
+ tempFile, err := file.CacheFullInTempFile()
760
+ if err != nil {
761
+ return nil, err
762
+ }
763
+ fileMd5, err := utils.HashFile(utils.MD5, tempFile)
764
+ if err != nil {
765
+ return nil, err
766
+ }
767
+
768
+ // 创建上传会话
769
+ uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, file.GetName(), fmt.Sprint(file.GetSize()), isFamily)
770
+ if err != nil {
771
+ return nil, err
772
+ }
773
+
774
+ // 网盘中不存在该文件,开始上传
775
+ status := GetUploadFileStatusResp{CreateUploadFileResp: *uploadInfo}
776
+ for status.GetSize() < file.GetSize() && status.FileDataExists != 1 {
777
+ if utils.IsCanceled(ctx) {
778
+ return nil, ctx.Err()
779
+ }
780
+
781
+ header := map[string]string{
782
+ "ResumePolicy": "1",
783
+ "Expect": "100-continue",
784
+ }
785
+
786
+ if isFamily {
787
+ header["FamilyId"] = fmt.Sprint(y.FamilyID)
788
+ header["UploadFileId"] = fmt.Sprint(status.UploadFileId)
789
+ } else {
790
+ header["Edrive-UploadFileId"] = fmt.Sprint(status.UploadFileId)
791
+ }
792
+
793
+ _, err := y.put(ctx, status.FileUploadUrl, header, true, io.NopCloser(tempFile), isFamily)
794
+ if err, ok := err.(*RespErr); ok && err.Code != "InputStreamReadError" {
795
+ return nil, err
796
+ }
797
+
798
+ // 获取断点状态
799
+ fullUrl := API_URL + "/getUploadFileStatus.action"
800
+ if y.isFamily() {
801
+ fullUrl = API_URL + "/family/file/getFamilyFileStatus.action"
802
+ }
803
+ _, err = y.get(fullUrl, func(req *resty.Request) {
804
+ req.SetContext(ctx).SetQueryParams(map[string]string{
805
+ "uploadFileId": fmt.Sprint(status.UploadFileId),
806
+ "resumePolicy": "1",
807
+ })
808
+ if isFamily {
809
+ req.SetQueryParam("familyId", fmt.Sprint(y.FamilyID))
810
+ }
811
+ }, &status, isFamily)
812
+ if err != nil {
813
+ return nil, err
814
+ }
815
+ if _, err := tempFile.Seek(status.GetSize(), io.SeekStart); err != nil {
816
+ return nil, err
817
+ }
818
+ up(float64(status.GetSize()) / float64(file.GetSize()) * 100)
819
+ }
820
+
821
+ return y.OldUploadCommit(ctx, status.FileCommitUrl, status.UploadFileId, isFamily, overwrite)
822
+ }
823
+
824
+ // 创建上传会话
825
+ func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileMd5, fileName, fileSize string, isFamily bool) (*CreateUploadFileResp, error) {
826
+ var uploadInfo CreateUploadFileResp
827
+
828
+ fullUrl := API_URL + "/createUploadFile.action"
829
+ if isFamily {
830
+ fullUrl = API_URL + "/family/file/createFamilyFile.action"
831
+ }
832
+ _, err := y.post(fullUrl, func(req *resty.Request) {
833
+ req.SetContext(ctx)
834
+ if isFamily {
835
+ req.SetQueryParams(map[string]string{
836
+ "familyId": y.FamilyID,
837
+ "parentId": parentID,
838
+ "fileMd5": fileMd5,
839
+ "fileName": fileName,
840
+ "fileSize": fileSize,
841
+ "resumePolicy": "1",
842
+ })
843
+ } else {
844
+ req.SetFormData(map[string]string{
845
+ "parentFolderId": parentID,
846
+ "fileName": fileName,
847
+ "size": fileSize,
848
+ "md5": fileMd5,
849
+ "opertype": "3",
850
+ "flag": "1",
851
+ "resumePolicy": "1",
852
+ "isLog": "0",
853
+ })
854
+ }
855
+ }, &uploadInfo, isFamily)
856
+
857
+ if err != nil {
858
+ return nil, err
859
+ }
860
+ return &uploadInfo, nil
861
+ }
862
+
863
+ // 提交上传文件
864
+ func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string, uploadFileID int64, isFamily bool, overwrite bool) (model.Obj, error) {
865
+ var resp OldCommitUploadFileResp
866
+ _, err := y.post(fileCommitUrl, func(req *resty.Request) {
867
+ req.SetContext(ctx)
868
+ if isFamily {
869
+ req.SetHeaders(map[string]string{
870
+ "ResumePolicy": "1",
871
+ "UploadFileId": fmt.Sprint(uploadFileID),
872
+ "FamilyId": fmt.Sprint(y.FamilyID),
873
+ })
874
+ } else {
875
+ req.SetFormData(map[string]string{
876
+ "opertype": IF(overwrite, "3", "1"),
877
+ "resumePolicy": "1",
878
+ "uploadFileId": fmt.Sprint(uploadFileID),
879
+ "isLog": "0",
880
+ })
881
+ }
882
+ }, &resp, isFamily)
883
+ if err != nil {
884
+ return nil, err
885
+ }
886
+ return resp.toFile(), nil
887
+ }
888
+
889
+ func (y *Cloud189PC) isFamily() bool {
890
+ return y.Type == "family"
891
+ }
892
+
893
+ func (y *Cloud189PC) isLogin() bool {
894
+ if y.tokenInfo == nil {
895
+ return false
896
+ }
897
+ _, err := y.get(API_URL+"/getUserInfo.action", nil, nil)
898
+ return err == nil
899
+ }
900
+
901
+ // 创建家庭云中转文件夹
902
+ func (y *Cloud189PC) createFamilyTransferFolder(count int) (*ring.Ring, error) {
903
+ folders := ring.New(count)
904
+ var rootFolder Cloud189Folder
905
+ _, err := y.post(API_URL+"/family/file/createFolder.action", func(req *resty.Request) {
906
+ req.SetQueryParams(map[string]string{
907
+ "folderName": "FamilyTransferFolder",
908
+ "familyId": y.FamilyID,
909
+ })
910
+ }, &rootFolder, true)
911
+ if err != nil {
912
+ return nil, err
913
+ }
914
+
915
+ folderCount := 0
916
+
917
+ // 获取已有目录
918
+ files, err := y.getFiles(context.TODO(), rootFolder.GetID(), true)
919
+ if err != nil {
920
+ return nil, err
921
+ }
922
+ for _, file := range files {
923
+ if folder, ok := file.(*Cloud189Folder); ok {
924
+ folders.Value = folder
925
+ folders = folders.Next()
926
+ folderCount++
927
+ }
928
+ }
929
+
930
+ // 创建新的目录
931
+ for folderCount < count {
932
+ var newFolder Cloud189Folder
933
+ _, err := y.post(API_URL+"/family/file/createFolder.action", func(req *resty.Request) {
934
+ req.SetQueryParams(map[string]string{
935
+ "folderName": uuid.NewString(),
936
+ "familyId": y.FamilyID,
937
+ "parentId": rootFolder.GetID(),
938
+ })
939
+ }, &newFolder, true)
940
+ if err != nil {
941
+ return nil, err
942
+ }
943
+ folders.Value = &newFolder
944
+ folders = folders.Next()
945
+ folderCount++
946
+ }
947
+ return folders, nil
948
+ }
949
+
950
+ // 清理中转文件夹
951
+ func (y *Cloud189PC) cleanFamilyTransfer(ctx context.Context) error {
952
+ var tasks []BatchTaskInfo
953
+ r := y.familyTransferFolder
954
+ for p := r.Next(); p != r; p = p.Next() {
955
+ folder := p.Value.(*Cloud189Folder)
956
+
957
+ files, err := y.getFiles(ctx, folder.GetID(), true)
958
+ if err != nil {
959
+ return err
960
+ }
961
+ for _, file := range files {
962
+ tasks = append(tasks, BatchTaskInfo{
963
+ FileId: file.GetID(),
964
+ FileName: file.GetName(),
965
+ IsFolder: BoolToNumber(file.IsDir()),
966
+ })
967
+ }
968
+ }
969
+
970
+ if len(tasks) > 0 {
971
+ // 删除
972
+ resp, err := y.CreateBatchTask("DELETE", y.FamilyID, "", nil, tasks...)
973
+ if err != nil {
974
+ return err
975
+ }
976
+ err = y.WaitBatchTask("DELETE", resp.TaskID, time.Second)
977
+ if err != nil {
978
+ return err
979
+ }
980
+ // 永久删除
981
+ resp, err = y.CreateBatchTask("CLEAR_RECYCLE", y.FamilyID, "", nil, tasks...)
982
+ if err != nil {
983
+ return err
984
+ }
985
+ err = y.WaitBatchTask("CLEAR_RECYCLE", resp.TaskID, time.Second)
986
+ return err
987
+ }
988
+ return nil
989
+ }
990
+
991
+ // 获取家庭云所有用户信息
992
+ func (y *Cloud189PC) getFamilyInfoList() ([]FamilyInfoResp, error) {
993
+ var resp FamilyInfoListResp
994
+ _, err := y.get(API_URL+"/family/manage/getFamilyList.action", nil, &resp, true)
995
+ if err != nil {
996
+ return nil, err
997
+ }
998
+ return resp.FamilyInfoResp, nil
999
+ }
1000
+
1001
+ // 抽取家庭云ID
1002
+ func (y *Cloud189PC) getFamilyID() (string, error) {
1003
+ infos, err := y.getFamilyInfoList()
1004
+ if err != nil {
1005
+ return "", err
1006
+ }
1007
+ if len(infos) == 0 {
1008
+ return "", fmt.Errorf("cannot get automatically,please input family_id")
1009
+ }
1010
+ for _, info := range infos {
1011
+ if strings.Contains(y.tokenInfo.LoginName, info.RemarkName) {
1012
+ return fmt.Sprint(info.FamilyID), nil
1013
+ }
1014
+ }
1015
+ return fmt.Sprint(infos[0].FamilyID), nil
1016
+ }
1017
+
1018
+ // 保存家庭云中的文件到个人云
1019
+ func (y *Cloud189PC) SaveFamilyFileToPersonCloud(ctx context.Context, familyId string, srcObj, dstDir model.Obj, overwrite bool) error {
1020
+ // _, err := y.post(API_URL+"/family/file/saveFileToMember.action", func(req *resty.Request) {
1021
+ // req.SetQueryParams(map[string]string{
1022
+ // "channelId": "home",
1023
+ // "familyId": familyId,
1024
+ // "destParentId": destParentId,
1025
+ // "fileIdList": familyFileId,
1026
+ // })
1027
+ // }, nil)
1028
+ // return err
1029
+
1030
+ task := BatchTaskInfo{
1031
+ FileId: srcObj.GetID(),
1032
+ FileName: srcObj.GetName(),
1033
+ IsFolder: BoolToNumber(srcObj.IsDir()),
1034
+ }
1035
+ resp, err := y.CreateBatchTask("COPY", familyId, dstDir.GetID(), map[string]string{
1036
+ "groupId": "null",
1037
+ "copyType": "2",
1038
+ "shareId": "null",
1039
+ }, task)
1040
+ if err != nil {
1041
+ return err
1042
+ }
1043
+
1044
+ for {
1045
+ state, err := y.CheckBatchTask("COPY", resp.TaskID)
1046
+ if err != nil {
1047
+ return err
1048
+ }
1049
+ switch state.TaskStatus {
1050
+ case 2:
1051
+ task.DealWay = IF(overwrite, 3, 2)
1052
+ // 冲突时覆盖文件
1053
+ if err := y.ManageBatchTask("COPY", resp.TaskID, dstDir.GetID(), task); err != nil {
1054
+ return err
1055
+ }
1056
+ case 4:
1057
+ return nil
1058
+ }
1059
+ time.Sleep(time.Millisecond * 400)
1060
+ }
1061
+ }
1062
+
1063
+ func (y *Cloud189PC) CreateBatchTask(aType string, familyID string, targetFolderId string, other map[string]string, taskInfos ...BatchTaskInfo) (*CreateBatchTaskResp, error) {
1064
+ var resp CreateBatchTaskResp
1065
+ _, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
1066
+ req.SetFormData(map[string]string{
1067
+ "type": aType,
1068
+ "taskInfos": MustString(utils.Json.MarshalToString(taskInfos)),
1069
+ })
1070
+ if targetFolderId != "" {
1071
+ req.SetFormData(map[string]string{"targetFolderId": targetFolderId})
1072
+ }
1073
+ if familyID != "" {
1074
+ req.SetFormData(map[string]string{"familyId": familyID})
1075
+ }
1076
+ req.SetFormData(other)
1077
+ }, &resp, familyID != "")
1078
+ if err != nil {
1079
+ return nil, err
1080
+ }
1081
+ return &resp, nil
1082
+ }
1083
+
1084
+ // 检测任务状态
1085
+ func (y *Cloud189PC) CheckBatchTask(aType string, taskID string) (*BatchTaskStateResp, error) {
1086
+ var resp BatchTaskStateResp
1087
+ _, err := y.post(API_URL+"/batch/checkBatchTask.action", func(req *resty.Request) {
1088
+ req.SetFormData(map[string]string{
1089
+ "type": aType,
1090
+ "taskId": taskID,
1091
+ })
1092
+ }, &resp)
1093
+ if err != nil {
1094
+ return nil, err
1095
+ }
1096
+ return &resp, nil
1097
+ }
1098
+
1099
+ // 获取冲突的任务信息
1100
+ func (y *Cloud189PC) GetConflictTaskInfo(aType string, taskID string) (*BatchTaskConflictTaskInfoResp, error) {
1101
+ var resp BatchTaskConflictTaskInfoResp
1102
+ _, err := y.post(API_URL+"/batch/getConflictTaskInfo.action", func(req *resty.Request) {
1103
+ req.SetFormData(map[string]string{
1104
+ "type": aType,
1105
+ "taskId": taskID,
1106
+ })
1107
+ }, &resp)
1108
+ if err != nil {
1109
+ return nil, err
1110
+ }
1111
+ return &resp, nil
1112
+ }
1113
+
1114
+ // 处理冲突
1115
+ func (y *Cloud189PC) ManageBatchTask(aType string, taskID string, targetFolderId string, taskInfos ...BatchTaskInfo) error {
1116
+ _, err := y.post(API_URL+"/batch/manageBatchTask.action", func(req *resty.Request) {
1117
+ req.SetFormData(map[string]string{
1118
+ "targetFolderId": targetFolderId,
1119
+ "type": aType,
1120
+ "taskId": taskID,
1121
+ "taskInfos": MustString(utils.Json.MarshalToString(taskInfos)),
1122
+ })
1123
+ }, nil)
1124
+ return err
1125
+ }
1126
+
1127
+ var ErrIsConflict = errors.New("there is a conflict with the target object")
1128
+
1129
+ // 等待任务完成
1130
+ func (y *Cloud189PC) WaitBatchTask(aType string, taskID string, t time.Duration) error {
1131
+ for {
1132
+ state, err := y.CheckBatchTask(aType, taskID)
1133
+ if err != nil {
1134
+ return err
1135
+ }
1136
+ switch state.TaskStatus {
1137
+ case 2:
1138
+ return ErrIsConflict
1139
+ case 4:
1140
+ return nil
1141
+ }
1142
+ time.Sleep(t)
1143
+ }
1144
+ }