Mr.L
commited on
Commit
·
7107f0b
1
Parent(s):
ff2ab3b
feat: add full alist source code for Docker build
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- cmd/admin.go +100 -0
- cmd/cancel2FA.go +46 -0
- cmd/common.go +49 -0
- cmd/flags/config.go +10 -0
- cmd/lang.go +161 -0
- cmd/restart.go +32 -0
- cmd/root.go +35 -0
- cmd/server.go +181 -0
- cmd/start.go +71 -0
- cmd/stop.go +58 -0
- cmd/storage.go +163 -0
- cmd/user.go +52 -0
- cmd/version.go +43 -0
- drivers/115/appver.go +43 -0
- drivers/115/driver.go +251 -0
- drivers/115/meta.go +29 -0
- drivers/115/types.go +38 -0
- drivers/115/util.go +537 -0
- drivers/115_share/driver.go +112 -0
- drivers/115_share/meta.go +34 -0
- drivers/115_share/utils.go +111 -0
- drivers/123/driver.go +267 -0
- drivers/123/meta.go +27 -0
- drivers/123/types.go +123 -0
- drivers/123/upload.go +155 -0
- drivers/123/util.go +281 -0
- drivers/123_link/driver.go +77 -0
- drivers/123_link/meta.go +23 -0
- drivers/123_link/parse.go +152 -0
- drivers/123_link/types.go +66 -0
- drivers/123_link/util.go +30 -0
- drivers/123_share/driver.go +161 -0
- drivers/123_share/meta.go +35 -0
- drivers/123_share/types.go +99 -0
- drivers/123_share/util.go +117 -0
- drivers/139/driver.go +653 -0
- drivers/139/meta.go +25 -0
- drivers/139/types.go +255 -0
- drivers/139/util.go +438 -0
- drivers/189/driver.go +197 -0
- drivers/189/help.go +186 -0
- drivers/189/login.go +126 -0
- drivers/189/meta.go +26 -0
- drivers/189/types.go +68 -0
- drivers/189/util.go +398 -0
- drivers/189pc/driver.go +361 -0
- drivers/189pc/help.go +210 -0
- drivers/189pc/meta.go +34 -0
- drivers/189pc/types.go +398 -0
- drivers/189pc/utils.go +1144 -0
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¶ms=%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, "&", "&"), "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("¶ms=%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 = ¶m
|
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 |
+
}
|