mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2025-07-18 17:38:07 +08:00
feat(quark_open): add quark open driver support (#324)
This commit is contained in:
@ -50,6 +50,7 @@ import (
|
||||
_ "github.com/OpenListTeam/OpenList/drivers/openlist"
|
||||
_ "github.com/OpenListTeam/OpenList/drivers/pikpak"
|
||||
_ "github.com/OpenListTeam/OpenList/drivers/pikpak_share"
|
||||
_ "github.com/OpenListTeam/OpenList/drivers/quark_open"
|
||||
_ "github.com/OpenListTeam/OpenList/drivers/quark_uc"
|
||||
_ "github.com/OpenListTeam/OpenList/drivers/quark_uc_tv"
|
||||
_ "github.com/OpenListTeam/OpenList/drivers/s3"
|
||||
|
216
drivers/quark_open/driver.go
Normal file
216
drivers/quark_open/driver.go
Normal file
@ -0,0 +1,216 @@
|
||||
package quark_open
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/OpenListTeam/OpenList/drivers/base"
|
||||
"github.com/OpenListTeam/OpenList/internal/driver"
|
||||
"github.com/OpenListTeam/OpenList/internal/errs"
|
||||
"github.com/OpenListTeam/OpenList/internal/model"
|
||||
streamPkg "github.com/OpenListTeam/OpenList/internal/stream"
|
||||
"github.com/OpenListTeam/OpenList/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"hash"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type QuarkOpen struct {
|
||||
model.Storage
|
||||
Addition
|
||||
config driver.Config
|
||||
conf Conf
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) Config() driver.Config {
|
||||
return d.config
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) Init(ctx context.Context) error {
|
||||
_, err := d.request(ctx, "/open/v1/user/info", http.MethodGet, nil, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
files, err := d.GetFiles(ctx, dir.GetID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
|
||||
return fileToObj(src), nil
|
||||
})
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
data := base.Json{
|
||||
"fid": file.GetID(),
|
||||
}
|
||||
var resp FileLikeResp
|
||||
_, err := d.request(ctx, "/open/v1/file/get_download_url", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.Link{
|
||||
URL: resp.Data.DownloadURL,
|
||||
Header: http.Header{
|
||||
"Cookie": []string{d.generateAuthCookie()},
|
||||
},
|
||||
Concurrency: 3,
|
||||
PartSize: 10 * utils.MB,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
data := base.Json{
|
||||
"dir_path": dirName,
|
||||
"pdir_fid": parentDir.GetID(),
|
||||
}
|
||||
_, err := d.request(ctx, "/open/v1/dir", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, nil)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
data := base.Json{
|
||||
"action_type": 1,
|
||||
"fid_list": []string{srcObj.GetID()},
|
||||
"to_pdir_fid": dstDir.GetID(),
|
||||
}
|
||||
_, err := d.request(ctx, "/open/v1/file/move", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, nil)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
data := base.Json{
|
||||
"fid": srcObj.GetID(),
|
||||
"file_name": newName,
|
||||
"conflict_mode": "REUSE",
|
||||
}
|
||||
_, err := d.request(ctx, "/open/v1/file/rename", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, nil)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) Remove(ctx context.Context, obj model.Obj) error {
|
||||
data := base.Json{
|
||||
"action_type": 1,
|
||||
"fid_list": []string{obj.GetID()},
|
||||
}
|
||||
_, err := d.request(ctx, "/open/v1/file/delete", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, nil)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
md5Str, sha1Str := stream.GetHash().GetHash(utils.MD5), stream.GetHash().GetHash(utils.SHA1)
|
||||
var (
|
||||
md5 hash.Hash
|
||||
sha1 hash.Hash
|
||||
)
|
||||
writers := []io.Writer{}
|
||||
if len(md5Str) != utils.MD5.Width {
|
||||
md5 = utils.MD5.NewFunc()
|
||||
writers = append(writers, md5)
|
||||
}
|
||||
if len(sha1Str) != utils.SHA1.Width {
|
||||
sha1 = utils.SHA1.NewFunc()
|
||||
writers = append(writers, sha1)
|
||||
}
|
||||
|
||||
if len(writers) > 0 {
|
||||
_, err := streamPkg.CacheFullInTempFileAndWriter(stream, io.MultiWriter(writers...))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if md5 != nil {
|
||||
md5Str = hex.EncodeToString(md5.Sum(nil))
|
||||
}
|
||||
if sha1 != nil {
|
||||
sha1Str = hex.EncodeToString(sha1.Sum(nil))
|
||||
}
|
||||
}
|
||||
// pre
|
||||
pre, err := d.upPre(ctx, stream, dstDir.GetID(), md5Str, sha1Str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// get part info
|
||||
partInfo := d._getPartInfo(stream, pre.Data.PartSize)
|
||||
// get upload url info
|
||||
upUrlInfo, err := d.upUrl(ctx, pre, partInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// part up
|
||||
total := stream.GetSize()
|
||||
left := total
|
||||
part := make([]byte, pre.Data.PartSize)
|
||||
// 用于存储每个分片的ETag,后续commit时需要
|
||||
etags := make([]string, len(partInfo))
|
||||
|
||||
// 遍历上传每个分片
|
||||
for i, urlInfo := range upUrlInfo.UploadUrls {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
currentSize := int64(urlInfo.PartSize)
|
||||
if left < currentSize {
|
||||
part = part[:left]
|
||||
} else {
|
||||
part = part[:currentSize]
|
||||
}
|
||||
|
||||
// 读取分片数据
|
||||
n, err := io.ReadFull(stream, part)
|
||||
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 准备上传分片
|
||||
reader := driver.NewLimitedUploadStream(ctx, bytes.NewReader(part))
|
||||
etag, err := d.upPart(ctx, upUrlInfo, i, reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload part %d: %w", i, err)
|
||||
}
|
||||
|
||||
// 保存ETag,用于后续commit
|
||||
etags[i] = etag
|
||||
|
||||
// 更新剩余大小和进度
|
||||
left -= int64(n)
|
||||
up(float64(total-left) / float64(total) * 100)
|
||||
}
|
||||
|
||||
return d.upFinish(ctx, pre, partInfo, etags)
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*QuarkOpen)(nil)
|
40
drivers/quark_open/meta.go
Normal file
40
drivers/quark_open/meta.go
Normal file
@ -0,0 +1,40 @@
|
||||
package quark_open
|
||||
|
||||
import (
|
||||
"github.com/OpenListTeam/OpenList/internal/driver"
|
||||
"github.com/OpenListTeam/OpenList/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
driver.RootID
|
||||
OrderBy string `json:"order_by" type:"select" options:"none,file_type,file_name,updated_at,created_at" default:"none"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||
UseOnlineAPI bool `json:"use_online_api" default:"true"`
|
||||
APIAddress string `json:"api_url_address" default:"https://api.oplist.org/quarkyun/renewapi"`
|
||||
AccessToken string `json:"access_token" required:"false" default:""`
|
||||
RefreshToken string `json:"refresh_token" required:"true"`
|
||||
AppID string `json:"app_id" required:"true" help:"Keep it empty if you don't have one"`
|
||||
SignKey string `json:"sign_key" required:"true" help:"Keep it empty if you don't have one"`
|
||||
}
|
||||
|
||||
type Conf struct {
|
||||
ua string
|
||||
api string
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &QuarkOpen{
|
||||
config: driver.Config{
|
||||
Name: "QuarkOpen",
|
||||
OnlyLocal: true,
|
||||
DefaultRoot: "0",
|
||||
NoOverwriteUpload: true,
|
||||
},
|
||||
conf: Conf{
|
||||
ua: "go-resty/3.0.0-beta.1 (https://resty.dev)",
|
||||
api: "https://open-api-drive.quark.cn",
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
131
drivers/quark_open/types.go
Normal file
131
drivers/quark_open/types.go
Normal file
@ -0,0 +1,131 @@
|
||||
package quark_open
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/internal/model"
|
||||
)
|
||||
|
||||
type Resp struct {
|
||||
CommonRsp
|
||||
Errno int `json:"errno"`
|
||||
ErrorInfo string `json:"error_info"`
|
||||
}
|
||||
|
||||
type CommonRsp struct {
|
||||
Status int `json:"status"`
|
||||
ReqID string `json:"req_id"`
|
||||
}
|
||||
|
||||
type RefreshTokenOnlineAPIResp struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
AccessToken string `json:"access_token"`
|
||||
AppID string `json:"app_id"`
|
||||
SignKey string `json:"sign_key"`
|
||||
ErrorMessage string `json:"text"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Fid string `json:"fid"`
|
||||
ParentFid string `json:"parent_fid"`
|
||||
Category int64 `json:"category"`
|
||||
FileName string `json:"filename"`
|
||||
Size int64 `json:"size"`
|
||||
FileType string `json:"file_type"`
|
||||
ThumbnailURL string `json:"thumbnail_url"`
|
||||
ContentHash string `json:"content_hash"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
func fileToObj(f File) *model.Object {
|
||||
return &model.Object{
|
||||
ID: f.Fid,
|
||||
Name: f.FileName,
|
||||
Size: f.Size,
|
||||
Modified: time.UnixMilli(f.UpdatedAt),
|
||||
IsFolder: f.FileType == "0",
|
||||
}
|
||||
}
|
||||
|
||||
type QueryCursor struct {
|
||||
Version string `json:"version"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type FileListResp struct {
|
||||
CommonRsp
|
||||
Data struct {
|
||||
FileList []File `json:"file_list"`
|
||||
LastPage bool `json:"last_page"`
|
||||
NextQueryCursor QueryCursor `json:"next_query_cursor"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type FileLikeResp struct {
|
||||
CommonRsp
|
||||
Data struct {
|
||||
Fid string `json:"fid"`
|
||||
Size int `json:"size"`
|
||||
FileName string `json:"file_name"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type UpPreResp struct {
|
||||
CommonRsp
|
||||
Data struct {
|
||||
Finish bool `json:"finish"`
|
||||
TaskID string `json:"task_id"`
|
||||
Fid string `json:"fid"`
|
||||
CommonHeaders struct {
|
||||
XOssContentSha256 string `json:"X-Oss-Content-Sha256"`
|
||||
XOssDate string `json:"X-Oss-Date"`
|
||||
} `json:"common_headers"`
|
||||
UploadUrls []struct {
|
||||
PartNumber int `json:"part_number"`
|
||||
SignatureInfo struct {
|
||||
AuthType string `json:"auth_type"`
|
||||
Signature string `json:"signature"`
|
||||
} `json:"signature_info"`
|
||||
UploadURL string `json:"upload_url"`
|
||||
Expired int64 `json:"expired"`
|
||||
} `json:"upload_urls"`
|
||||
PartSize int64 `json:"part_size"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type UpUrlInfo struct {
|
||||
UploadUrls []struct {
|
||||
PartNumber int `json:"part_number"`
|
||||
PartSize int `json:"part_size"`
|
||||
SignatureInfo struct {
|
||||
AuthType string `json:"auth_type"`
|
||||
Signature string `json:"signature"`
|
||||
} `json:"signature_info"`
|
||||
UploadURL string `json:"upload_url"`
|
||||
} `json:"upload_urls"`
|
||||
CommonHeaders struct {
|
||||
XOssContentSha256 string `json:"X-Oss-Content-Sha256"`
|
||||
XOssDate string `json:"X-Oss-Date"`
|
||||
} `json:"common_headers"`
|
||||
UploadID string `json:"upload_id"`
|
||||
}
|
||||
|
||||
type UpUrlResp struct {
|
||||
CommonRsp
|
||||
Data UpUrlInfo `json:"data"`
|
||||
}
|
||||
|
||||
type UpFinishResp struct {
|
||||
CommonRsp
|
||||
Data struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Fid string `json:"fid"`
|
||||
Finish bool `json:"finish"`
|
||||
PdirFid string `json:"pdir_fid"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
FormatType string `json:"format_type"`
|
||||
Size int `json:"size"`
|
||||
} `json:"data"`
|
||||
}
|
309
drivers/quark_open/util.go
Normal file
309
drivers/quark_open/util.go
Normal file
@ -0,0 +1,309 @@
|
||||
package quark_open
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/drivers/base"
|
||||
"github.com/OpenListTeam/OpenList/internal/model"
|
||||
"github.com/OpenListTeam/OpenList/internal/op"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (d *QuarkOpen) request(ctx context.Context, pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
u := d.conf.api + pathname
|
||||
tm, token, reqID := d.generateReqSign(method, pathname, d.Addition.SignKey)
|
||||
req := base.RestyClient.R()
|
||||
req.SetContext(ctx)
|
||||
req.SetHeaders(map[string]string{
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"User-Agent": d.conf.ua,
|
||||
"x-pan-tm": tm,
|
||||
"x-pan-token": token,
|
||||
"x-pan-client-id": d.Addition.AppID,
|
||||
})
|
||||
req.SetQueryParams(map[string]string{
|
||||
"req_id": reqID,
|
||||
"access_token": d.Addition.AccessToken,
|
||||
})
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
var e Resp
|
||||
req.SetError(&e)
|
||||
res, err := req.Execute(method, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 判断 是否需要 刷新 access_token
|
||||
if e.Status == -1 && (e.Errno == 11001 || e.Errno == 14001) {
|
||||
// token 过期
|
||||
err = d.refreshToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx1, cancelFunc := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancelFunc()
|
||||
return d.request(ctx1, pathname, method, callback, resp)
|
||||
}
|
||||
|
||||
if e.Status >= 400 || e.Errno != 0 {
|
||||
return nil, errors.New(e.ErrorInfo)
|
||||
}
|
||||
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) GetFiles(ctx context.Context, parent string) ([]File, error) {
|
||||
files := make([]File, 0)
|
||||
var queryCursor QueryCursor
|
||||
|
||||
for {
|
||||
reqBody := map[string]interface{}{
|
||||
"parent_fid": parent,
|
||||
"size": 100, // 默认每页100个文件
|
||||
"sort": "file_name:asc", // 基本排序方式
|
||||
}
|
||||
// 如果有排序设置
|
||||
if d.OrderBy != "none" {
|
||||
reqBody["sort"] = d.OrderBy + ":" + d.OrderDirection
|
||||
}
|
||||
// 设置查询游标(用于分页)
|
||||
if queryCursor.Token != "" {
|
||||
reqBody["query_cursor"] = queryCursor
|
||||
}
|
||||
|
||||
var resp FileListResp
|
||||
_, err := d.request(ctx, "/open/v1/file/list", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(reqBody)
|
||||
}, &resp)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files = append(files, resp.Data.FileList...)
|
||||
if resp.Data.LastPage {
|
||||
break
|
||||
}
|
||||
|
||||
queryCursor = resp.Data.NextQueryCursor
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) upPre(ctx context.Context, file model.FileStreamer, parentId, md5, sha1 string) (UpPreResp, error) {
|
||||
now := time.Now()
|
||||
data := base.Json{
|
||||
"file_name": file.GetName(),
|
||||
"size": file.GetSize(),
|
||||
"format_type": file.GetMimetype(),
|
||||
"md5": md5,
|
||||
"sha1": sha1,
|
||||
"l_created_at": now.UnixMilli(),
|
||||
"l_updated_at": now.UnixMilli(),
|
||||
"pdir_fid": parentId,
|
||||
"same_path_reuse": true,
|
||||
}
|
||||
var resp UpPreResp
|
||||
_, err := d.request(ctx, "/open/v1/file/upload_pre", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, &resp)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) _getPartInfo(stream model.FileStreamer, partSize int64) []base.Json {
|
||||
// 计算分片信息
|
||||
partInfo := make([]base.Json, 0)
|
||||
total := stream.GetSize()
|
||||
left := total
|
||||
partNumber := 1
|
||||
|
||||
// 计算每个分片的大小和编号
|
||||
for left > 0 {
|
||||
size := partSize
|
||||
if left < partSize {
|
||||
size = left
|
||||
}
|
||||
|
||||
partInfo = append(partInfo, base.Json{
|
||||
"part_number": partNumber,
|
||||
"part_size": size,
|
||||
})
|
||||
|
||||
left -= size
|
||||
partNumber++
|
||||
}
|
||||
|
||||
return partInfo
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) upUrl(ctx context.Context, pre UpPreResp, partInfo []base.Json) (upUrlInfo UpUrlInfo, err error) {
|
||||
// 构建请求体
|
||||
data := base.Json{
|
||||
"task_id": pre.Data.TaskID,
|
||||
"part_info_list": partInfo,
|
||||
}
|
||||
var resp UpUrlResp
|
||||
|
||||
_, err = d.request(ctx, "/open/v1/file/get_upload_urls", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, &resp)
|
||||
|
||||
if err != nil {
|
||||
return upUrlInfo, err
|
||||
}
|
||||
|
||||
return resp.Data, nil
|
||||
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) upPart(ctx context.Context, upUrlInfo UpUrlInfo, partNumber int, bytes io.Reader) (string, error) {
|
||||
// 创建请求
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, upUrlInfo.UploadUrls[partNumber].UploadURL, bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", upUrlInfo.UploadUrls[partNumber].SignatureInfo.Signature)
|
||||
req.Header.Set("X-Oss-Date", upUrlInfo.CommonHeaders.XOssDate)
|
||||
req.Header.Set("X-Oss-Content-Sha256", upUrlInfo.CommonHeaders.XOssContentSha256)
|
||||
req.Header.Set("Accept-Encoding", "gzip")
|
||||
req.Header.Set("User-Agent", "Go-http-client/1.1")
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("up status: %d, error: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
// 返回 Etag 作为分片上传的标识
|
||||
return resp.Header.Get("Etag"), nil
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) upFinish(ctx context.Context, pre UpPreResp, partInfo []base.Json, etags []string) error {
|
||||
// 创建 part_info_list
|
||||
partInfoList := make([]base.Json, len(partInfo))
|
||||
// 确保 partInfo 和 etags 长度一致
|
||||
if len(partInfo) != len(etags) {
|
||||
return fmt.Errorf("part info count (%d) does not match etags count (%d)", len(partInfo), len(etags))
|
||||
}
|
||||
// 组合 part_info_list
|
||||
for i, part := range partInfo {
|
||||
partInfoList[i] = base.Json{
|
||||
"part_number": part["part_number"],
|
||||
"part_size": part["part_size"],
|
||||
"etag": etags[i],
|
||||
}
|
||||
}
|
||||
// 构建请求体
|
||||
data := base.Json{
|
||||
"task_id": pre.Data.TaskID,
|
||||
"part_info_list": partInfoList,
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
var resp UpFinishResp
|
||||
_, err := d.request(ctx, "/open/v1/file/upload_finish", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, &resp)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.Data.Finish != true {
|
||||
return fmt.Errorf("upload finish failed, task_id: %s", resp.Data.TaskID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) generateReqSign(method string, pathname string, signKey string) (string, string, string) {
|
||||
// 生成时间戳 (13位毫秒级)
|
||||
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
|
||||
|
||||
// 生成 x-pan-token token的组成是: method + "&" + pathname + "&" + timestamp + "&" + signKey
|
||||
tokenData := method + "&" + pathname + "&" + timestamp + "&" + signKey
|
||||
tokenHash := sha256.Sum256([]byte(tokenData))
|
||||
xPanToken := hex.EncodeToString(tokenHash[:])
|
||||
|
||||
// 生成 req_id
|
||||
reqUuid, _ := uuid.NewRandom()
|
||||
reqID := reqUuid.String()
|
||||
|
||||
return timestamp, xPanToken, reqID
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) refreshToken() error {
|
||||
refresh, access, err := d._refreshToken()
|
||||
for i := 0; i < 3; i++ {
|
||||
if err == nil {
|
||||
break
|
||||
} else {
|
||||
log.Errorf("[quark_open] failed to refresh token: %s", err)
|
||||
}
|
||||
refresh, access, err = d._refreshToken()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("[quark_open] token exchange: %s -> %s", d.RefreshToken, refresh)
|
||||
d.RefreshToken, d.AccessToken = refresh, access
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *QuarkOpen) _refreshToken() (string, string, error) {
|
||||
if d.UseOnlineAPI && d.APIAddress != "" {
|
||||
u := d.APIAddress
|
||||
var resp RefreshTokenOnlineAPIResp
|
||||
_, err := base.RestyClient.R().
|
||||
SetResult(&resp).
|
||||
SetQueryParams(map[string]string{
|
||||
"refresh_ui": d.RefreshToken,
|
||||
"server_use": "true",
|
||||
"driver_txt": "quarkyun_oa",
|
||||
}).
|
||||
Get(u)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if resp.RefreshToken == "" || resp.AccessToken == "" {
|
||||
if resp.ErrorMessage != "" {
|
||||
return "", "", fmt.Errorf("failed to refresh token: %s", resp.ErrorMessage)
|
||||
}
|
||||
return "", "", fmt.Errorf("empty token returned from official API")
|
||||
}
|
||||
return resp.RefreshToken, resp.AccessToken, nil
|
||||
}
|
||||
|
||||
// TODO 本地刷新逻辑
|
||||
return "", "", fmt.Errorf("local refresh token logic is not implemented yet, please use online API or contact the developer")
|
||||
}
|
||||
|
||||
// 生成认证 Cookie
|
||||
func (d *QuarkOpen) generateAuthCookie() string {
|
||||
return fmt.Sprintf("x_pan_client_id=%s; x_pan_access_token=%s",
|
||||
d.Addition.AppID, d.Addition.AccessToken)
|
||||
}
|
Reference in New Issue
Block a user