mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2025-11-25 11:29:29 +08:00
feat(189tv): add 189cloudTV driver (#418)
This commit is contained in:
274
drivers/189_tv/driver.go
Normal file
274
drivers/189_tv/driver.go
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
package _189_tv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/ring"
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/OpenListTeam/OpenList/drivers/base"
|
||||||
|
"github.com/OpenListTeam/OpenList/internal/driver"
|
||||||
|
"github.com/OpenListTeam/OpenList/internal/errs"
|
||||||
|
"github.com/OpenListTeam/OpenList/internal/model"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cloud189TV struct {
|
||||||
|
model.Storage
|
||||||
|
Addition
|
||||||
|
client *resty.Client
|
||||||
|
tokenInfo *AppSessionResp
|
||||||
|
uploadThread int
|
||||||
|
familyTransferFolder *ring.Ring
|
||||||
|
cleanFamilyTransferFile func()
|
||||||
|
storageConfig driver.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) Config() driver.Config {
|
||||||
|
if y.storageConfig.Name == "" {
|
||||||
|
y.storageConfig = config
|
||||||
|
}
|
||||||
|
return y.storageConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) GetAddition() driver.Additional {
|
||||||
|
return &y.Addition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) Init(ctx context.Context) (err error) {
|
||||||
|
// 兼容旧上传接口
|
||||||
|
y.storageConfig.NoOverwriteUpload = y.isFamily() && y.Addition.RapidUpload
|
||||||
|
|
||||||
|
// 处理个人云和家庭云参数
|
||||||
|
if y.isFamily() && y.RootFolderID == "-11" {
|
||||||
|
y.RootFolderID = ""
|
||||||
|
}
|
||||||
|
if !y.isFamily() && y.RootFolderID == "" {
|
||||||
|
y.RootFolderID = "-11"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制上传线程数
|
||||||
|
y.uploadThread, _ = strconv.Atoi(y.UploadThread)
|
||||||
|
if y.uploadThread < 1 || y.uploadThread > 32 {
|
||||||
|
y.uploadThread, y.UploadThread = 3, "3"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化请求客户端
|
||||||
|
if y.client == nil {
|
||||||
|
y.client = base.NewRestyClient().SetHeaders(
|
||||||
|
map[string]string{
|
||||||
|
"Accept": "application/json;charset=UTF-8",
|
||||||
|
"User-Agent": "EcloudTV/6.5.5 (PJX110; unknown; home02) Android/35",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 避免重复登陆
|
||||||
|
if !y.isLogin() || y.Addition.AccessToken == "" {
|
||||||
|
if err = y.login(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理家庭云ID
|
||||||
|
if y.FamilyID == "" {
|
||||||
|
if y.FamilyID, err = y.getFamilyID(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) Drop(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
|
return y.getFiles(ctx, dir.GetID(), y.isFamily())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
|
var downloadUrl struct {
|
||||||
|
URL string `json:"fileDownloadUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
isFamily := y.isFamily()
|
||||||
|
fullUrl := ApiUrl
|
||||||
|
if isFamily {
|
||||||
|
fullUrl += "/family/file"
|
||||||
|
}
|
||||||
|
fullUrl += "/getFileDownloadUrl.action"
|
||||||
|
|
||||||
|
_, err := y.get(fullUrl, func(r *resty.Request) {
|
||||||
|
r.SetContext(ctx)
|
||||||
|
r.SetQueryParam("fileId", file.GetID())
|
||||||
|
if isFamily {
|
||||||
|
r.SetQueryParams(map[string]string{
|
||||||
|
"familyId": y.FamilyID,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
r.SetQueryParams(map[string]string{
|
||||||
|
"dt": "3",
|
||||||
|
"flag": "1",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, &downloadUrl, isFamily)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重定向获取真实链接
|
||||||
|
downloadUrl.URL = strings.Replace(strings.ReplaceAll(downloadUrl.URL, "&", "&"), "http://", "https://", 1)
|
||||||
|
res, err := base.NoRedirectClient.R().SetContext(ctx).SetDoNotParseResponse(true).Get(downloadUrl.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.RawBody().Close()
|
||||||
|
if res.StatusCode() == 302 {
|
||||||
|
downloadUrl.URL = res.Header().Get("location")
|
||||||
|
}
|
||||||
|
|
||||||
|
like := &model.Link{
|
||||||
|
URL: downloadUrl.URL,
|
||||||
|
Header: http.Header{
|
||||||
|
"User-Agent": []string{base.UserAgent},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return like, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||||
|
isFamily := y.isFamily()
|
||||||
|
fullUrl := ApiUrl
|
||||||
|
if isFamily {
|
||||||
|
fullUrl += "/family/file"
|
||||||
|
}
|
||||||
|
fullUrl += "/createFolder.action"
|
||||||
|
|
||||||
|
var newFolder Cloud189Folder
|
||||||
|
_, err := y.post(fullUrl, func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx)
|
||||||
|
req.SetQueryParams(map[string]string{
|
||||||
|
"folderName": dirName,
|
||||||
|
"relativePath": "",
|
||||||
|
})
|
||||||
|
if isFamily {
|
||||||
|
req.SetQueryParams(map[string]string{
|
||||||
|
"familyId": y.FamilyID,
|
||||||
|
"parentId": parentDir.GetID(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
req.SetQueryParams(map[string]string{
|
||||||
|
"parentFolderId": parentDir.GetID(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, &newFolder, isFamily)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &newFolder, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
|
isFamily := y.isFamily()
|
||||||
|
other := map[string]string{"targetFileName": dstDir.GetName()}
|
||||||
|
|
||||||
|
resp, err := y.CreateBatchTask("MOVE", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
|
||||||
|
FileId: srcObj.GetID(),
|
||||||
|
FileName: srcObj.GetName(),
|
||||||
|
IsFolder: BoolToNumber(srcObj.IsDir()),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = y.WaitBatchTask("MOVE", resp.TaskID, time.Millisecond*400); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return srcObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||||
|
isFamily := y.isFamily()
|
||||||
|
queryParam := make(map[string]string)
|
||||||
|
fullUrl := ApiUrl
|
||||||
|
method := http.MethodPost
|
||||||
|
if isFamily {
|
||||||
|
fullUrl += "/family/file"
|
||||||
|
method = http.MethodGet
|
||||||
|
queryParam["familyId"] = y.FamilyID
|
||||||
|
}
|
||||||
|
|
||||||
|
var newObj model.Obj
|
||||||
|
switch f := srcObj.(type) {
|
||||||
|
case *Cloud189File:
|
||||||
|
fullUrl += "/renameFile.action"
|
||||||
|
queryParam["fileId"] = srcObj.GetID()
|
||||||
|
queryParam["destFileName"] = newName
|
||||||
|
newObj = &Cloud189File{Icon: f.Icon} // 复用预览
|
||||||
|
case *Cloud189Folder:
|
||||||
|
fullUrl += "/renameFolder.action"
|
||||||
|
queryParam["folderId"] = srcObj.GetID()
|
||||||
|
queryParam["destFolderName"] = newName
|
||||||
|
newObj = &Cloud189Folder{}
|
||||||
|
default:
|
||||||
|
return nil, errs.NotSupport
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := y.request(fullUrl, method, func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx).SetQueryParams(queryParam)
|
||||||
|
}, nil, newObj, isFamily)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
|
isFamily := y.isFamily()
|
||||||
|
other := map[string]string{"targetFileName": dstDir.GetName()}
|
||||||
|
|
||||||
|
resp, err := y.CreateBatchTask("COPY", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
|
||||||
|
FileId: srcObj.GetID(),
|
||||||
|
FileName: srcObj.GetName(),
|
||||||
|
IsFolder: BoolToNumber(srcObj.IsDir()),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return y.WaitBatchTask("COPY", resp.TaskID, time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
|
isFamily := y.isFamily()
|
||||||
|
|
||||||
|
resp, err := y.CreateBatchTask("DELETE", IF(isFamily, y.FamilyID, ""), "", nil, BatchTaskInfo{
|
||||||
|
FileId: obj.GetID(),
|
||||||
|
FileName: obj.GetName(),
|
||||||
|
IsFolder: BoolToNumber(obj.IsDir()),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 批量任务数量限制,过快会导致无法删除
|
||||||
|
return y.WaitBatchTask("DELETE", resp.TaskID, time.Millisecond*200)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (newObj model.Obj, err error) {
|
||||||
|
overwrite := true
|
||||||
|
isFamily := y.isFamily()
|
||||||
|
|
||||||
|
// 响应时间长,按需启用
|
||||||
|
if y.Addition.RapidUpload && !stream.IsForceStreamUpload() {
|
||||||
|
if newObj, err := y.RapidUpload(ctx, dstDir, stream, isFamily, overwrite); err == nil {
|
||||||
|
return newObj, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return y.OldUpload(ctx, dstDir, stream, up, isFamily, overwrite)
|
||||||
|
|
||||||
|
}
|
||||||
166
drivers/189_tv/help.go
Normal file
166
drivers/189_tv/help.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package _189_tv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func clientSuffix() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"clientType": AndroidTV,
|
||||||
|
"version": TvVersion,
|
||||||
|
"channelId": TvChannelId,
|
||||||
|
"clientSn": "unknown",
|
||||||
|
"model": "PJX110",
|
||||||
|
"osFamily": "Android",
|
||||||
|
"osVersion": "35",
|
||||||
|
"networkAccessMode": "WIFI",
|
||||||
|
"telecomsOperator": "46011",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionKeySignatureOfHmac HMAC签名
|
||||||
|
func SessionKeySignatureOfHmac(sessionSecret, sessionKey, operate, fullUrl, dateOfGmt string) string {
|
||||||
|
urlpath := regexp.MustCompile(`://[^/]+((/[^/\s?#]+)*)`).FindStringSubmatch(fullUrl)[1]
|
||||||
|
mac := hmac.New(sha1.New, []byte(sessionSecret))
|
||||||
|
data := fmt.Sprintf("SessionKey=%s&Operate=%s&RequestURI=%s&Date=%s", sessionKey, operate, urlpath, dateOfGmt)
|
||||||
|
mac.Write([]byte(data))
|
||||||
|
return strings.ToUpper(hex.EncodeToString(mac.Sum(nil)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppKeySignatureOfHmac HMAC签名
|
||||||
|
func AppKeySignatureOfHmac(sessionSecret, appKey, operate, fullUrl string, timestamp int64) string {
|
||||||
|
urlpath := regexp.MustCompile(`://[^/]+((/[^/\s?#]+)*)`).FindStringSubmatch(fullUrl)[1]
|
||||||
|
mac := hmac.New(sha1.New, []byte(sessionSecret))
|
||||||
|
data := fmt.Sprintf("AppKey=%s&Operate=%s&RequestURI=%s&Timestamp=%d", appKey, operate, urlpath, timestamp)
|
||||||
|
mac.Write([]byte(data))
|
||||||
|
return strings.ToUpper(hex.EncodeToString(mac.Sum(nil)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取http规范的时间
|
||||||
|
func getHttpDateStr() string {
|
||||||
|
return time.Now().UTC().Format(http.TimeFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间戳
|
||||||
|
func timestamp() int64 {
|
||||||
|
return time.Now().UTC().UnixNano() / 1e6
|
||||||
|
}
|
||||||
|
|
||||||
|
type Time time.Time
|
||||||
|
|
||||||
|
func (t *Time) UnmarshalJSON(b []byte) error { return t.Unmarshal(b) }
|
||||||
|
func (t *Time) UnmarshalXML(e *xml.Decoder, ee xml.StartElement) error {
|
||||||
|
b, err := e.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if b, ok := b.(xml.CharData); ok {
|
||||||
|
if err = t.Unmarshal(b); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.Skip()
|
||||||
|
}
|
||||||
|
func (t *Time) Unmarshal(b []byte) error {
|
||||||
|
bs := strings.Trim(string(b), "\"")
|
||||||
|
var v time.Time
|
||||||
|
var err error
|
||||||
|
for _, f := range []string{"2006-01-02 15:04:05 -07", "Jan 2, 2006 15:04:05 PM -07"} {
|
||||||
|
v, err = time.ParseInLocation(f, bs+" +08", time.Local)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*t = Time(v)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type String string
|
||||||
|
|
||||||
|
func (t *String) UnmarshalJSON(b []byte) error { return t.Unmarshal(b) }
|
||||||
|
func (t *String) UnmarshalXML(e *xml.Decoder, ee xml.StartElement) error {
|
||||||
|
b, err := e.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if b, ok := b.(xml.CharData); ok {
|
||||||
|
if err = t.Unmarshal(b); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.Skip()
|
||||||
|
}
|
||||||
|
func (s *String) Unmarshal(b []byte) error {
|
||||||
|
*s = String(bytes.Trim(b, "\""))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toFamilyOrderBy(o string) string {
|
||||||
|
switch o {
|
||||||
|
case "filename":
|
||||||
|
return "1"
|
||||||
|
case "filesize":
|
||||||
|
return "2"
|
||||||
|
case "lastOpTime":
|
||||||
|
return "3"
|
||||||
|
default:
|
||||||
|
return "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toDesc(o string) string {
|
||||||
|
switch o {
|
||||||
|
case "desc":
|
||||||
|
return "true"
|
||||||
|
case "asc":
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseHttpHeader(str string) map[string]string {
|
||||||
|
header := make(map[string]string)
|
||||||
|
for _, value := range strings.Split(str, "&") {
|
||||||
|
if k, v, found := strings.Cut(value, "="); found {
|
||||||
|
header[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustString(str string, err error) string {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
func BoolToNumber(b bool) int {
|
||||||
|
if b {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBool(bs ...bool) bool {
|
||||||
|
for _, b := range bs {
|
||||||
|
if b {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IF[V any](o bool, t V, f V) V {
|
||||||
|
if o {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
30
drivers/189_tv/meta.go
Normal file
30
drivers/189_tv/meta.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package _189_tv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/OpenListTeam/OpenList/internal/driver"
|
||||||
|
"github.com/OpenListTeam/OpenList/internal/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Addition struct {
|
||||||
|
driver.RootID
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
TempUuid string
|
||||||
|
OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
|
||||||
|
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||||
|
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
|
||||||
|
FamilyID string `json:"family_id"`
|
||||||
|
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||||
|
RapidUpload bool `json:"rapid_upload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = driver.Config{
|
||||||
|
Name: "189CloudTV",
|
||||||
|
DefaultRoot: "-11",
|
||||||
|
CheckStatus: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
op.RegisterDriver(func() driver.Driver {
|
||||||
|
return &Cloud189TV{}
|
||||||
|
})
|
||||||
|
}
|
||||||
318
drivers/189_tv/types.go
Normal file
318
drivers/189_tv/types.go
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
package _189_tv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/OpenListTeam/OpenList/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 居然有四种返回方式
|
||||||
|
type RespErr struct {
|
||||||
|
ResCode any `json:"res_code"` // int or string
|
||||||
|
ResMessage string `json:"res_message"`
|
||||||
|
|
||||||
|
Error_ string `json:"error"`
|
||||||
|
|
||||||
|
XMLName xml.Name `xml:"error"`
|
||||||
|
Code string `json:"code" xml:"code"`
|
||||||
|
Message string `json:"message" xml:"message"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
|
||||||
|
ErrorCode string `json:"errorCode"`
|
||||||
|
ErrorMsg string `json:"errorMsg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *RespErr) HasError() bool {
|
||||||
|
switch v := e.ResCode.(type) {
|
||||||
|
case int, int64, int32:
|
||||||
|
return v != 0
|
||||||
|
case string:
|
||||||
|
return e.ResCode != ""
|
||||||
|
}
|
||||||
|
return (e.Code != "" && e.Code != "SUCCESS") || e.ErrorCode != "" || e.Error_ != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *RespErr) Error() string {
|
||||||
|
switch v := e.ResCode.(type) {
|
||||||
|
case int, int64, int32:
|
||||||
|
if v != 0 {
|
||||||
|
return fmt.Sprintf("res_code: %d ,res_msg: %s", v, e.ResMessage)
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
if e.ResCode != "" {
|
||||||
|
return fmt.Sprintf("res_code: %s ,res_msg: %s", e.ResCode, e.ResMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Code != "" && e.Code != "SUCCESS" {
|
||||||
|
if e.Msg != "" {
|
||||||
|
return fmt.Sprintf("code: %s ,msg: %s", e.Code, e.Msg)
|
||||||
|
}
|
||||||
|
if e.Message != "" {
|
||||||
|
return fmt.Sprintf("code: %s ,msg: %s", e.Code, e.Message)
|
||||||
|
}
|
||||||
|
return "code: " + e.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.ErrorCode != "" {
|
||||||
|
return fmt.Sprintf("err_code: %s ,err_msg: %s", e.ErrorCode, e.ErrorMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Error_ != "" {
|
||||||
|
return fmt.Sprintf("error: %s ,message: %s", e.ErrorCode, e.Message)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新session返回
|
||||||
|
type UserSessionResp struct {
|
||||||
|
ResCode int `json:"res_code"`
|
||||||
|
ResMessage string `json:"res_message"`
|
||||||
|
|
||||||
|
LoginName string `json:"loginName"`
|
||||||
|
|
||||||
|
KeepAlive int `json:"keepAlive"`
|
||||||
|
GetFileDiffSpan int `json:"getFileDiffSpan"`
|
||||||
|
GetUserInfoSpan int `json:"getUserInfoSpan"`
|
||||||
|
|
||||||
|
// 个人云
|
||||||
|
SessionKey string `json:"sessionKey"`
|
||||||
|
SessionSecret string `json:"sessionSecret"`
|
||||||
|
// 家庭云
|
||||||
|
FamilySessionKey string `json:"familySessionKey"`
|
||||||
|
FamilySessionSecret string `json:"familySessionSecret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UuidInfoResp struct {
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type E189AccessTokenResp struct {
|
||||||
|
E189AccessToken string `json:"accessToken"`
|
||||||
|
ExpiresIn int64 `json:"expiresIn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录返回
|
||||||
|
type AppSessionResp struct {
|
||||||
|
UserSessionResp
|
||||||
|
|
||||||
|
IsSaveName string `json:"isSaveName"`
|
||||||
|
|
||||||
|
// 会话刷新Token
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
//Token刷新
|
||||||
|
RefreshToken string `json:"refreshToken"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 家庭云账户
|
||||||
|
type FamilyInfoListResp struct {
|
||||||
|
FamilyInfoResp []FamilyInfoResp `json:"familyInfoResp"`
|
||||||
|
}
|
||||||
|
type FamilyInfoResp struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
CreateTime string `json:"createTime"`
|
||||||
|
FamilyID int64 `json:"familyId"`
|
||||||
|
RemarkName string `json:"remarkName"`
|
||||||
|
Type int `json:"type"`
|
||||||
|
UseFlag int `json:"useFlag"`
|
||||||
|
UserRole int `json:"userRole"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*文件部分*/
|
||||||
|
// 文件
|
||||||
|
type Cloud189File struct {
|
||||||
|
ID String `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Md5 string `json:"md5"`
|
||||||
|
|
||||||
|
LastOpTime Time `json:"lastOpTime"`
|
||||||
|
CreateDate Time `json:"createDate"`
|
||||||
|
Icon struct {
|
||||||
|
//iconOption 5
|
||||||
|
SmallUrl string `json:"smallUrl"`
|
||||||
|
LargeUrl string `json:"largeUrl"`
|
||||||
|
|
||||||
|
// iconOption 10
|
||||||
|
Max600 string `json:"max600"`
|
||||||
|
MediumURL string `json:"mediumUrl"`
|
||||||
|
} `json:"icon"`
|
||||||
|
|
||||||
|
// Orientation int64 `json:"orientation"`
|
||||||
|
// FileCata int64 `json:"fileCata"`
|
||||||
|
// MediaType int `json:"mediaType"`
|
||||||
|
// Rev string `json:"rev"`
|
||||||
|
// StarLabel int64 `json:"starLabel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cloud189File) CreateTime() time.Time {
|
||||||
|
return time.Time(c.CreateDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cloud189File) GetHash() utils.HashInfo {
|
||||||
|
return utils.NewHashInfo(utils.MD5, c.Md5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cloud189File) GetSize() int64 { return c.Size }
|
||||||
|
func (c *Cloud189File) GetName() string { return c.Name }
|
||||||
|
func (c *Cloud189File) ModTime() time.Time { return time.Time(c.LastOpTime) }
|
||||||
|
func (c *Cloud189File) IsDir() bool { return false }
|
||||||
|
func (c *Cloud189File) GetID() string { return string(c.ID) }
|
||||||
|
func (c *Cloud189File) GetPath() string { return "" }
|
||||||
|
func (c *Cloud189File) Thumb() string { return c.Icon.SmallUrl }
|
||||||
|
|
||||||
|
// 文件夹
|
||||||
|
type Cloud189Folder struct {
|
||||||
|
ID String `json:"id"`
|
||||||
|
ParentID int64 `json:"parentId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
|
||||||
|
LastOpTime Time `json:"lastOpTime"`
|
||||||
|
CreateDate Time `json:"createDate"`
|
||||||
|
|
||||||
|
// FileListSize int64 `json:"fileListSize"`
|
||||||
|
// FileCount int64 `json:"fileCount"`
|
||||||
|
// FileCata int64 `json:"fileCata"`
|
||||||
|
// Rev string `json:"rev"`
|
||||||
|
// StarLabel int64 `json:"starLabel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cloud189Folder) CreateTime() time.Time {
|
||||||
|
return time.Time(c.CreateDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cloud189Folder) GetHash() utils.HashInfo {
|
||||||
|
return utils.HashInfo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cloud189Folder) GetSize() int64 { return 0 }
|
||||||
|
func (c *Cloud189Folder) GetName() string { return c.Name }
|
||||||
|
func (c *Cloud189Folder) ModTime() time.Time { return time.Time(c.LastOpTime) }
|
||||||
|
func (c *Cloud189Folder) IsDir() bool { return true }
|
||||||
|
func (c *Cloud189Folder) GetID() string { return string(c.ID) }
|
||||||
|
func (c *Cloud189Folder) GetPath() string { return "" }
|
||||||
|
|
||||||
|
type Cloud189FilesResp struct {
|
||||||
|
//ResCode int `json:"res_code"`
|
||||||
|
//ResMessage string `json:"res_message"`
|
||||||
|
FileListAO struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
FileList []Cloud189File `json:"fileList"`
|
||||||
|
FolderList []Cloud189Folder `json:"folderList"`
|
||||||
|
} `json:"fileListAO"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskInfo 任务信息
|
||||||
|
type BatchTaskInfo struct {
|
||||||
|
// FileId 文件ID
|
||||||
|
FileId string `json:"fileId"`
|
||||||
|
// FileName 文件名
|
||||||
|
FileName string `json:"fileName"`
|
||||||
|
// IsFolder 是否是文件夹,0-否,1-是
|
||||||
|
IsFolder int `json:"isFolder"`
|
||||||
|
// SrcParentId 文件所在父目录ID
|
||||||
|
SrcParentId string `json:"srcParentId,omitempty"`
|
||||||
|
|
||||||
|
/* 冲突管理 */
|
||||||
|
// 1 -> 跳过 2 -> 保留 3 -> 覆盖
|
||||||
|
DealWay int `json:"dealWay,omitempty"`
|
||||||
|
IsConflict int `json:"isConflict,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上传部分 */
|
||||||
|
type InitMultiUploadResp struct {
|
||||||
|
//Code string `json:"code"`
|
||||||
|
Data struct {
|
||||||
|
UploadType int `json:"uploadType"`
|
||||||
|
UploadHost string `json:"uploadHost"`
|
||||||
|
UploadFileID string `json:"uploadFileId"`
|
||||||
|
FileDataExists int `json:"fileDataExists"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
type UploadUrlsResp struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Data map[string]UploadUrlsData `json:"uploadUrls"`
|
||||||
|
}
|
||||||
|
type UploadUrlsData struct {
|
||||||
|
RequestURL string `json:"requestURL"`
|
||||||
|
RequestHeader string `json:"requestHeader"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 第二种上传方式 */
|
||||||
|
type CreateUploadFileResp struct {
|
||||||
|
// 上传文件请求ID
|
||||||
|
UploadFileId int64 `json:"uploadFileId"`
|
||||||
|
// 上传文件数据的URL路径
|
||||||
|
FileUploadUrl string `json:"fileUploadUrl"`
|
||||||
|
// 上传文件完成后确认路径
|
||||||
|
FileCommitUrl string `json:"fileCommitUrl"`
|
||||||
|
// 文件是否已存在云盘中,0-未存在,1-已存在
|
||||||
|
FileDataExists int `json:"fileDataExists"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetUploadFileStatusResp struct {
|
||||||
|
CreateUploadFileResp
|
||||||
|
|
||||||
|
// 已上传的大小
|
||||||
|
DataSize int64 `json:"dataSize"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GetUploadFileStatusResp) GetSize() int64 {
|
||||||
|
return r.DataSize + r.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommitMultiUploadFileResp struct {
|
||||||
|
File struct {
|
||||||
|
UserFileID String `json:"userFileId"`
|
||||||
|
FileName string `json:"fileName"`
|
||||||
|
FileSize int64 `json:"fileSize"`
|
||||||
|
FileMd5 string `json:"fileMd5"`
|
||||||
|
CreateDate Time `json:"createDate"`
|
||||||
|
} `json:"file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OldCommitUploadFileResp struct {
|
||||||
|
XMLName xml.Name `xml:"file"`
|
||||||
|
ID String `xml:"id"`
|
||||||
|
Name string `xml:"name"`
|
||||||
|
Size int64 `xml:"size"`
|
||||||
|
Md5 string `xml:"md5"`
|
||||||
|
CreateDate Time `xml:"createDate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *OldCommitUploadFileResp) toFile() *Cloud189File {
|
||||||
|
return &Cloud189File{
|
||||||
|
ID: f.ID,
|
||||||
|
Name: f.Name,
|
||||||
|
Size: f.Size,
|
||||||
|
Md5: f.Md5,
|
||||||
|
CreateDate: f.CreateDate,
|
||||||
|
LastOpTime: f.CreateDate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateBatchTaskResp struct {
|
||||||
|
TaskID string `json:"taskId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BatchTaskStateResp struct {
|
||||||
|
FailedCount int `json:"failedCount"`
|
||||||
|
Process int `json:"process"`
|
||||||
|
SkipCount int `json:"skipCount"`
|
||||||
|
SubTaskCount int `json:"subTaskCount"`
|
||||||
|
SuccessedCount int `json:"successedCount"`
|
||||||
|
SuccessedFileIDList []int64 `json:"successedFileIdList"`
|
||||||
|
TaskID string `json:"taskId"`
|
||||||
|
TaskStatus int `json:"taskStatus"` //1 初始化 2 存在冲突 3 执行中,4 完成
|
||||||
|
}
|
||||||
|
|
||||||
|
type BatchTaskConflictTaskInfoResp struct {
|
||||||
|
SessionKey string `json:"sessionKey"`
|
||||||
|
TargetFolderID int `json:"targetFolderId"`
|
||||||
|
TaskID string `json:"taskId"`
|
||||||
|
TaskInfos []BatchTaskInfo
|
||||||
|
TaskType int `json:"taskType"`
|
||||||
|
}
|
||||||
564
drivers/189_tv/utils.go
Normal file
564
drivers/189_tv/utils.go
Normal file
@@ -0,0 +1,564 @@
|
|||||||
|
package _189_tv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"github.com/skip2/go-qrcode"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/OpenListTeam/OpenList/drivers/base"
|
||||||
|
"github.com/OpenListTeam/OpenList/internal/driver"
|
||||||
|
"github.com/OpenListTeam/OpenList/internal/model"
|
||||||
|
"github.com/OpenListTeam/OpenList/internal/op"
|
||||||
|
"github.com/OpenListTeam/OpenList/pkg/utils"
|
||||||
|
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TVAppKey = "600100885"
|
||||||
|
TVAppSignatureSecre = "fe5734c74c2f96a38157f420b32dc995"
|
||||||
|
TvVersion = "6.5.5"
|
||||||
|
AndroidTV = "FAMILY_TV"
|
||||||
|
TvChannelId = "home02"
|
||||||
|
|
||||||
|
ApiUrl = "https://api.cloud.189.cn"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (y *Cloud189TV) SignatureHeader(url, method string, isFamily bool) map[string]string {
|
||||||
|
dateOfGmt := getHttpDateStr()
|
||||||
|
sessionKey := y.tokenInfo.SessionKey
|
||||||
|
sessionSecret := y.tokenInfo.SessionSecret
|
||||||
|
if isFamily {
|
||||||
|
sessionKey = y.tokenInfo.FamilySessionKey
|
||||||
|
sessionSecret = y.tokenInfo.FamilySessionSecret
|
||||||
|
}
|
||||||
|
|
||||||
|
header := map[string]string{
|
||||||
|
"Date": dateOfGmt,
|
||||||
|
"SessionKey": sessionKey,
|
||||||
|
"X-Request-ID": uuid.NewString(),
|
||||||
|
"Signature": SessionKeySignatureOfHmac(sessionSecret, sessionKey, method, url, dateOfGmt),
|
||||||
|
}
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) AppKeySignatureHeader(url, method string) map[string]string {
|
||||||
|
tempTime := timestamp()
|
||||||
|
header := map[string]string{
|
||||||
|
"Timestamp": strconv.FormatInt(tempTime, 10),
|
||||||
|
"X-Request-ID": uuid.NewString(),
|
||||||
|
"AppKey": TVAppKey,
|
||||||
|
"AppSignature": AppKeySignatureOfHmac(TVAppSignatureSecre, TVAppKey, method, url, tempTime),
|
||||||
|
}
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) request(url, method string, callback base.ReqCallback, params map[string]string, resp interface{}, isFamily ...bool) ([]byte, error) {
|
||||||
|
req := y.client.R().SetQueryParams(clientSuffix())
|
||||||
|
|
||||||
|
if params != nil {
|
||||||
|
req.SetQueryParams(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signature
|
||||||
|
req.SetHeaders(y.SignatureHeader(url, method, isBool(isFamily...)))
|
||||||
|
|
||||||
|
var erron RespErr
|
||||||
|
req.SetError(&erron)
|
||||||
|
|
||||||
|
if callback != nil {
|
||||||
|
callback(req)
|
||||||
|
}
|
||||||
|
if resp != nil {
|
||||||
|
req.SetResult(resp)
|
||||||
|
}
|
||||||
|
res, err := req.Execute(method, url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(res.String(), "userSessionBO is null") ||
|
||||||
|
strings.Contains(res.String(), "InvalidSessionKey") {
|
||||||
|
return nil, errors.New("session expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理错误
|
||||||
|
if erron.HasError() {
|
||||||
|
return nil, &erron
|
||||||
|
}
|
||||||
|
return res.Body(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) get(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {
|
||||||
|
return y.request(url, http.MethodGet, callback, nil, resp, isFamily...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) post(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {
|
||||||
|
return y.request(url, http.MethodPost, callback, nil, resp, isFamily...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) put(ctx context.Context, url string, headers map[string]string, sign bool, file io.Reader, isFamily bool) ([]byte, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := req.URL.Query()
|
||||||
|
for key, value := range clientSuffix() {
|
||||||
|
query.Add(key, value)
|
||||||
|
}
|
||||||
|
req.URL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
for key, value := range headers {
|
||||||
|
req.Header.Add(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sign {
|
||||||
|
for key, value := range y.SignatureHeader(url, http.MethodPut, isFamily) {
|
||||||
|
req.Header.Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := base.HttpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var erron RespErr
|
||||||
|
jsoniter.Unmarshal(body, &erron)
|
||||||
|
xml.Unmarshal(body, &erron)
|
||||||
|
if erron.HasError() {
|
||||||
|
return nil, &erron
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, errors.Errorf("put fail,err:%s", string(body))
|
||||||
|
}
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
func (y *Cloud189TV) getFiles(ctx context.Context, fileId string, isFamily bool) ([]model.Obj, error) {
|
||||||
|
fullUrl := ApiUrl
|
||||||
|
if isFamily {
|
||||||
|
fullUrl += "/family/file"
|
||||||
|
}
|
||||||
|
fullUrl += "/listFiles.action"
|
||||||
|
|
||||||
|
res := make([]model.Obj, 0, 130)
|
||||||
|
for pageNum := 1; ; pageNum++ {
|
||||||
|
var resp Cloud189FilesResp
|
||||||
|
_, err := y.get(fullUrl, func(r *resty.Request) {
|
||||||
|
r.SetContext(ctx)
|
||||||
|
r.SetQueryParams(map[string]string{
|
||||||
|
"folderId": fileId,
|
||||||
|
"fileType": "0",
|
||||||
|
"mediaAttr": "0",
|
||||||
|
"iconOption": "5",
|
||||||
|
"pageNum": fmt.Sprint(pageNum),
|
||||||
|
"pageSize": "130",
|
||||||
|
})
|
||||||
|
if isFamily {
|
||||||
|
r.SetQueryParams(map[string]string{
|
||||||
|
"familyId": y.FamilyID,
|
||||||
|
"orderBy": toFamilyOrderBy(y.OrderBy),
|
||||||
|
"descending": toDesc(y.OrderDirection),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
r.SetQueryParams(map[string]string{
|
||||||
|
"recursive": "0",
|
||||||
|
"orderBy": y.OrderBy,
|
||||||
|
"descending": toDesc(y.OrderDirection),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, &resp, isFamily)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 获取完毕跳出
|
||||||
|
if resp.FileListAO.Count == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(resp.FileListAO.FolderList); i++ {
|
||||||
|
res = append(res, &resp.FileListAO.FolderList[i])
|
||||||
|
}
|
||||||
|
for i := 0; i < len(resp.FileListAO.FileList); i++ {
|
||||||
|
res = append(res, &resp.FileListAO.FileList[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) login() (err error) {
|
||||||
|
req := y.client.R().SetQueryParams(clientSuffix())
|
||||||
|
var erron RespErr
|
||||||
|
var tokenInfo AppSessionResp
|
||||||
|
if y.Addition.AccessToken == "" {
|
||||||
|
if y.Addition.TempUuid == "" {
|
||||||
|
// 获取登录参数
|
||||||
|
var uuidInfo UuidInfoResp
|
||||||
|
req.SetResult(&uuidInfo).SetError(&erron)
|
||||||
|
// Signature
|
||||||
|
req.SetHeaders(y.AppKeySignatureHeader(ApiUrl+"/family/manage/getQrCodeUUID.action",
|
||||||
|
http.MethodGet))
|
||||||
|
_, err = req.Execute(http.MethodGet, ApiUrl+"/family/manage/getQrCodeUUID.action")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if erron.HasError() {
|
||||||
|
return &erron
|
||||||
|
}
|
||||||
|
|
||||||
|
if uuidInfo.Uuid == "" {
|
||||||
|
return errors.New("uuidInfo is empty")
|
||||||
|
}
|
||||||
|
y.Addition.TempUuid = uuidInfo.Uuid
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
|
|
||||||
|
// 展示二维码
|
||||||
|
qrTemplate := `<body>
|
||||||
|
<img src="data:image/jpeg;base64,%s"/>
|
||||||
|
<br>Or Click here: <a href="%s">%s</a>
|
||||||
|
</body>`
|
||||||
|
|
||||||
|
// Generate QR code
|
||||||
|
qrCode, err := qrcode.Encode(uuidInfo.Uuid, qrcode.Medium, 256)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate QR code: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode QR code to base64
|
||||||
|
qrCodeBase64 := base64.StdEncoding.EncodeToString(qrCode)
|
||||||
|
|
||||||
|
// Create the HTML page
|
||||||
|
qrPage := fmt.Sprintf(qrTemplate, qrCodeBase64, uuidInfo.Uuid, uuidInfo.Uuid)
|
||||||
|
return fmt.Errorf("need verify: \n%s", qrPage)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
var accessTokenResp E189AccessTokenResp
|
||||||
|
req.SetResult(&accessTokenResp).SetError(&erron)
|
||||||
|
// Signature
|
||||||
|
req.SetHeaders(y.AppKeySignatureHeader(ApiUrl+"/family/manage/qrcodeLoginResult.action",
|
||||||
|
http.MethodGet))
|
||||||
|
req.SetQueryParam("uuid", y.Addition.TempUuid)
|
||||||
|
_, err = req.Execute(http.MethodGet, ApiUrl+"/family/manage/qrcodeLoginResult.action")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if erron.HasError() {
|
||||||
|
return &erron
|
||||||
|
}
|
||||||
|
if accessTokenResp.E189AccessToken == "" {
|
||||||
|
return errors.New("E189AccessToken is empty")
|
||||||
|
}
|
||||||
|
y.Addition.AccessToken = accessTokenResp.E189AccessToken
|
||||||
|
y.Addition.TempUuid = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 获取SessionKey 和 SessionSecret
|
||||||
|
reqb := y.client.R().SetQueryParams(clientSuffix())
|
||||||
|
reqb.SetResult(&tokenInfo).SetError(&erron)
|
||||||
|
// Signature
|
||||||
|
reqb.SetHeaders(y.AppKeySignatureHeader(ApiUrl+"/family/manage/loginFamilyMerge.action",
|
||||||
|
http.MethodGet))
|
||||||
|
reqb.SetQueryParam("e189AccessToken", y.Addition.AccessToken)
|
||||||
|
_, err = reqb.Execute(http.MethodGet, ApiUrl+"/family/manage/loginFamilyMerge.action")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if erron.HasError() {
|
||||||
|
return &erron
|
||||||
|
}
|
||||||
|
|
||||||
|
y.tokenInfo = &tokenInfo
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, isFamily bool, overwrite bool) (model.Obj, error) {
|
||||||
|
fileMd5 := stream.GetHash().GetHash(utils.MD5)
|
||||||
|
if len(fileMd5) < utils.MD5.Width {
|
||||||
|
return nil, errors.New("invalid hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, stream.GetName(), fmt.Sprint(stream.GetSize()), isFamily)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if uploadInfo.FileDataExists != 1 {
|
||||||
|
return nil, errors.New("rapid upload fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
return y.OldUploadCommit(ctx, uploadInfo.FileCommitUrl, uploadInfo.UploadFileId, isFamily, overwrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 旧版本上传,家庭云不支持覆盖
|
||||||
|
func (y *Cloud189TV) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
|
||||||
|
tempFile, err := file.CacheFullInTempFile()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fileMd5, err := utils.HashFile(utils.MD5, tempFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建上传会话
|
||||||
|
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, file.GetName(), fmt.Sprint(file.GetSize()), isFamily)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 网盘中不存在该文件,开始上传
|
||||||
|
status := GetUploadFileStatusResp{CreateUploadFileResp: *uploadInfo}
|
||||||
|
for status.GetSize() < file.GetSize() && status.FileDataExists != 1 {
|
||||||
|
if utils.IsCanceled(ctx) {
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
header := map[string]string{
|
||||||
|
"ResumePolicy": "1",
|
||||||
|
"Expect": "100-continue",
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFamily {
|
||||||
|
header["FamilyId"] = fmt.Sprint(y.FamilyID)
|
||||||
|
header["UploadFileId"] = fmt.Sprint(status.UploadFileId)
|
||||||
|
} else {
|
||||||
|
header["Edrive-UploadFileId"] = fmt.Sprint(status.UploadFileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := y.put(ctx, status.FileUploadUrl, header, true, io.NopCloser(tempFile), isFamily)
|
||||||
|
if err, ok := err.(*RespErr); ok && err.Code != "InputStreamReadError" {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取断点状态
|
||||||
|
fullUrl := ApiUrl + "/getUploadFileStatus.action"
|
||||||
|
if y.isFamily() {
|
||||||
|
fullUrl = ApiUrl + "/family/file/getFamilyFileStatus.action"
|
||||||
|
}
|
||||||
|
_, err = y.get(fullUrl, func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx).SetQueryParams(map[string]string{
|
||||||
|
"uploadFileId": fmt.Sprint(status.UploadFileId),
|
||||||
|
"resumePolicy": "1",
|
||||||
|
})
|
||||||
|
if isFamily {
|
||||||
|
req.SetQueryParam("familyId", fmt.Sprint(y.FamilyID))
|
||||||
|
}
|
||||||
|
}, &status, isFamily)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := tempFile.Seek(status.GetSize(), io.SeekStart); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
up(float64(status.GetSize()) / float64(file.GetSize()) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
return y.OldUploadCommit(ctx, status.FileCommitUrl, status.UploadFileId, isFamily, overwrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建上传会话
|
||||||
|
func (y *Cloud189TV) OldUploadCreate(ctx context.Context, parentID string, fileMd5, fileName, fileSize string, isFamily bool) (*CreateUploadFileResp, error) {
|
||||||
|
var uploadInfo CreateUploadFileResp
|
||||||
|
|
||||||
|
fullUrl := ApiUrl + "/createUploadFile.action"
|
||||||
|
if isFamily {
|
||||||
|
fullUrl = ApiUrl + "/family/file/createFamilyFile.action"
|
||||||
|
}
|
||||||
|
_, err := y.post(fullUrl, func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx)
|
||||||
|
if isFamily {
|
||||||
|
req.SetQueryParams(map[string]string{
|
||||||
|
"familyId": y.FamilyID,
|
||||||
|
"parentId": parentID,
|
||||||
|
"fileMd5": fileMd5,
|
||||||
|
"fileName": fileName,
|
||||||
|
"fileSize": fileSize,
|
||||||
|
"resumePolicy": "1",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
req.SetFormData(map[string]string{
|
||||||
|
"parentFolderId": parentID,
|
||||||
|
"fileName": fileName,
|
||||||
|
"size": fileSize,
|
||||||
|
"md5": fileMd5,
|
||||||
|
"opertype": "3",
|
||||||
|
"flag": "1",
|
||||||
|
"resumePolicy": "1",
|
||||||
|
"isLog": "0",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, &uploadInfo, isFamily)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &uploadInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交上传文件
|
||||||
|
func (y *Cloud189TV) OldUploadCommit(ctx context.Context, fileCommitUrl string, uploadFileID int64, isFamily bool, overwrite bool) (model.Obj, error) {
|
||||||
|
var resp OldCommitUploadFileResp
|
||||||
|
_, err := y.post(fileCommitUrl, func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx)
|
||||||
|
if isFamily {
|
||||||
|
req.SetHeaders(map[string]string{
|
||||||
|
"ResumePolicy": "1",
|
||||||
|
"UploadFileId": fmt.Sprint(uploadFileID),
|
||||||
|
"FamilyId": fmt.Sprint(y.FamilyID),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
req.SetFormData(map[string]string{
|
||||||
|
"opertype": IF(overwrite, "3", "1"),
|
||||||
|
"resumePolicy": "1",
|
||||||
|
"uploadFileId": fmt.Sprint(uploadFileID),
|
||||||
|
"isLog": "0",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, &resp, isFamily)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.toFile(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) isFamily() bool {
|
||||||
|
return y.Type == "family"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) isLogin() bool {
|
||||||
|
if y.tokenInfo == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err := y.get(ApiUrl+"/getUserInfo.action", nil, nil)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取家庭云所有用户信息
|
||||||
|
func (y *Cloud189TV) getFamilyInfoList() ([]FamilyInfoResp, error) {
|
||||||
|
var resp FamilyInfoListResp
|
||||||
|
_, err := y.get(ApiUrl+"/family/manage/getFamilyList.action", nil, &resp, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.FamilyInfoResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抽取家庭云ID
|
||||||
|
func (y *Cloud189TV) getFamilyID() (string, error) {
|
||||||
|
infos, err := y.getFamilyInfoList()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(infos) == 0 {
|
||||||
|
return "", fmt.Errorf("cannot get automatically,please input family_id")
|
||||||
|
}
|
||||||
|
for _, info := range infos {
|
||||||
|
if strings.Contains(y.tokenInfo.LoginName, info.RemarkName) {
|
||||||
|
return fmt.Sprint(info.FamilyID), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprint(infos[0].FamilyID), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) CreateBatchTask(aType string, familyID string, targetFolderId string, other map[string]string, taskInfos ...BatchTaskInfo) (*CreateBatchTaskResp, error) {
|
||||||
|
var resp CreateBatchTaskResp
|
||||||
|
_, err := y.post(ApiUrl+"/batch/createBatchTask.action", func(req *resty.Request) {
|
||||||
|
req.SetFormData(map[string]string{
|
||||||
|
"type": aType,
|
||||||
|
"taskInfos": MustString(utils.Json.MarshalToString(taskInfos)),
|
||||||
|
})
|
||||||
|
if targetFolderId != "" {
|
||||||
|
req.SetFormData(map[string]string{"targetFolderId": targetFolderId})
|
||||||
|
}
|
||||||
|
if familyID != "" {
|
||||||
|
req.SetFormData(map[string]string{"familyId": familyID})
|
||||||
|
}
|
||||||
|
req.SetFormData(other)
|
||||||
|
}, &resp, familyID != "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测任务状态
|
||||||
|
func (y *Cloud189TV) CheckBatchTask(aType string, taskID string) (*BatchTaskStateResp, error) {
|
||||||
|
var resp BatchTaskStateResp
|
||||||
|
_, err := y.post(ApiUrl+"/batch/checkBatchTask.action", func(req *resty.Request) {
|
||||||
|
req.SetFormData(map[string]string{
|
||||||
|
"type": aType,
|
||||||
|
"taskId": taskID,
|
||||||
|
})
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取冲突的任务信息
|
||||||
|
func (y *Cloud189TV) GetConflictTaskInfo(aType string, taskID string) (*BatchTaskConflictTaskInfoResp, error) {
|
||||||
|
var resp BatchTaskConflictTaskInfoResp
|
||||||
|
_, err := y.post(ApiUrl+"/batch/getConflictTaskInfo.action", func(req *resty.Request) {
|
||||||
|
req.SetFormData(map[string]string{
|
||||||
|
"type": aType,
|
||||||
|
"taskId": taskID,
|
||||||
|
})
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理冲突
|
||||||
|
func (y *Cloud189TV) ManageBatchTask(aType string, taskID string, targetFolderId string, taskInfos ...BatchTaskInfo) error {
|
||||||
|
_, err := y.post(ApiUrl+"/batch/manageBatchTask.action", func(req *resty.Request) {
|
||||||
|
req.SetFormData(map[string]string{
|
||||||
|
"targetFolderId": targetFolderId,
|
||||||
|
"type": aType,
|
||||||
|
"taskId": taskID,
|
||||||
|
"taskInfos": MustString(utils.Json.MarshalToString(taskInfos)),
|
||||||
|
})
|
||||||
|
}, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrIsConflict = errors.New("there is a conflict with the target object")
|
||||||
|
|
||||||
|
// 等待任务完成
|
||||||
|
func (y *Cloud189TV) WaitBatchTask(aType string, taskID string, t time.Duration) error {
|
||||||
|
for {
|
||||||
|
state, err := y.CheckBatchTask(aType, taskID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch state.TaskStatus {
|
||||||
|
case 2:
|
||||||
|
return ErrIsConflict
|
||||||
|
case 4:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
time.Sleep(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
_ "github.com/OpenListTeam/OpenList/drivers/123_share"
|
_ "github.com/OpenListTeam/OpenList/drivers/123_share"
|
||||||
_ "github.com/OpenListTeam/OpenList/drivers/139"
|
_ "github.com/OpenListTeam/OpenList/drivers/139"
|
||||||
_ "github.com/OpenListTeam/OpenList/drivers/189"
|
_ "github.com/OpenListTeam/OpenList/drivers/189"
|
||||||
|
_ "github.com/OpenListTeam/OpenList/drivers/189_tv"
|
||||||
_ "github.com/OpenListTeam/OpenList/drivers/189pc"
|
_ "github.com/OpenListTeam/OpenList/drivers/189pc"
|
||||||
_ "github.com/OpenListTeam/OpenList/drivers/alias"
|
_ "github.com/OpenListTeam/OpenList/drivers/alias"
|
||||||
_ "github.com/OpenListTeam/OpenList/drivers/aliyundrive"
|
_ "github.com/OpenListTeam/OpenList/drivers/aliyundrive"
|
||||||
|
|||||||
Reference in New Issue
Block a user