From 34a2eeb4a9da4a02a681677e0c2ac5dc36387f34 Mon Sep 17 00:00:00 2001 From: Ray Date: Sun, 22 Jun 2025 18:48:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=9B=B2=E5=A5=87=E4=BA=91?= =?UTF-8?q?=E7=9B=98=E9=A9=B1=E5=8A=A8=20(#294)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update all.go 删除quqi Signed-off-by: Ray * Delete drivers/quqi directory删除quqi驱动 Signed-off-by: Ray --------- Signed-off-by: Ray --- drivers/all.go | 1 - drivers/quqi/driver.go | 452 ----------------------------------------- drivers/quqi/meta.go | 28 --- drivers/quqi/types.go | 197 ------------------ drivers/quqi/util.go | 299 --------------------------- 5 files changed, 977 deletions(-) delete mode 100644 drivers/quqi/driver.go delete mode 100644 drivers/quqi/meta.go delete mode 100644 drivers/quqi/types.go delete mode 100644 drivers/quqi/util.go diff --git a/drivers/all.go b/drivers/all.go index 42bea73e..cc0b5c5d 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -52,7 +52,6 @@ import ( _ "github.com/OpenListTeam/OpenList/drivers/pikpak_share" _ "github.com/OpenListTeam/OpenList/drivers/quark_uc" _ "github.com/OpenListTeam/OpenList/drivers/quark_uc_tv" - _ "github.com/OpenListTeam/OpenList/drivers/quqi" _ "github.com/OpenListTeam/OpenList/drivers/s3" _ "github.com/OpenListTeam/OpenList/drivers/seafile" _ "github.com/OpenListTeam/OpenList/drivers/sftp" diff --git a/drivers/quqi/driver.go b/drivers/quqi/driver.go deleted file mode 100644 index b069812a..00000000 --- a/drivers/quqi/driver.go +++ /dev/null @@ -1,452 +0,0 @@ -package quqi - -import ( - "bytes" - "context" - "errors" - "io" - "strconv" - "strings" - "time" - - "github.com/OpenListTeam/OpenList/internal/driver" - "github.com/OpenListTeam/OpenList/internal/errs" - "github.com/OpenListTeam/OpenList/internal/model" - "github.com/OpenListTeam/OpenList/pkg/utils" - "github.com/OpenListTeam/OpenList/pkg/utils/random" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/s3" - "github.com/aws/aws-sdk-go/service/s3/s3manager" - "github.com/go-resty/resty/v2" - log "github.com/sirupsen/logrus" -) - -type Quqi struct { - model.Storage - Addition - Cookie string // Cookie - GroupID string // 私人云群组ID - ClientID string // 随机生成客户端ID 经过测试,部分接口调用若不携带client id会出现错误 -} - -func (d *Quqi) Config() driver.Config { - return config -} - -func (d *Quqi) GetAddition() driver.Additional { - return &d.Addition -} - -func (d *Quqi) Init(ctx context.Context) error { - // 登录 - if err := d.login(); err != nil { - return err - } - - // 生成随机client id (与网页端生成逻辑一致) - d.ClientID = "quqipc_" + random.String(10) - - // 获取私人云ID (暂时仅获取私人云) - groupResp := &GroupRes{} - if _, err := d.request("group.quqi.com", "/v1/group/list", resty.MethodGet, nil, groupResp); err != nil { - return err - } - for _, groupInfo := range groupResp.Data { - if groupInfo == nil { - continue - } - if groupInfo.Type == 2 { - d.GroupID = strconv.Itoa(groupInfo.ID) - break - } - } - if d.GroupID == "" { - return errs.StorageNotFound - } - - return nil -} - -func (d *Quqi) Drop(ctx context.Context) error { - return nil -} - -func (d *Quqi) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { - var ( - listResp = &ListRes{} - files []model.Obj - ) - - if _, err := d.request("", "/api/dir/ls", resty.MethodPost, func(req *resty.Request) { - req.SetFormData(map[string]string{ - "quqi_id": d.GroupID, - "tree_id": "1", - "node_id": dir.GetID(), - "client_id": d.ClientID, - }) - }, listResp); err != nil { - return nil, err - } - - if listResp.Data == nil { - return nil, nil - } - - // dirs - for _, dirInfo := range listResp.Data.Dir { - if dirInfo == nil { - continue - } - files = append(files, &model.Object{ - ID: strconv.FormatInt(dirInfo.NodeID, 10), - Name: dirInfo.Name, - Modified: time.Unix(dirInfo.UpdateTime, 0), - Ctime: time.Unix(dirInfo.AddTime, 0), - IsFolder: true, - }) - } - - // files - for _, fileInfo := range listResp.Data.File { - if fileInfo == nil { - continue - } - if fileInfo.EXT != "" { - fileInfo.Name = strings.Join([]string{fileInfo.Name, fileInfo.EXT}, ".") - } - - files = append(files, &model.Object{ - ID: strconv.FormatInt(fileInfo.NodeID, 10), - Name: fileInfo.Name, - Size: fileInfo.Size, - Modified: time.Unix(fileInfo.UpdateTime, 0), - Ctime: time.Unix(fileInfo.AddTime, 0), - }) - } - - return files, nil -} - -func (d *Quqi) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { - if d.CDN { - link, err := d.linkFromCDN(file.GetID()) - if err != nil { - log.Warn(err) - } else { - return link, nil - } - } - - link, err := d.linkFromPreview(file.GetID()) - if err != nil { - log.Warn(err) - } else { - return link, nil - } - - link, err = d.linkFromDownload(file.GetID()) - if err != nil { - return nil, err - } - return link, nil -} - -func (d *Quqi) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) { - var ( - makeDirRes = &MakeDirRes{} - timeNow = time.Now() - ) - - if _, err := d.request("", "/api/dir/mkDir", resty.MethodPost, func(req *resty.Request) { - req.SetFormData(map[string]string{ - "quqi_id": d.GroupID, - "tree_id": "1", - "parent_id": parentDir.GetID(), - "name": dirName, - "client_id": d.ClientID, - }) - }, makeDirRes); err != nil { - return nil, err - } - - return &model.Object{ - ID: strconv.FormatInt(makeDirRes.Data.NodeID, 10), - Name: dirName, - Modified: timeNow, - Ctime: timeNow, - IsFolder: true, - }, nil -} - -func (d *Quqi) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - var moveRes = &MoveRes{} - - if _, err := d.request("", "/api/dir/mvDir", resty.MethodPost, func(req *resty.Request) { - req.SetFormData(map[string]string{ - "quqi_id": d.GroupID, - "tree_id": "1", - "node_id": dstDir.GetID(), - "source_quqi_id": d.GroupID, - "source_tree_id": "1", - "source_node_id": srcObj.GetID(), - "client_id": d.ClientID, - }) - }, moveRes); err != nil { - return nil, err - } - - return &model.Object{ - ID: strconv.FormatInt(moveRes.Data.NodeID, 10), - Name: moveRes.Data.NodeName, - Size: srcObj.GetSize(), - Modified: time.Now(), - Ctime: srcObj.CreateTime(), - IsFolder: srcObj.IsDir(), - }, nil -} - -func (d *Quqi) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) { - var realName = newName - - if !srcObj.IsDir() { - srcExt, newExt := utils.Ext(srcObj.GetName()), utils.Ext(newName) - - // 曲奇网盘的文件名称由文件名和扩展名组成,若存在扩展名,则重命名时仅支持更改文件名,扩展名在曲奇服务端保留 - if srcExt != "" && srcExt == newExt { - parts := strings.Split(newName, ".") - if len(parts) > 1 { - realName = strings.Join(parts[:len(parts)-1], ".") - } - } - } - - if _, err := d.request("", "/api/dir/renameDir", resty.MethodPost, func(req *resty.Request) { - req.SetFormData(map[string]string{ - "quqi_id": d.GroupID, - "tree_id": "1", - "node_id": srcObj.GetID(), - "rename": realName, - "client_id": d.ClientID, - }) - }, nil); err != nil { - return nil, err - } - - return &model.Object{ - ID: srcObj.GetID(), - Name: newName, - Size: srcObj.GetSize(), - Modified: time.Now(), - Ctime: srcObj.CreateTime(), - IsFolder: srcObj.IsDir(), - }, nil -} - -func (d *Quqi) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - // 无法从曲奇接口响应中直接获取复制后的文件信息 - if _, err := d.request("", "/api/node/copy", resty.MethodPost, func(req *resty.Request) { - req.SetFormData(map[string]string{ - "quqi_id": d.GroupID, - "tree_id": "1", - "node_id": dstDir.GetID(), - "source_quqi_id": d.GroupID, - "source_tree_id": "1", - "source_node_id": srcObj.GetID(), - "client_id": d.ClientID, - }) - }, nil); err != nil { - return nil, err - } - - return nil, nil -} - -func (d *Quqi) Remove(ctx context.Context, obj model.Obj) error { - // 暂时不做直接删除,默认都放到回收站。直接删除方法:先调用删除接口放入回收站,在通过回收站接口删除文件 - if _, err := d.request("", "/api/node/del", resty.MethodPost, func(req *resty.Request) { - req.SetFormData(map[string]string{ - "quqi_id": d.GroupID, - "tree_id": "1", - "node_id": obj.GetID(), - "client_id": d.ClientID, - }) - }, nil); err != nil { - return err - } - - return nil -} - -func (d *Quqi) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { - // base info - sizeStr := strconv.FormatInt(stream.GetSize(), 10) - f, err := stream.CacheFullInTempFile() - if err != nil { - return nil, err - } - md5, err := utils.HashFile(utils.MD5, f) - if err != nil { - return nil, err - } - sha, err := utils.HashFile(utils.SHA256, f) - if err != nil { - return nil, err - } - // init upload - var uploadInitResp UploadInitResp - _, err = d.request("", "/api/upload/v1/file/init", resty.MethodPost, func(req *resty.Request) { - req.SetFormData(map[string]string{ - "quqi_id": d.GroupID, - "tree_id": "1", - "parent_id": dstDir.GetID(), - "size": sizeStr, - "file_name": stream.GetName(), - "md5": md5, - "sha": sha, - "is_slice": "true", - "client_id": d.ClientID, - }) - }, &uploadInitResp) - if err != nil { - return nil, err - } - // check exist - // if the file already exists in Quqi server, there is no need to actually upload it - if uploadInitResp.Data.Exist { - // the file name returned by Quqi does not include the extension name - nodeName, nodeExt := uploadInitResp.Data.NodeName, utils.Ext(stream.GetName()) - if nodeExt != "" { - nodeName = nodeName + "." + nodeExt - } - return &model.Object{ - ID: strconv.FormatInt(uploadInitResp.Data.NodeID, 10), - Name: nodeName, - Size: stream.GetSize(), - Modified: stream.ModTime(), - Ctime: stream.CreateTime(), - }, nil - } - // listParts - _, err = d.request("upload.quqi.com:20807", "/upload/v1/listParts", resty.MethodPost, func(req *resty.Request) { - req.SetFormData(map[string]string{ - "token": uploadInitResp.Data.Token, - "task_id": uploadInitResp.Data.TaskID, - "client_id": d.ClientID, - }) - }, nil) - if err != nil { - return nil, err - } - // get temp key - var tempKeyResp TempKeyResp - _, err = d.request("upload.quqi.com:20807", "/upload/v1/tempKey", resty.MethodGet, func(req *resty.Request) { - req.SetQueryParams(map[string]string{ - "token": uploadInitResp.Data.Token, - "task_id": uploadInitResp.Data.TaskID, - }) - }, &tempKeyResp) - if err != nil { - return nil, err - } - // upload - // u, err := url.Parse(fmt.Sprintf("https://%s.cos.ap-shanghai.myqcloud.com", uploadInitResp.Data.Bucket)) - // b := &cos.BaseURL{BucketURL: u} - // client := cos.NewClient(b, &http.Client{ - // Transport: &cos.CredentialTransport{ - // Credential: cos.NewTokenCredential(tempKeyResp.Data.Credentials.TmpSecretID, tempKeyResp.Data.Credentials.TmpSecretKey, tempKeyResp.Data.Credentials.SessionToken), - // }, - // }) - // partSize := int64(1024 * 1024 * 2) - // partCount := (stream.GetSize() + partSize - 1) / partSize - // for i := 1; i <= int(partCount); i++ { - // length := partSize - // if i == int(partCount) { - // length = stream.GetSize() - (int64(i)-1)*partSize - // } - // _, err := client.Object.UploadPart( - // ctx, uploadInitResp.Data.Key, uploadInitResp.Data.UploadID, i, io.LimitReader(f, partSize), &cos.ObjectUploadPartOptions{ - // ContentLength: length, - // }, - // ) - // if err != nil { - // return nil, err - // } - // } - - cfg := &aws.Config{ - Credentials: credentials.NewStaticCredentials(tempKeyResp.Data.Credentials.TmpSecretID, tempKeyResp.Data.Credentials.TmpSecretKey, tempKeyResp.Data.Credentials.SessionToken), - Region: aws.String("ap-shanghai"), - Endpoint: aws.String("cos.ap-shanghai.myqcloud.com"), - } - s, err := session.NewSession(cfg) - if err != nil { - return nil, err - } - uploader := s3manager.NewUploader(s) - buf := make([]byte, 1024*1024*2) - fup := &driver.ReaderUpdatingProgress{ - Reader: &driver.SimpleReaderWithSize{ - Reader: f, - Size: int64(len(buf)), - }, - UpdateProgress: up, - } - for partNumber := int64(1); ; partNumber++ { - n, err := io.ReadFull(fup, buf) - if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) { - if err == io.EOF { - break - } - return nil, err - } - reader := bytes.NewReader(buf[:n]) - _, err = uploader.S3.UploadPartWithContext(ctx, &s3.UploadPartInput{ - UploadId: &uploadInitResp.Data.UploadID, - Key: &uploadInitResp.Data.Key, - Bucket: &uploadInitResp.Data.Bucket, - PartNumber: aws.Int64(partNumber), - Body: struct { - *driver.RateLimitReader - io.Seeker - }{ - RateLimitReader: driver.NewLimitedUploadStream(ctx, reader), - Seeker: reader, - }, - }) - if err != nil { - return nil, err - } - } - // finish upload - var uploadFinishResp UploadFinishResp - _, err = d.request("", "/api/upload/v1/file/finish", resty.MethodPost, func(req *resty.Request) { - req.SetFormData(map[string]string{ - "token": uploadInitResp.Data.Token, - "task_id": uploadInitResp.Data.TaskID, - "client_id": d.ClientID, - }) - }, &uploadFinishResp) - if err != nil { - return nil, err - } - // the file name returned by Quqi does not include the extension name - nodeName, nodeExt := uploadFinishResp.Data.NodeName, utils.Ext(stream.GetName()) - if nodeExt != "" { - nodeName = nodeName + "." + nodeExt - } - return &model.Object{ - ID: strconv.FormatInt(uploadFinishResp.Data.NodeID, 10), - Name: nodeName, - Size: stream.GetSize(), - Modified: stream.ModTime(), - Ctime: stream.CreateTime(), - }, nil -} - -//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { -// return nil, errs.NotSupport -//} - -var _ driver.Driver = (*Quqi)(nil) diff --git a/drivers/quqi/meta.go b/drivers/quqi/meta.go deleted file mode 100644 index 311284d4..00000000 --- a/drivers/quqi/meta.go +++ /dev/null @@ -1,28 +0,0 @@ -package quqi - -import ( - "github.com/OpenListTeam/OpenList/internal/driver" - "github.com/OpenListTeam/OpenList/internal/op" -) - -type Addition struct { - driver.RootID - Phone string `json:"phone"` - Password string `json:"password"` - Cookie string `json:"cookie" help:"Cookie can be used on multiple clients at the same time"` - CDN bool `json:"cdn" help:"If you enable this option, the download speed can be increased, but there will be some performance loss"` -} - -var config = driver.Config{ - Name: "Quqi", - OnlyLocal: true, - LocalSort: true, - //NoUpload: true, - DefaultRoot: "0", -} - -func init() { - op.RegisterDriver(func() driver.Driver { - return &Quqi{} - }) -} diff --git a/drivers/quqi/types.go b/drivers/quqi/types.go deleted file mode 100644 index 32557361..00000000 --- a/drivers/quqi/types.go +++ /dev/null @@ -1,197 +0,0 @@ -package quqi - -type BaseReqQuery struct { - ID string `json:"quqiid"` -} - -type BaseReq struct { - GroupID string `json:"quqi_id"` -} - -type BaseRes struct { - //Data interface{} `json:"data"` - Code int `json:"err"` - Message string `json:"msg"` -} - -type GroupRes struct { - BaseRes - Data []*Group `json:"data"` -} - -type ListRes struct { - BaseRes - Data *List `json:"data"` -} - -type GetDocRes struct { - BaseRes - Data struct { - OriginPath string `json:"origin_path"` - } `json:"data"` -} - -type GetDownloadResp struct { - BaseRes - Data struct { - Url string `json:"url"` - } `json:"data"` -} - -type MakeDirRes struct { - BaseRes - Data struct { - IsRoot bool `json:"is_root"` - NodeID int64 `json:"node_id"` - ParentID int64 `json:"parent_id"` - } `json:"data"` -} - -type MoveRes struct { - BaseRes - Data struct { - NodeChildNum int64 `json:"node_child_num"` - NodeID int64 `json:"node_id"` - NodeName string `json:"node_name"` - ParentID int64 `json:"parent_id"` - GroupID int64 `json:"quqi_id"` - TreeID int64 `json:"tree_id"` - } `json:"data"` -} - -type RenameRes struct { - BaseRes - Data struct { - NodeID int64 `json:"node_id"` - GroupID int64 `json:"quqi_id"` - Rename string `json:"rename"` - TreeID int64 `json:"tree_id"` - UpdateTime int64 `json:"updatetime"` - } `json:"data"` -} - -type CopyRes struct { - BaseRes -} - -type RemoveRes struct { - BaseRes -} - -type Group struct { - ID int `json:"quqi_id"` - Type int `json:"type"` - Name string `json:"name"` - IsAdministrator int `json:"is_administrator"` - Role int `json:"role"` - Avatar string `json:"avatar_url"` - IsStick int `json:"is_stick"` - Nickname string `json:"nickname"` - Status int `json:"status"` -} - -type List struct { - ListDir - Dir []*ListDir `json:"dir"` - File []*ListFile `json:"file"` -} - -type ListItem struct { - AddTime int64 `json:"add_time"` - IsDir int `json:"is_dir"` - IsExpand int `json:"is_expand"` - IsFinalize int `json:"is_finalize"` - LastEditorName string `json:"last_editor_name"` - Name string `json:"name"` - NodeID int64 `json:"nid"` - ParentID int64 `json:"parent_id"` - Permission int `json:"permission"` - TreeID int64 `json:"tid"` - UpdateCNT int64 `json:"update_cnt"` - UpdateTime int64 `json:"update_time"` -} - -type ListDir struct { - ListItem - ChildDocNum int64 `json:"child_doc_num"` - DirDetail string `json:"dir_detail"` - DirType int `json:"dir_type"` -} - -type ListFile struct { - ListItem - BroadDocType string `json:"broad_doc_type"` - CanDisplay bool `json:"can_display"` - Detail string `json:"detail"` - EXT string `json:"ext"` - Filetype string `json:"filetype"` - HasMobileThumbnail bool `json:"has_mobile_thumbnail"` - HasThumbnail bool `json:"has_thumbnail"` - Size int64 `json:"size"` - Version int `json:"version"` -} - -type UploadInitResp struct { - Data struct { - Bucket string `json:"bucket"` - Exist bool `json:"exist"` - Key string `json:"key"` - TaskID string `json:"task_id"` - Token string `json:"token"` - UploadID string `json:"upload_id"` - URL string `json:"url"` - NodeID int64 `json:"node_id"` - NodeName string `json:"node_name"` - ParentID int64 `json:"parent_id"` - } `json:"data"` - Err int `json:"err"` - Msg string `json:"msg"` -} - -type TempKeyResp struct { - Err int `json:"err"` - Msg string `json:"msg"` - Data struct { - ExpiredTime int `json:"expiredTime"` - Expiration string `json:"expiration"` - Credentials struct { - SessionToken string `json:"sessionToken"` - TmpSecretID string `json:"tmpSecretId"` - TmpSecretKey string `json:"tmpSecretKey"` - } `json:"credentials"` - RequestID string `json:"requestId"` - StartTime int `json:"startTime"` - } `json:"data"` -} - -type UploadFinishResp struct { - Data struct { - NodeID int64 `json:"node_id"` - NodeName string `json:"node_name"` - ParentID int64 `json:"parent_id"` - QuqiID int64 `json:"quqi_id"` - TreeID int64 `json:"tree_id"` - } `json:"data"` - Err int `json:"err"` - Msg string `json:"msg"` -} - -type UrlExchangeResp struct { - BaseRes - Data struct { - Name string `json:"name"` - Mime string `json:"mime"` - Size int64 `json:"size"` - DownloadType int `json:"download_type"` - ChannelType int `json:"channel_type"` - ChannelID int `json:"channel_id"` - Url string `json:"url"` - ExpiredTime int64 `json:"expired_time"` - IsEncrypted bool `json:"is_encrypted"` - EncryptedSize int64 `json:"encrypted_size"` - EncryptedAlg string `json:"encrypted_alg"` - EncryptedKey string `json:"encrypted_key"` - PassportID int64 `json:"passport_id"` - RequestExpiredTime int64 `json:"request_expired_time"` - } `json:"data"` -} diff --git a/drivers/quqi/util.go b/drivers/quqi/util.go deleted file mode 100644 index a1a23b76..00000000 --- a/drivers/quqi/util.go +++ /dev/null @@ -1,299 +0,0 @@ -package quqi - -import ( - "bufio" - "context" - "encoding/base64" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - - "github.com/OpenListTeam/OpenList/drivers/base" - "github.com/OpenListTeam/OpenList/internal/errs" - "github.com/OpenListTeam/OpenList/internal/model" - "github.com/OpenListTeam/OpenList/internal/stream" - "github.com/OpenListTeam/OpenList/pkg/http_range" - "github.com/OpenListTeam/OpenList/pkg/utils" - "github.com/go-resty/resty/v2" - "github.com/minio/sio" -) - -// do others that not defined in Driver interface -func (d *Quqi) request(host string, path string, method string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) { - var ( - reqUrl = url.URL{ - Scheme: "https", - Host: "quqi.com", - Path: path, - } - req = base.RestyClient.R() - result BaseRes - ) - - if host != "" { - reqUrl.Host = host - } - req.SetHeaders(map[string]string{ - "Origin": "https://quqi.com", - "Cookie": d.Cookie, - }) - - if d.GroupID != "" { - req.SetQueryParam("quqiid", d.GroupID) - } - - if callback != nil { - callback(req) - } - - res, err := req.Execute(method, reqUrl.String()) - if err != nil { - return nil, err - } - // resty.Request.SetResult cannot parse result correctly sometimes - err = utils.Json.Unmarshal(res.Body(), &result) - if err != nil { - return nil, err - } - if result.Code != 0 { - return nil, errors.New(result.Message) - } - if resp != nil { - err = utils.Json.Unmarshal(res.Body(), resp) - if err != nil { - return nil, err - } - } - return res, nil -} - -func (d *Quqi) login() error { - if d.Addition.Cookie != "" { - d.Cookie = d.Addition.Cookie - } - if d.checkLogin() { - return nil - } - if d.Cookie != "" { - return errors.New("cookie is invalid") - } - if d.Phone == "" { - return errors.New("phone number is empty") - } - if d.Password == "" { - return errs.EmptyPassword - } - - resp, err := d.request("", "/auth/person/v2/login/password", resty.MethodPost, func(req *resty.Request) { - req.SetFormData(map[string]string{ - "phone": d.Phone, - "password": base64.StdEncoding.EncodeToString([]byte(d.Password)), - }) - }, nil) - if err != nil { - return err - } - - var cookies []string - for _, cookie := range resp.RawResponse.Cookies() { - cookies = append(cookies, fmt.Sprintf("%s=%s", cookie.Name, cookie.Value)) - } - d.Cookie = strings.Join(cookies, ";") - - return nil -} - -func (d *Quqi) checkLogin() bool { - if _, err := d.request("", "/auth/account/baseInfo", resty.MethodGet, nil, nil); err != nil { - return false - } - return true -} - -// decryptKey 获取密码 -func decryptKey(encodeKey string) []byte { - // 移除非法字符 - u := strings.ReplaceAll(encodeKey, "[^A-Za-z0-9+\\/]", "") - - // 计算输出字节数组的长度 - o := len(u) - a := 32 - - // 创建输出字节数组 - c := make([]byte, a) - - // 编码循环 - s := uint32(0) // 累加器 - f := 0 // 输出数组索引 - for l := 0; l < o; l++ { - r := l & 3 // 取模4,得到当前字符在四字节块中的位置 - i := u[l] // 当前字符的ASCII码 - - // 编码当前字符 - switch { - case i >= 65 && i < 91: // 大写字母 - s |= uint32(i-65) << uint32(6*(3-r)) - case i >= 97 && i < 123: // 小写字母 - s |= uint32(i-71) << uint32(6*(3-r)) - case i >= 48 && i < 58: // 数字 - s |= uint32(i+4) << uint32(6*(3-r)) - case i == 43: // 加号 - s |= uint32(62) << uint32(6*(3-r)) - case i == 47: // 斜杠 - s |= uint32(63) << uint32(6*(3-r)) - } - - // 如果累加器已经包含了四个字符,或者是最后一个字符,则写入输出数组 - if r == 3 || l == o-1 { - for e := 0; e < 3 && f < a; e, f = e+1, f+1 { - c[f] = byte(s >> (16 >> e & 24) & 255) - } - s = 0 - } - } - - return c -} - -func (d *Quqi) linkFromPreview(id string) (*model.Link, error) { - var getDocResp GetDocRes - if _, err := d.request("", "/api/doc/getDoc", resty.MethodPost, func(req *resty.Request) { - req.SetFormData(map[string]string{ - "quqi_id": d.GroupID, - "tree_id": "1", - "node_id": id, - "client_id": d.ClientID, - }) - }, &getDocResp); err != nil { - return nil, err - } - if getDocResp.Data.OriginPath == "" { - return nil, errors.New("cannot get link from preview") - } - return &model.Link{ - URL: getDocResp.Data.OriginPath, - Header: http.Header{ - "Origin": []string{"https://quqi.com"}, - "Cookie": []string{d.Cookie}, - }, - }, nil -} - -func (d *Quqi) linkFromDownload(id string) (*model.Link, error) { - var getDownloadResp GetDownloadResp - if _, err := d.request("", "/api/doc/getDownload", resty.MethodGet, func(req *resty.Request) { - req.SetQueryParams(map[string]string{ - "quqi_id": d.GroupID, - "tree_id": "1", - "node_id": id, - "url_type": "undefined", - "entry_type": "undefined", - "client_id": d.ClientID, - "no_redirect": "1", - }) - }, &getDownloadResp); err != nil { - return nil, err - } - if getDownloadResp.Data.Url == "" { - return nil, errors.New("cannot get link from download") - } - - return &model.Link{ - URL: getDownloadResp.Data.Url, - Header: http.Header{ - "Origin": []string{"https://quqi.com"}, - "Cookie": []string{d.Cookie}, - }, - }, nil -} - -func (d *Quqi) linkFromCDN(id string) (*model.Link, error) { - downloadLink, err := d.linkFromDownload(id) - if err != nil { - return nil, err - } - - var urlExchangeResp UrlExchangeResp - if _, err = d.request("api.quqi.com", "/preview/downloadInfo/url/exchange", resty.MethodGet, func(req *resty.Request) { - req.SetQueryParam("url", downloadLink.URL) - }, &urlExchangeResp); err != nil { - return nil, err - } - if urlExchangeResp.Data.Url == "" { - return nil, errors.New("cannot get link from cdn") - } - - // 假设存在未加密的情况 - if !urlExchangeResp.Data.IsEncrypted { - return &model.Link{ - URL: urlExchangeResp.Data.Url, - Header: http.Header{ - "Origin": []string{"https://quqi.com"}, - "Cookie": []string{d.Cookie}, - }, - }, nil - } - - // 根据sio(https://github.com/minio/sio/blob/master/DARE.md)描述及实际测试,得出以下结论: - // 1. 加密后大小(encrypted_size)-原始文件大小(size) = 加密包的头大小+身份验证标识 = (16+16) * N -> N为加密包的数量 - // 2. 原始文件大小(size)+64*1024-1 / (64*1024) = N -> 每个包的有效负载为64K - remoteClosers := utils.EmptyClosers() - payloadSize := int64(1 << 16) - expiration := time.Until(time.Unix(urlExchangeResp.Data.ExpiredTime, 0)) - resultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) { - encryptedOffset := httpRange.Start / payloadSize * (payloadSize + 32) - decryptedOffset := httpRange.Start % payloadSize - encryptedLength := (httpRange.Length+httpRange.Start+payloadSize-1)/payloadSize*(payloadSize+32) - encryptedOffset - if httpRange.Length < 0 { - encryptedLength = httpRange.Length - } else { - if httpRange.Length+httpRange.Start >= urlExchangeResp.Data.Size || encryptedLength+encryptedOffset >= urlExchangeResp.Data.EncryptedSize { - encryptedLength = -1 - } - } - //log.Debugf("size: %d\tencrypted_size: %d", urlExchangeResp.Data.Size, urlExchangeResp.Data.EncryptedSize) - //log.Debugf("http range offset: %d, length: %d", httpRange.Start, httpRange.Length) - //log.Debugf("encrypted offset: %d, length: %d, decrypted offset: %d", encryptedOffset, encryptedLength, decryptedOffset) - - rrc, err := stream.GetRangeReadCloserFromLink(urlExchangeResp.Data.EncryptedSize, &model.Link{ - URL: urlExchangeResp.Data.Url, - Header: http.Header{ - "Origin": []string{"https://quqi.com"}, - "Cookie": []string{d.Cookie}, - }, - }) - if err != nil { - return nil, err - } - - rc, err := rrc.RangeRead(ctx, http_range.Range{Start: encryptedOffset, Length: encryptedLength}) - remoteClosers.AddClosers(rrc.GetClosers()) - if err != nil { - return nil, err - } - - decryptReader, err := sio.DecryptReader(rc, sio.Config{ - MinVersion: sio.Version10, - MaxVersion: sio.Version20, - CipherSuites: []byte{sio.CHACHA20_POLY1305, sio.AES_256_GCM}, - Key: decryptKey(urlExchangeResp.Data.EncryptedKey), - SequenceNumber: uint32(httpRange.Start / payloadSize), - }) - if err != nil { - return nil, err - } - bufferReader := bufio.NewReader(decryptReader) - bufferReader.Discard(int(decryptedOffset)) - - return io.NopCloser(bufferReader), nil - } - - return &model.Link{ - RangeReadCloser: &model.RangeReadCloser{RangeReader: resultRangeReader, Closers: remoteClosers}, - Expiration: &expiration, - }, nil -}