package _139 import ( "context" "encoding/base64" "fmt" "io" "net/http" "strconv" "strings" "time" "github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/cron" log "github.com/sirupsen/logrus" ) type Yun139 struct { model.Storage Addition cron *cron.Cron Account string } func (d *Yun139) Config() driver.Config { return config } func (d *Yun139) GetAddition() driver.Additional { return &d.Addition } func (d *Yun139) Init(ctx context.Context) error { if d.Authorization == "" { return fmt.Errorf("authorization is empty") } d.cron = cron.NewCron(time.Hour * 24 * 7) d.cron.Do(func() { err := d.refreshToken() if err != nil { log.Errorf("%+v", err) } }) switch d.Addition.Type { case MetaPersonalNew: if len(d.Addition.RootFolderID) == 0 { d.RootFolderID = "/" } return nil case MetaPersonal: if len(d.Addition.RootFolderID) == 0 { d.RootFolderID = "root" } fallthrough case MetaFamily: decode, err := base64.StdEncoding.DecodeString(d.Authorization) if err != nil { return err } decodeStr := string(decode) splits := strings.Split(decodeStr, ":") if len(splits) < 2 { return fmt.Errorf("authorization is invalid, splits < 2") } d.Account = splits[1] _, err = d.post("/orchestration/personalCloud/user/v1.0/qryUserExternInfo", base.Json{ "qryUserExternInfoReq": base.Json{ "commonAccountInfo": base.Json{ "account": d.Account, "accountType": 1, }, }, }, nil) return err default: return errs.NotImplement } } func (d *Yun139) Drop(ctx context.Context) error { if d.cron != nil { d.cron.Stop() } return nil } func (d *Yun139) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { switch d.Addition.Type { case MetaPersonalNew: return d.personalGetFiles(dir.GetID()) case MetaPersonal: return d.getFiles(dir.GetID()) case MetaFamily: return d.familyGetFiles(dir.GetID()) default: return nil, errs.NotImplement } } func (d *Yun139) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { var url string var err error switch d.Addition.Type { case MetaPersonalNew: url, err = d.personalGetLink(file.GetID()) case MetaPersonal: fallthrough case MetaFamily: url, err = d.getLink(file.GetID()) default: return nil, errs.NotImplement } if err != nil { return nil, err } return &model.Link{URL: url}, nil } func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { var err error switch d.Addition.Type { case MetaPersonalNew: data := base.Json{ "parentFileId": parentDir.GetID(), "name": dirName, "description": "", "type": "folder", "fileRenameMode": "force_rename", } pathname := "/hcy/file/create" _, err = d.personalPost(pathname, data, nil) case MetaPersonal: data := base.Json{ "createCatalogExtReq": base.Json{ "parentCatalogID": parentDir.GetID(), "newCatalogName": dirName, "commonAccountInfo": base.Json{ "account": d.Account, "accountType": 1, }, }, } pathname := "/orchestration/personalCloud/catalog/v1.0/createCatalogExt" _, err = d.post(pathname, data, nil) case MetaFamily: data := base.Json{ "cloudID": d.CloudID, "commonAccountInfo": base.Json{ "account": d.Account, "accountType": 1, }, "docLibName": dirName, } pathname := "/orchestration/familyCloud/cloudCatalog/v1.0/createCloudDoc" _, err = d.post(pathname, data, nil) default: err = errs.NotImplement } return err } func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { switch d.Addition.Type { case MetaPersonalNew: data := base.Json{ "fileIds": []string{srcObj.GetID()}, "toParentFileId": dstDir.GetID(), } pathname := "/hcy/file/batchMove" _, err := d.personalPost(pathname, data, nil) if err != nil { return nil, err } return srcObj, nil case MetaPersonal: var contentInfoList []string var catalogInfoList []string if srcObj.IsDir() { catalogInfoList = append(catalogInfoList, srcObj.GetID()) } else { contentInfoList = append(contentInfoList, srcObj.GetID()) } data := base.Json{ "createBatchOprTaskReq": base.Json{ "taskType": 3, "actionType": "304", "taskInfo": base.Json{ "contentInfoList": contentInfoList, "catalogInfoList": catalogInfoList, "newCatalogID": dstDir.GetID(), }, "commonAccountInfo": base.Json{ "account": d.Account, "accountType": 1, }, }, } pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask" _, err := d.post(pathname, data, nil) if err != nil { return nil, err } return srcObj, nil default: return nil, errs.NotImplement } } func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) error { var err error switch d.Addition.Type { case MetaPersonalNew: data := base.Json{ "fileId": srcObj.GetID(), "name": newName, "description": "", } pathname := "/hcy/file/update" _, err = d.personalPost(pathname, data, nil) case MetaPersonal: var data base.Json var pathname string if srcObj.IsDir() { data = base.Json{ "catalogID": srcObj.GetID(), "catalogName": newName, "commonAccountInfo": base.Json{ "account": d.Account, "accountType": 1, }, } pathname = "/orchestration/personalCloud/catalog/v1.0/updateCatalogInfo" } else { data = base.Json{ "contentID": srcObj.GetID(), "contentName": newName, "commonAccountInfo": base.Json{ "account": d.Account, "accountType": 1, }, } pathname = "/orchestration/personalCloud/content/v1.0/updateContentInfo" } _, err = d.post(pathname, data, nil) default: err = errs.NotImplement } return err } func (d *Yun139) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { var err error switch d.Addition.Type { case MetaPersonalNew: data := base.Json{ "fileIds": []string{srcObj.GetID()}, "toParentFileId": dstDir.GetID(), } pathname := "/hcy/file/batchCopy" _, err := d.personalPost(pathname, data, nil) return err case MetaPersonal: var contentInfoList []string var catalogInfoList []string if srcObj.IsDir() { catalogInfoList = append(catalogInfoList, srcObj.GetID()) } else { contentInfoList = append(contentInfoList, srcObj.GetID()) } data := base.Json{ "createBatchOprTaskReq": base.Json{ "taskType": 3, "actionType": 309, "taskInfo": base.Json{ "contentInfoList": contentInfoList, "catalogInfoList": catalogInfoList, "newCatalogID": dstDir.GetID(), }, "commonAccountInfo": base.Json{ "account": d.Account, "accountType": 1, }, }, } pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask" _, err = d.post(pathname, data, nil) default: err = errs.NotImplement } return err } func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error { switch d.Addition.Type { case MetaPersonalNew: data := base.Json{ "fileIds": []string{obj.GetID()}, } pathname := "/hcy/recyclebin/batchTrash" _, err := d.personalPost(pathname, data, nil) return err case MetaPersonal: fallthrough case MetaFamily: var contentInfoList []string var catalogInfoList []string if obj.IsDir() { catalogInfoList = append(catalogInfoList, obj.GetID()) } else { contentInfoList = append(contentInfoList, obj.GetID()) } data := base.Json{ "createBatchOprTaskReq": base.Json{ "taskType": 2, "actionType": 201, "taskInfo": base.Json{ "newCatalogID": "", "contentInfoList": contentInfoList, "catalogInfoList": catalogInfoList, }, "commonAccountInfo": base.Json{ "account": d.Account, "accountType": 1, }, }, } pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask" if d.isFamily() { data = base.Json{ "catalogList": catalogInfoList, "contentList": contentInfoList, "commonAccountInfo": base.Json{ "account": d.Account, "accountType": 1, }, "sourceCatalogType": 1002, "taskType": 2, } pathname = "/orchestration/familyCloud/batchOprTask/v1.0/createBatchOprTask" } _, err := d.post(pathname, data, nil) return err default: return errs.NotImplement } } const ( _ = iota //ignore first value by assigning to blank identifier KB = 1 << (10 * iota) MB GB TB ) func getPartSize(size int64) int64 { // 网盘对于分片数量存在上限 if size/GB > 30 { return 512 * MB } return 100 * MB } func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { switch d.Addition.Type { case MetaPersonalNew: var err error fullHash := stream.GetHash().GetHash(utils.SHA256) if len(fullHash) <= 0 { tmpF, err := stream.CacheFullInTempFile() if err != nil { return err } fullHash, err = utils.HashFile(utils.SHA256, tmpF) if err != nil { return err } } // return errs.NotImplement data := base.Json{ "contentHash": fullHash, "contentHashAlgorithm": "SHA256", "contentType": "application/octet-stream", "parallelUpload": false, "partInfos": []base.Json{{ "parallelHashCtx": base.Json{ "partOffset": 0, }, "partNumber": 1, "partSize": stream.GetSize(), }}, "size": stream.GetSize(), "parentFileId": dstDir.GetID(), "name": stream.GetName(), "type": "file", "fileRenameMode": "auto_rename", } pathname := "/hcy/file/create" var resp PersonalUploadResp _, err = d.personalPost(pathname, data, &resp) if err != nil { return err } if resp.Data.Exist || resp.Data.RapidUpload { return nil } // Progress p := driver.NewProgress(stream.GetSize(), up) // Update Progress r := io.TeeReader(stream, p) req, err := http.NewRequest("PUT", resp.Data.PartInfos[0].UploadUrl, r) if err != nil { return err } req = req.WithContext(ctx) req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Length", fmt.Sprint(stream.GetSize())) req.Header.Set("Origin", "https://yun.139.com") req.Header.Set("Referer", "https://yun.139.com/") req.ContentLength = stream.GetSize() res, err := base.HttpClient.Do(req) if err != nil { return err } _ = res.Body.Close() log.Debugf("%+v", res) if res.StatusCode != http.StatusOK { return fmt.Errorf("unexpected status code: %d", res.StatusCode) } data = base.Json{ "contentHash": fullHash, "contentHashAlgorithm": "SHA256", "fileId": resp.Data.FileId, "uploadId": resp.Data.UploadId, } _, err = d.personalPost("/hcy/file/complete", data, nil) if err != nil { return err } return nil case MetaPersonal: fallthrough case MetaFamily: data := base.Json{ "manualRename": 2, "operation": 0, "fileCount": 1, "totalSize": 0, // 去除上传大小限制 "uploadContentList": []base.Json{{ "contentName": stream.GetName(), "contentSize": 0, // 去除上传大小限制 // "digest": "5a3231986ce7a6b46e408612d385bafa" }}, "parentCatalogID": dstDir.GetID(), "newCatalogName": "", "commonAccountInfo": base.Json{ "account": d.Account, "accountType": 1, }, } pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest" if d.isFamily() { // data = d.newJson(base.Json{ // "fileCount": 1, // "manualRename": 2, // "operation": 0, // "path": "", // "seqNo": "", // "totalSize": 0, // "uploadContentList": []base.Json{{ // "contentName": stream.GetName(), // "contentSize": 0, // // "digest": "5a3231986ce7a6b46e408612d385bafa" // }}, // }) // pathname = "/orchestration/familyCloud/content/v1.0/getFileUploadURL" return errs.NotImplement } var resp UploadResp _, err := d.post(pathname, data, &resp) if err != nil { return err } // Progress p := driver.NewProgress(stream.GetSize(), up) var partSize = getPartSize(stream.GetSize()) part := (stream.GetSize() + partSize - 1) / partSize if part == 0 { part = 1 } for i := int64(0); i < part; i++ { if utils.IsCanceled(ctx) { return ctx.Err() } start := i * partSize byteSize := stream.GetSize() - start if byteSize > partSize { byteSize = partSize } limitReader := io.LimitReader(stream, byteSize) // Update Progress r := io.TeeReader(limitReader, p) req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, r) if err != nil { return err } req = req.WithContext(ctx) req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName())) req.Header.Set("contentSize", strconv.FormatInt(stream.GetSize(), 10)) req.Header.Set("range", fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1)) req.Header.Set("uploadtaskID", resp.Data.UploadResult.UploadTaskID) req.Header.Set("rangeType", "0") req.ContentLength = byteSize res, err := base.HttpClient.Do(req) if err != nil { return err } _ = res.Body.Close() log.Debugf("%+v", res) if res.StatusCode != http.StatusOK { return fmt.Errorf("unexpected status code: %d", res.StatusCode) } } return nil default: return errs.NotImplement } } func (d *Yun139) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { switch d.Addition.Type { case MetaPersonalNew: var resp base.Json var uri string data := base.Json{ "category": "video", "fileId": args.Obj.GetID(), } switch args.Method { case "video_preview": uri = "/hcy/videoPreview/getPreviewInfo" default: return nil, errs.NotSupport } _, err := d.personalPost(uri, data, &resp) if err != nil { return nil, err } return resp["data"], nil default: return nil, errs.NotImplement } } var _ driver.Driver = (*Yun139)(nil)