| package homecloud |
|
|
| import ( |
| "crypto/md5" |
| "crypto/sha1" |
| "encoding/base64" |
| "encoding/hex" |
| "errors" |
| "fmt" |
| "net/http" |
| "net/url" |
| "sort" |
| "strconv" |
| "strings" |
| "time" |
|
|
| "github.com/alist-org/alist/v3/drivers/base" |
| "github.com/alist-org/alist/v3/internal/model" |
| "github.com/alist-org/alist/v3/internal/op" |
| "github.com/alist-org/alist/v3/pkg/utils" |
| "github.com/alist-org/alist/v3/pkg/utils/random" |
| "github.com/go-resty/resty/v2" |
| jsoniter "github.com/json-iterator/go" |
| log "github.com/sirupsen/logrus" |
| ) |
|
|
| |
| func (d *HomeCloud) isFamily() bool { |
| return false |
| } |
|
|
| func encodeURIComponent(str string) string { |
| r := url.QueryEscape(str) |
| r = strings.Replace(r, "+", "%20", -1) |
| r = strings.Replace(r, "%21", "!", -1) |
| r = strings.Replace(r, "%27", "'", -1) |
| r = strings.Replace(r, "%28", "(", -1) |
| r = strings.Replace(r, "%29", ")", -1) |
| r = strings.Replace(r, "%2A", "*", -1) |
| return r |
| } |
|
|
| func calSign(body, ts, randStr string) string { |
| body = encodeURIComponent(body) |
| strs := strings.Split(body, "") |
| sort.Strings(strs) |
| body = strings.Join(strs, "") |
| body = base64.StdEncoding.EncodeToString([]byte(body)) |
| res := utils.GetMD5EncodeStr(body) + utils.GetMD5EncodeStr(ts+":"+randStr) |
| res = strings.ToUpper(utils.GetMD5EncodeStr(res)) |
| return res |
| } |
|
|
| func getTime(t string) time.Time { |
| stamp, _ := time.ParseInLocation("20060102150405", t, utils.CNLoc) |
| return stamp |
| } |
|
|
| func (d *HomeCloud) refreshToken() error { |
| pathname := "/auth/refreshToken/v2" |
| url := "https://homecloud.komect.com/front" + pathname |
|
|
| data := base.Json{ |
| "refresh_token": d.RefreshToken, |
| "scope": "sdk", |
| } |
|
|
| requestID := random.String(12) |
| body, err := utils.Json.Marshal(data) |
|
|
| if err != nil { |
| return err |
| } |
|
|
| timestamp := fmt.Sprintf("%.3f", float64(time.Now().UnixNano())/1e6) |
| h := sha1.New() |
| var sha1Hash string |
|
|
| if body == nil { |
| h.Write([]byte("{}")) |
| sha1Hash = strings.ToUpper(hex.EncodeToString(h.Sum(nil))) |
| } else { |
| h.Write(body) |
| sha1Hash = strings.ToUpper(hex.EncodeToString(h.Sum(nil))) |
| } |
|
|
| encStr := fmt.Sprintf("%s;%s;%s;Basic VTdUOU1xSHpVbklqeWdETzppQzZCU25QaExyODZGZmJX;%s", pathname, sha1Hash, requestID, timestamp) |
| signature := strings.ToUpper(fmt.Sprintf("%x", md5.Sum([]byte(encStr)))) |
|
|
| var resp RefreshTokenResp |
| var e FrontResp |
| req := base.RestyClient.R() |
| req.SetHeaders(map[string]string{ |
| "Accept": "application/json, text/plain, */*", |
| "Authorization": "Basic VTdUOU1xSHpVbklqeWdETzppQzZCU25QaExyODZGZmJX", |
| "Content-Type": "application/json", |
| "X-User-Agent": "Web|Chrome 127.0.0.0||OS X|homecloudWebDisk_1.1.1||yunpan 1.1.1|unknown", |
| "Timestamp": timestamp, |
| "Signature": signature, |
| "Request-Id": requestID, |
| "userId": "", |
| }) |
| req.SetBody(data) |
| req.SetResult(&resp) |
| req.SetError(&e) |
| _, err = req.Post(url) |
| |
|
|
| if err != nil { |
| return err |
| } |
| |
| |
| |
| if resp.Data.RefreshToken == "" { |
| return errors.New("failed to refresh token: refresh token is empty") |
| } |
| d.RefreshToken, d.AccessToken, d.UserID = resp.Data.RefreshToken, resp.Data.AccessToken, resp.Data.UserID |
| op.MustSaveDriverStorage(d) |
| return nil |
| } |
|
|
| func (d *HomeCloud) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { |
| url := "https://homecloud.komect.com/front" + pathname |
| req := base.RestyClient.R() |
| requestID := random.String(12) |
| |
| if callback != nil { |
| callback(req) |
| } |
| body, err := utils.Json.Marshal(req.Body) |
|
|
| if err != nil { |
| return nil, err |
| } |
|
|
| timestamp := fmt.Sprintf("%.3f", float64(time.Now().UnixNano())/1e6) |
| h := sha1.New() |
| var sha1Hash string |
|
|
| if body == nil { |
| h.Write([]byte("{}")) |
| sha1Hash = strings.ToUpper(hex.EncodeToString(h.Sum(nil))) |
| } else { |
| h.Write(body) |
| sha1Hash = strings.ToUpper(hex.EncodeToString(h.Sum(nil))) |
| } |
|
|
| encStr := fmt.Sprintf("%s;%s;%s;Bearer %s;%s", pathname, sha1Hash, requestID, d.AccessToken, timestamp) |
| signature := strings.ToUpper(fmt.Sprintf("%x", md5.Sum([]byte(encStr)))) |
|
|
| req.SetHeaders(map[string]string{ |
| "Accept": "application/json, text/plain, */*", |
| "Authorization": "Bearer " + d.AccessToken, |
| "Content-Type": "application/json", |
| "X-User-Agent": "Web|Chrome 127.0.0.0||OS X|homecloudWebDisk_1.1.1||yunpan 1.1.1|unknown", |
| "Timestamp": timestamp, |
| "Signature": signature, |
| "Request-Id": requestID, |
| "userId": d.UserID, |
| }) |
|
|
| var e FrontResp |
| req.SetResult(&e) |
| res, err := req.Execute(method, url) |
|
|
| |
| if e.Ret != 200 { |
| return nil, errors.New(e.Reason) |
| } |
| if resp != nil { |
| err = utils.Json.Unmarshal(res.Body(), resp) |
| if err != nil { |
| return nil, err |
| } |
| } |
| |
| return res.Body(), nil |
| } |
|
|
| func (d *HomeCloud) post(pathname string, data interface{}, resp interface{}) ([]byte, error) { |
| return d.request(pathname, http.MethodPost, func(req *resty.Request) { |
| req.SetBody(data) |
| }, resp) |
| } |
|
|
| func (d *HomeCloud) getFiles(catalogID string) ([]model.Obj, error) { |
| start := 0 |
| limit := 100 |
| files := make([]model.Obj, 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": d.Account, |
| "accountType": 1, |
| }, |
| } |
| var resp GetDiskResp |
| _, err := d.post("/orchestration/personalCloud/catalog/v1.0/getDisk", data, &resp) |
| if err != nil { |
| return nil, err |
| } |
| for _, catalog := range resp.Data.GetDiskResult.CatalogList { |
| f := model.Object{ |
| ID: catalog.CatalogID, |
| Name: catalog.CatalogName, |
| Size: 0, |
| Modified: getTime(catalog.UpdateTime), |
| Ctime: getTime(catalog.CreateTime), |
| IsFolder: true, |
| } |
| files = append(files, &f) |
| } |
| for _, content := range resp.Data.GetDiskResult.ContentList { |
| f := model.ObjThumb{ |
| Object: model.Object{ |
| ID: content.ContentID, |
| Name: content.ContentName, |
| Size: content.ContentSize, |
| Modified: getTime(content.UpdateTime), |
| HashInfo: utils.NewHashInfo(utils.MD5, content.Digest), |
| }, |
| Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL}, |
| |
| } |
| files = append(files, &f) |
| } |
| if start+limit >= resp.Data.GetDiskResult.NodeCount { |
| break |
| } |
| start += limit |
| } |
| return files, nil |
| } |
|
|
| func (d *HomeCloud) newJson(data map[string]interface{}) base.Json { |
| common := map[string]interface{}{} |
| return utils.MergeMap(data, common) |
| } |
|
|
| func (d *HomeCloud) familyGetFiles(catalogID string) ([]model.Obj, error) { |
|
|
| |
| |
| |
|
|
| pageNum := 1 |
| files := make([]model.Obj, 0) |
| for { |
| data := base.Json{ |
| "pageInfo": base.Json{ |
| "pageNum": pageNum, |
| "pageSize": 100, |
| }, |
| "sortInfo": base.Json{ |
| "sortField": 1, |
| "sortOrder": 2, |
| }, |
| "userId": d.UserID, |
| "groupId": d.GroupID, |
| "fileId": catalogID, |
| } |
|
|
| |
| var resp QueryContentListResp |
| _, err := d.post("/storage/getFileInfoList/v1", data, &resp) |
| if err != nil { |
| return nil, err |
| } |
| for _, content := range resp.Data.FileInfos { |
| filesize, err := strconv.ParseInt(content.Size, 10, 64) |
|
|
| if err != nil { |
| return nil, err |
| } |
|
|
| isfolder := false |
|
|
| if content.Type == 1 { |
| isfolder = true |
| } |
|
|
| ctimestamp, err := strconv.ParseInt(content.CreateTime, 10, 64) |
| if err != nil { |
| fmt.Println("Error parsing timestamp:", err) |
| return nil, err |
| } |
|
|
| mtimestamp, err := strconv.ParseInt(content.UpdateTime, 10, 64) |
| if err != nil { |
| fmt.Println("Error parsing timestamp:", err) |
| return nil, err |
| } |
|
|
| |
| cseconds := ctimestamp / 1000 |
| cnanoseconds := (ctimestamp % 1000) * 1000000 |
|
|
| |
| mseconds := mtimestamp / 1000 |
| mnanoseconds := (mtimestamp % 1000) * 1000000 |
|
|
| |
| ct := time.Unix(cseconds, cnanoseconds) |
| mt := time.Unix(mseconds, mnanoseconds) |
|
|
| f := model.ObjThumb{ |
| Object: model.Object{ |
| ID: content.ID, |
| Name: content.Name, |
| Size: filesize, |
| IsFolder: isfolder, |
| Modified: mt, |
| Ctime: ct, |
| }, |
| } |
| files = append(files, &f) |
| } |
|
|
| total_count, err := strconv.Atoi(resp.Data.Total) |
|
|
| if err != nil { |
| return nil, err |
| } |
|
|
| if 100*pageNum > total_count { |
| break |
| } |
| pageNum++ |
| } |
| return files, nil |
| } |
|
|
| func (d *HomeCloud) getLink(contentId string) (string, error) { |
| data := base.Json{ |
| "userId": d.UserID, |
| "groupId": d.GroupID, |
| "fileId": contentId, |
| } |
|
|
| res, err := d.post("/storage/getFileDownloadUrl/v1", |
| data, nil) |
| if err != nil { |
| return "", err |
| } |
| download_url := "https://cdn.homecloud.komect.com/gateway" + jsoniter.Get(res, "data", "downloadUrl").ToString() |
| return download_url, nil |
| } |
|
|
| func unicode(str string) string { |
| textQuoted := strconv.QuoteToASCII(str) |
| textUnquoted := textQuoted[1 : len(textQuoted)-1] |
| return textUnquoted |
| } |
|
|
| func (d *HomeCloud) personalRequest(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { |
| url := "https://personal-kd-njs.yun.139.com" + pathname |
| req := base.RestyClient.R() |
| randStr := random.String(16) |
| ts := time.Now().Format("2006-01-02 15:04:05") |
| if callback != nil { |
| callback(req) |
| } |
| body, err := utils.Json.Marshal(req.Body) |
| if err != nil { |
| return nil, err |
| } |
| sign := calSign(string(body), ts, randStr) |
| svcType := "1" |
| if d.isFamily() { |
| svcType = "2" |
| } |
| req.SetHeaders(map[string]string{ |
| "Accept": "application/json, text/plain, */*", |
| "Authorization": "Basic " + d.AccessToken, |
| "Caller": "web", |
| "Cms-Device": "default", |
| "Mcloud-Channel": "1000101", |
| "Mcloud-Client": "10701", |
| "Mcloud-Route": "001", |
| "Mcloud-Sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign), |
| "Mcloud-Version": "7.13.0", |
| "Origin": "https://yun.139.com", |
| "Referer": "https://yun.139.com/w/", |
| "x-DeviceInfo": "||9|7.13.0|chrome|120.0.0.0|||windows 10||zh-CN|||", |
| "x-huawei-channelSrc": "10000034", |
| "x-inner-ntwk": "2", |
| "x-m4c-caller": "PC", |
| "x-m4c-src": "10002", |
| "x-SvcType": svcType, |
| "X-Yun-Api-Version": "v1", |
| "X-Yun-App-Channel": "10000034", |
| "X-Yun-Channel-Source": "10000034", |
| "X-Yun-Client-Info": "||9|7.13.0|chrome|120.0.0.0|||windows 10||zh-CN|||dW5kZWZpbmVk||", |
| "X-Yun-Module-Type": "100", |
| "X-Yun-Svc-Type": "1", |
| }) |
|
|
| var e BaseResp |
| req.SetResult(&e) |
| res, err := req.Execute(method, url) |
| if err != nil { |
| return nil, err |
| } |
| log.Debugln(res.String()) |
| 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 (d *HomeCloud) personalPost(pathname string, data interface{}, resp interface{}) ([]byte, error) { |
| return d.personalRequest(pathname, http.MethodPost, func(req *resty.Request) { |
| req.SetBody(data) |
| }, resp) |
| } |
|
|
| func getPersonalTime(t string) time.Time { |
| stamp, err := time.ParseInLocation("2006-01-02T15:04:05.999-07:00", t, utils.CNLoc) |
| if err != nil { |
| panic(err) |
| } |
| return stamp |
| } |
|
|
| func (d *HomeCloud) personalGetFiles(fileId string) ([]model.Obj, error) { |
| files := make([]model.Obj, 0) |
| nextPageCursor := "" |
| for { |
| data := base.Json{ |
| "imageThumbnailStyleList": []string{"Small", "Large"}, |
| "orderBy": "updated_at", |
| "orderDirection": "DESC", |
| "pageInfo": base.Json{ |
| "pageCursor": nextPageCursor, |
| "pageSize": 100, |
| }, |
| "parentFileId": fileId, |
| } |
| var resp PersonalListResp |
| _, err := d.personalPost("/hcy/file/list", data, &resp) |
| if err != nil { |
| return nil, err |
| } |
| nextPageCursor = resp.Data.NextPageCursor |
| for _, item := range resp.Data.Items { |
| var isFolder = (item.Type == "folder") |
| var f model.Obj |
| if isFolder { |
| f = &model.Object{ |
| ID: item.FileId, |
| Name: item.Name, |
| Size: 0, |
| Modified: getPersonalTime(item.UpdatedAt), |
| Ctime: getPersonalTime(item.CreatedAt), |
| IsFolder: isFolder, |
| } |
| } else { |
| var Thumbnails = item.Thumbnails |
| var ThumbnailUrl string |
| if len(Thumbnails) > 0 { |
| ThumbnailUrl = Thumbnails[len(Thumbnails)-1].Url |
| } |
| f = &model.ObjThumb{ |
| Object: model.Object{ |
| ID: item.FileId, |
| Name: item.Name, |
| Size: item.Size, |
| Modified: getPersonalTime(item.UpdatedAt), |
| Ctime: getPersonalTime(item.CreatedAt), |
| IsFolder: isFolder, |
| }, |
| Thumbnail: model.Thumbnail{Thumbnail: ThumbnailUrl}, |
| } |
| } |
| files = append(files, f) |
| } |
| if len(nextPageCursor) == 0 { |
| break |
| } |
| } |
| return files, nil |
| } |
|
|
| func (d *HomeCloud) personalGetLink(fileId string) (string, error) { |
| data := base.Json{ |
| "fileId": fileId, |
| } |
| res, err := d.personalPost("/hcy/file/getDownloadUrl", |
| data, nil) |
| if err != nil { |
| return "", err |
| } |
| var cdnUrl = jsoniter.Get(res, "data", "cdnUrl").ToString() |
| if cdnUrl != "" { |
| return cdnUrl, nil |
| } else { |
| return jsoniter.Get(res, "data", "url").ToString(), nil |
| } |
| } |
|
|