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" ) // do others that not defined in Driver interface 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) //fmt.Println(string(res.Body())) if err != nil { return err } // if e.Ret != 200 { // return fmt.Errorf("failed to refresh token: %s", e.Reason) // } 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) //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 } 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) //log.Debugln(res.String()) 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 } } //fmt.Println(string(res.Body())) 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}, //Thumbnail: content.BigthumbnailURL, } 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) { // if strings.Contains(catalogID, "/") { // catalogID = "0" // } 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, } //https://homecloud.komect.com/front/storage/getFileInfoList/v1 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 // 创建 time.Time 对象 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 } }