From 036373032c7412d2e02dc7f8493f12d326805ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E5=87=89?= <927625802@qq.com> Date: Sun, 9 Jan 2022 22:44:43 +0800 Subject: [PATCH] :sparkles: 139yun personal support --- drivers/139/139.go | 168 ++++++++++++++++++ drivers/139/driver.go | 383 ++++++++++++++++++++++++++++++++++++++++++ drivers/139/types.go | 132 +++++++++++++++ drivers/139/util.go | 57 +++++++ drivers/all.go | 1 + 5 files changed, 741 insertions(+) create mode 100644 drivers/139/139.go create mode 100644 drivers/139/driver.go create mode 100644 drivers/139/types.go create mode 100644 drivers/139/util.go diff --git a/drivers/139/139.go b/drivers/139/139.go new file mode 100644 index 00000000..ced784d7 --- /dev/null +++ b/drivers/139/139.go @@ -0,0 +1,168 @@ +package _39 + +import ( + "errors" + "fmt" + "github.com/Xhofe/alist/conf" + "github.com/Xhofe/alist/drivers/base" + "github.com/Xhofe/alist/model" + "github.com/Xhofe/alist/utils" + "github.com/go-resty/resty/v2" + jsoniter "github.com/json-iterator/go" + "path" + "time" +) + +func (driver Cloud139) Request(pathname string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) { + url := "https://yun.139.com" + pathname + req := base.RestyClient.R() + randStr := randomStr(16) + ts := time.Now().Format("2006-01-02 15:04:05") + body, err := utils.Json.Marshal(data) + if err != nil { + return nil, err + } + sign := calSign(string(body), ts, randStr) + req.SetHeaders(map[string]string{ + "Accept": "application/json, text/plain, */*", + "CMS-DEVICE": "default", + "Cookie": account.AccessToken, + "mcloud-channel": "1000101", + "mcloud-client": "10701", + //"mcloud-route": "001", + "mcloud-sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign), + //"mcloud-skey":"", + "mcloud-version": "6.6.0", + "Origin": "https://yun.139.com", + "Referer": "https://yun.139.com/w/", + "x-DeviceInfo": "||9|6.6.0|chrome|95.0.4638.69|uwIy75obnsRPIwlJSd7D9GhUvFwG96ce||macos 10.15.2||zh-CN|||", + "x-huawei-channelSrc": "10000034", + "x-inner-ntwk": "2", + "x-m4c-caller": "PC", + "x-m4c-src": "10002", + "x-SvcType": "1", + }) + if headers != nil { + req.SetHeaders(headers) + } + if query != nil { + req.SetQueryParams(query) + } + if form != nil { + req.SetFormData(form) + } + if data != nil { + req.SetBody(data) + } + var e BaseResp + //var err error + var res *resty.Response + req.SetResult(&e) + switch method { + case base.Get: + res, err = req.Get(url) + case base.Post: + res, err = req.Post(url) + case base.Delete: + res, err = req.Delete(url) + case base.Patch: + res, err = req.Patch(url) + case base.Put: + res, err = req.Put(url) + default: + return nil, base.ErrNotSupport + } + if err != nil { + return nil, err + } + if !e.Success { + return nil, errors.New(e.Message) + } + if resp != nil { + err = utils.Json.Unmarshal(res.Body(), resp) + if err != nil { + return nil, err + } + } + return res.Body(), nil +} + +func (driver Cloud139) Post(pathname string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) { + return driver.Request(pathname, base.Post, nil, nil, nil, data, resp, account) +} + +func (driver Cloud139) GetFiles(catalogID string, account *model.Account) ([]model.File, error) { + start := 0 + limit := 100 + files := make([]model.File, 0) + for { + data := base.Json{ + "catalogID": catalogID, + "sortDirection": 1, + "startNumber": start + 1, + "endNumber": start + limit, + "filterType": 0, + "catalogSortType": 0, + "contentSortType": 0, + "commonAccountInfo": base.Json{ + "account": account.Username, + "accountType": 1, + }, + } + var resp GetDiskResp + _, err := driver.Post("/orchestration/personalCloud/catalog/v1.0/getDisk", data, &resp, account) + if err != nil { + return nil, err + } + for _, catalog := range resp.Data.GetDiskResult.CatalogList { + f := model.File{ + Id: catalog.CatalogID, + Name: catalog.CatalogName, + Size: 0, + Type: conf.FOLDER, + Driver: driver.Config().Name, + UpdatedAt: getTime(catalog.UpdateTime), + } + files = append(files, f) + } + for _, content := range resp.Data.GetDiskResult.ContentList { + f := model.File{ + Id: content.ContentID, + Name: content.ContentName, + Size: int64(content.ContentSize), + Type: utils.GetFileType(path.Ext(content.ContentName)), + Driver: driver.Config().Name, + UpdatedAt: getTime(content.UpdateTime), + Thumbnail: content.ThumbnailURL, + //Thumbnail: content.BigthumbnailURL, + } + files = append(files, f) + } + if start+limit >= resp.Data.GetDiskResult.NodeCount { + break + } + start += limit + } + return files, nil +} + +func (driver Cloud139) GetLink(contentId string, account *model.Account) (string, error) { + data := base.Json{ + "appName": "", + "contentID": contentId, + "commonAccountInfo": base.Json{ + "account": "18627147660", + "accountType": 1, + }, + } + res, err := driver.Post("/orchestration/personalCloud/uploadAndDownload/v1.0/downloadRequest", + data, nil, account) + if err != nil { + return "", err + } + return jsoniter.Get(res, "data", "downloadURL").ToString(), nil +} + +func init() { + base.RegisterDriver(&Cloud139{}) +} diff --git a/drivers/139/driver.go b/drivers/139/driver.go new file mode 100644 index 00000000..a9fd8168 --- /dev/null +++ b/drivers/139/driver.go @@ -0,0 +1,383 @@ +package _39 + +import ( + "bytes" + "fmt" + "github.com/Xhofe/alist/conf" + "github.com/Xhofe/alist/drivers/base" + "github.com/Xhofe/alist/model" + "github.com/Xhofe/alist/utils" + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + "io" + "math" + "net/http" + "path/filepath" + "strconv" +) + +type Cloud139 struct{} + +func (driver Cloud139) Config() base.DriverConfig { + return base.DriverConfig{ + Name: "139Yun", + LocalSort: true, + } +} + +func (driver Cloud139) Items() []base.Item { + return []base.Item{ + { + Name: "username", + Label: "phone", + Type: base.TypeString, + Required: true, + Description: "phone number", + }, + { + Name: "access_token", + Label: "Cookie", + Type: base.TypeString, + Required: true, + Description: "Unknown expiration time", + }, + { + Name: "internal_type", + Label: "139yun type", + Type: base.TypeSelect, + Required: true, + Values: "Personal,Family", + }, + { + Name: "root_folder", + Label: "root folder file_id", + Type: base.TypeString, + Required: true, + }, + } +} + +func (driver Cloud139) Save(account *model.Account, old *model.Account) error { + _, err := driver.Request("/orchestration/personalCloud/user/v1.0/qryUserExternInfo", base.Post, nil, nil, nil, base.Json{ + "qryUserExternInfoReq": base.Json{ + "commonAccountInfo": base.Json{ + "account": account.Username, + "accountType": 1, + }, + }, + }, nil, account) + return err +} + +func (driver Cloud139) File(path string, account *model.Account) (*model.File, error) { + path = utils.ParsePath(path) + if path == "/" { + return &model.File{ + Id: account.RootFolder, + Name: account.Name, + Size: 0, + Type: conf.FOLDER, + Driver: driver.Config().Name, + UpdatedAt: account.UpdatedAt, + }, nil + } + dir, name := filepath.Split(path) + files, err := driver.Files(dir, account) + if err != nil { + return nil, err + } + for _, file := range files { + if file.Name == name { + return &file, nil + } + } + return nil, base.ErrPathNotFound +} + +func (driver Cloud139) Files(path string, account *model.Account) ([]model.File, error) { + path = utils.ParsePath(path) + var files []model.File + cache, err := base.GetCache(path, account) + if err == nil { + files, _ = cache.([]model.File) + } else { + file, err := driver.File(path, account) + if err != nil { + return nil, err + } + files, err = driver.GetFiles(file.Id, account) + if err != nil { + return nil, err + } + if len(files) > 0 { + _ = base.SetCache(path, files, account) + } + } + return files, nil +} + +func (driver Cloud139) Link(args base.Args, account *model.Account) (*base.Link, error) { + file, err := driver.File(args.Path, account) + if err != nil { + return nil, err + } + u, err := driver.GetLink(file.Id, account) + if err != nil { + return nil, err + } + return &base.Link{Url: u}, nil +} + +func (driver Cloud139) Path(path string, account *model.Account) (*model.File, []model.File, error) { + path = utils.ParsePath(path) + log.Debugf("139 path: %s", path) + file, err := driver.File(path, account) + if err != nil { + return nil, nil, err + } + if !file.IsDir() { + return file, nil, nil + } + files, err := driver.Files(path, account) + if err != nil { + return nil, nil, err + } + return nil, files, nil +} + +func (driver Cloud139) Proxy(c *gin.Context, account *model.Account) { + +} + +func (driver Cloud139) Preview(path string, account *model.Account) (interface{}, error) { + return nil, base.ErrNotSupport +} + +func (driver Cloud139) MakeDir(path string, account *model.Account) error { + parentFile, err := driver.File(utils.Dir(path), account) + if err != nil { + return err + } + data := base.Json{ + "createCatalogExtReq": base.Json{ + "parentCatalogID": parentFile.Id, + "newCatalogName": utils.Base(path), + "commonAccountInfo": base.Json{ + "account": account.Username, + "accountType": 1, + }, + }, + } + pathname := "/orchestration/personalCloud/catalog/v1.0/createCatalogExt" + _, err = driver.Post(pathname, + data, nil, account) + return err +} + +func (driver Cloud139) Move(src string, dst string, account *model.Account) error { + srcFile, err := driver.File(src, account) + if err != nil { + return err + } + dstParentFile, err := driver.File(utils.Dir(dst), account) + if err != nil { + return err + } + var contentInfoList []string + var catalogInfoList []string + if srcFile.IsDir() { + catalogInfoList = append(catalogInfoList, srcFile.Id) + } else { + contentInfoList = append(contentInfoList, srcFile.Id) + } + data := base.Json{ + "createBatchOprTaskReq": base.Json{ + "taskType": 3, + "actionType": "304", + "taskInfo": base.Json{ + "contentInfoList": contentInfoList, + "catalogInfoList": catalogInfoList, + "newCatalogID": dstParentFile.Id, + }, + "commonAccountInfo": base.Json{ + "account": "18627147660", + "accountType": 1, + }, + }, + } + pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask" + _, err = driver.Post(pathname, data, nil, account) + return err +} + +func (driver Cloud139) Rename(src string, dst string, account *model.Account) error { + srcFile, err := driver.File(src, account) + if err != nil { + return err + } + var data base.Json + var pathname string + if srcFile.IsDir() { + data = base.Json{ + "catalogID": srcFile.Id, + "catalogName": utils.Base(dst), + "commonAccountInfo": base.Json{ + "account": "18627147660", + "accountType": 1, + }, + } + pathname = "/orchestration/personalCloud/catalog/v1.0/updateCatalogInfo" + } else { + data = base.Json{ + "contentID": srcFile.Id, + "contentName": utils.Base(dst), + "commonAccountInfo": base.Json{ + "account": "18627147660", + "accountType": 1, + }, + } + pathname = "/orchestration/personalCloud/catalog/v1.0/updateContentInfo" + } + _, err = driver.Post(pathname, data, nil, account) + return err +} + +func (driver Cloud139) Copy(src string, dst string, account *model.Account) error { + srcFile, err := driver.File(src, account) + if err != nil { + return err + } + dstParentFile, err := driver.File(utils.Dir(dst), account) + if err != nil { + return err + } + argName := "contentInfoList" + if srcFile.IsDir() { + argName = "catalogInfoList" + } + data := base.Json{ + "createBatchOprTaskReq": base.Json{ + "taskType": 3, + "actionType": 309, + "taskInfo": base.Json{ + "contentInfoList": []string{}, + "catalogInfoList": []string{}, + "newCatalogID": dstParentFile.Id, + argName: []string{srcFile.Id}, + }, + "commonAccountInfo": base.Json{ + "account": "18627147660", + "accountType": 1, + }, + }, + } + pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask" + _, err = driver.Post(pathname, data, nil, account) + return err +} + +func (driver Cloud139) Delete(path string, account *model.Account) error { + file, err := driver.File(path, account) + if err != nil { + return err + } + argName := "contentInfoList" + if file.IsDir() { + argName = "catalogInfoList" + } + data := base.Json{ + "createBatchOprTaskReq": base.Json{ + "taskType": 2, + "actionType": 201, + "taskInfo": base.Json{ + "newCatalogID": "", + argName: []string{file.Id}, + }, + "commonAccountInfo": base.Json{ + "account": "18627147660", + "accountType": 1, + }, + }, + } + pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask" + _, err = driver.Post(pathname, data, nil, account) + return err +} + +func (driver Cloud139) Upload(file *model.FileStream, account *model.Account) error { + if file == nil { + return base.ErrEmptyFile + } + parentFile, err := driver.File(file.ParentPath, account) + if err != nil { + return err + } + if !parentFile.IsDir() { + return base.ErrNotFolder + } + data := base.Json{ + "manualRename": 2, + "operation": 0, + "fileCount": 1, + "totalSize": file.Size, + "uploadContentList": []base.Json{{ + "contentName": file.Name, + "contentSize": file.Size, + // "digest": "5a3231986ce7a6b46e408612d385bafa" + }}, + "parentCatalogID": parentFile.Id, + "newCatalogName": "", + "commonAccountInfo": base.Json{ + "account": "18627147660", + "accountType": 1, + }, + } + pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest" + var resp UploadResp + _, err = driver.Post(pathname, data, &resp, account) + if err != nil { + return err + } + var Default uint64 = 10485760 + part := int(math.Ceil(float64(file.Size) / float64(Default))) + var start uint64 = 0 + for i := 0; i < part; i++ { + byteSize := file.Size - start + if byteSize > Default { + byteSize = Default + } + byteData := make([]byte, byteSize) + _, err = io.ReadFull(file, byteData) + if err != nil { + return err + } + req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, bytes.NewBuffer(byteData)) + if err != nil { + return err + } + headers := map[string]string{ + "Accept": "*/*", + "Content-Type": "text/plain;name=" + unicode(file.Name), + "contentSize": strconv.FormatUint(file.Size, 10), + "range": fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1), + "content-length": strconv.FormatUint(byteSize, 10), + "uploadtaskID": resp.Data.UploadResult.UploadTaskID, + "rangeType": "0", + "Referer": "https://yun.139.com/", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Edg/95.0.1020.44", + "x-SvcType": "1", + } + for k, v := range headers { + req.Header.Set(k, v) + } + res, err := base.HttpClient.Do(req) + if err != nil { + return err + } + log.Debugf("%+v", res) + start += byteSize + } + return nil +} + +var _ base.Driver = (*Cloud139)(nil) diff --git a/drivers/139/types.go b/drivers/139/types.go new file mode 100644 index 00000000..3abf3950 --- /dev/null +++ b/drivers/139/types.go @@ -0,0 +1,132 @@ +package _39 + +type BaseResp struct { + Success bool `json:"success"` + Code string `json:"code"` + Message string `json:"message"` +} + +type Catalog struct { + CatalogID string `json:"catalogID"` + CatalogName string `json:"catalogName"` + CatalogType int `json:"catalogType"` + CreateTime string `json:"createTime"` + UpdateTime string `json:"updateTime"` + IsShared bool `json:"isShared"` + CatalogLevel int `json:"catalogLevel"` + ShareDoneeCount int `json:"shareDoneeCount"` + OpenType int `json:"openType"` + ParentCatalogID string `json:"parentCatalogId"` + DirEtag int `json:"dirEtag"` + Tombstoned int `json:"tombstoned"` + ProxyID interface{} `json:"proxyID"` + Moved int `json:"moved"` + IsFixedDir int `json:"isFixedDir"` + IsSynced interface{} `json:"isSynced"` + Owner string `json:"owner"` + Modifier interface{} `json:"modifier"` + Path string `json:"path"` + ShareType int `json:"shareType"` + SoftLink interface{} `json:"softLink"` + ExtProp1 interface{} `json:"extProp1"` + ExtProp2 interface{} `json:"extProp2"` + ExtProp3 interface{} `json:"extProp3"` + ExtProp4 interface{} `json:"extProp4"` + ExtProp5 interface{} `json:"extProp5"` + ETagOprType int `json:"ETagOprType"` +} + +type Content struct { + ContentID string `json:"contentID"` + ContentName string `json:"contentName"` + ContentSuffix string `json:"contentSuffix"` + ContentSize int `json:"contentSize"` + ContentDesc string `json:"contentDesc"` + ContentType int `json:"contentType"` + ContentOrigin int `json:"contentOrigin"` + UpdateTime string `json:"updateTime"` + CommentCount int `json:"commentCount"` + ThumbnailURL string `json:"thumbnailURL"` + BigthumbnailURL string `json:"bigthumbnailURL"` + PresentURL string `json:"presentURL"` + PresentLURL string `json:"presentLURL"` + PresentHURL string `json:"presentHURL"` + ContentTAGList interface{} `json:"contentTAGList"` + ShareDoneeCount int `json:"shareDoneeCount"` + Safestate int `json:"safestate"` + Transferstate int `json:"transferstate"` + IsFocusContent int `json:"isFocusContent"` + UpdateShareTime interface{} `json:"updateShareTime"` + UploadTime string `json:"uploadTime"` + OpenType int `json:"openType"` + AuditResult int `json:"auditResult"` + ParentCatalogID string `json:"parentCatalogId"` + Channel string `json:"channel"` + GeoLocFlag string `json:"geoLocFlag"` + Digest string `json:"digest"` + Version string `json:"version"` + FileEtag string `json:"fileEtag"` + FileVersion string `json:"fileVersion"` + Tombstoned int `json:"tombstoned"` + ProxyID string `json:"proxyID"` + Moved int `json:"moved"` + MidthumbnailURL string `json:"midthumbnailURL"` + Owner string `json:"owner"` + Modifier string `json:"modifier"` + ShareType int `json:"shareType"` + ExtInfo struct { + Uploader string `json:"uploader"` + Address string `json:"address"` + } `json:"extInfo"` + Exif struct { + CreateTime string `json:"createTime"` + Longitude interface{} `json:"longitude"` + Latitude interface{} `json:"latitude"` + LocalSaveTime interface{} `json:"localSaveTime"` + } `json:"exif"` + CollectionFlag interface{} `json:"collectionFlag"` + TreeInfo interface{} `json:"treeInfo"` + IsShared bool `json:"isShared"` + ETagOprType int `json:"ETagOprType"` +} + +type GetDiskResp struct { + BaseResp + Data struct { + Result struct { + ResultCode string `json:"resultCode"` + ResultDesc interface{} `json:"resultDesc"` + } `json:"result"` + GetDiskResult struct { + ParentCatalogID string `json:"parentCatalogID"` + NodeCount int `json:"nodeCount"` + CatalogList []Catalog `json:"catalogList"` + ContentList []Content `json:"contentList"` + IsCompleted int `json:"isCompleted"` + } `json:"getDiskResult"` + } `json:"data"` +} + +type UploadResp struct { + BaseResp + Data struct { + Result struct { + ResultCode string `json:"resultCode"` + ResultDesc interface{} `json:"resultDesc"` + } `json:"result"` + UploadResult struct { + UploadTaskID string `json:"uploadTaskID"` + RedirectionURL string `json:"redirectionUrl"` + NewContentIDList []struct { + ContentID string `json:"contentID"` + ContentName string `json:"contentName"` + IsNeedUpload string `json:"isNeedUpload"` + FileEtag int64 `json:"fileEtag"` + FileVersion int64 `json:"fileVersion"` + OverridenFlag int `json:"overridenFlag"` + } `json:"newContentIDList"` + CatalogIDList interface{} `json:"catalogIDList"` + IsSlice interface{} `json:"isSlice"` + } `json:"uploadResult"` + } `json:"data"` +} diff --git a/drivers/139/util.go b/drivers/139/util.go new file mode 100644 index 00000000..83789d1b --- /dev/null +++ b/drivers/139/util.go @@ -0,0 +1,57 @@ +package _39 + +import ( + "encoding/base64" + "github.com/Xhofe/alist/model" + "github.com/Xhofe/alist/utils" + "math/rand" + "net/url" + "sort" + "strconv" + "strings" + "time" +) + +func randomStr(n int) string { + builder := strings.Builder{} + t := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + for i := 0; i < n; i++ { + r := rand.Intn(len(t)) + builder.WriteString(t[r : r+1]) + } + return builder.String() +} + +func encodeURIComponent(str string) string { + r := url.QueryEscape(str) + r = strings.Replace(r, "+", "%20", -1) + return r +} + +func calSign(body, ts, randStr string) string { + body = strings.ReplaceAll(body, "\n", "") + body = strings.ReplaceAll(body, " ", "") + body = encodeURIComponent(body) + strs := strings.Split(body, "") + sort.Strings(strs) + body = strings.Join(strs, "") + body = base64.StdEncoding.EncodeToString([]byte(body)) + res := utils.GetMD5Encode(body) + utils.GetMD5Encode(ts+":"+randStr) + res = strings.ToUpper(utils.GetMD5Encode(res)) + return res +} + +func getTime(t string) *time.Time { + stamp, _ := time.ParseInLocation("20060102150405", t, time.Local) + return &stamp +} + +func isFamily(account *model.Account) bool { + return account.InternalType == "Family" +} + +func unicode(str string) string { + textQuoted := strconv.QuoteToASCII(str) + textUnquoted := textQuoted[1 : len(textQuoted)-1] + return textUnquoted +} diff --git a/drivers/all.go b/drivers/all.go index ab351ef1..0910487c 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -2,6 +2,7 @@ package drivers import ( _ "github.com/Xhofe/alist/drivers/123" + _ "github.com/Xhofe/alist/drivers/139" _ "github.com/Xhofe/alist/drivers/189" _ "github.com/Xhofe/alist/drivers/alidrive" _ "github.com/Xhofe/alist/drivers/alist"