mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2025-09-19 20:26:26 +08:00
feat(quark_open): support rapid upload and thumbnail (#393)
This commit is contained in:
@ -34,7 +34,19 @@ func (d *QuarkOpen) GetAddition() driver.Additional {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *QuarkOpen) Init(ctx context.Context) error {
|
func (d *QuarkOpen) Init(ctx context.Context) error {
|
||||||
_, err := d.request(ctx, "/open/v1/user/info", http.MethodGet, nil, nil)
|
var resp UserInfoResp
|
||||||
|
|
||||||
|
_, err := d.request(ctx, "/open/v1/user/info", http.MethodGet, nil, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Data.UserID != "" {
|
||||||
|
d.conf.userId = resp.Data.UserID
|
||||||
|
} else {
|
||||||
|
return errors.New("failed to get user ID")
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,6 +173,12 @@ func (d *QuarkOpen) Put(ctx context.Context, dstDir model.Obj, stream model.File
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// 如果预上传已经完成,直接返回--秒传
|
||||||
|
if pre.Data.Finish == true {
|
||||||
|
up(100)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// get part info
|
// get part info
|
||||||
partInfo := d._getPartInfo(stream, pre.Data.PartSize)
|
partInfo := d._getPartInfo(stream, pre.Data.PartSize)
|
||||||
// get upload url info
|
// get upload url info
|
||||||
|
@ -18,8 +18,9 @@ type Addition struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Conf struct {
|
type Conf struct {
|
||||||
ua string
|
ua string
|
||||||
api string
|
api string
|
||||||
|
userId string
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
package quark_open
|
package quark_open
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/OpenListTeam/OpenList/internal/model"
|
"github.com/OpenListTeam/OpenList/internal/model"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Resp struct {
|
type Resp struct {
|
||||||
@ -17,6 +16,17 @@ type CommonRsp struct {
|
|||||||
ReqID string `json:"req_id"`
|
ReqID string `json:"req_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserInfo struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
AvatarURL string `json:"avatar_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserInfoResp struct {
|
||||||
|
CommonRsp
|
||||||
|
Data UserInfo `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
type RefreshTokenOnlineAPIResp struct {
|
type RefreshTokenOnlineAPIResp struct {
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
@ -38,13 +48,17 @@ type File struct {
|
|||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileToObj(f File) *model.Object {
|
func fileToObj(f File) *model.ObjThumb {
|
||||||
return &model.Object{
|
return &model.ObjThumb{
|
||||||
ID: f.Fid,
|
Object: model.Object{
|
||||||
Name: f.FileName,
|
ID: f.Fid,
|
||||||
Size: f.Size,
|
Name: f.FileName,
|
||||||
Modified: time.UnixMilli(f.UpdatedAt),
|
Size: f.Size,
|
||||||
IsFolder: f.FileType == "0",
|
Modified: time.UnixMilli(f.UpdatedAt),
|
||||||
|
IsFolder: f.FileType == "0",
|
||||||
|
Ctime: time.UnixMilli(f.CreatedAt),
|
||||||
|
},
|
||||||
|
Thumbnail: model.Thumbnail{Thumbnail: f.ThumbnailURL},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,14 +2,18 @@ package quark_open
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/OpenListTeam/OpenList/pkg/http_range"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/OpenListTeam/OpenList/drivers/base"
|
"github.com/OpenListTeam/OpenList/drivers/base"
|
||||||
@ -19,9 +23,21 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *QuarkOpen) request(ctx context.Context, pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
func (d *QuarkOpen) request(ctx context.Context, pathname string, method string, callback base.ReqCallback, resp interface{}, manualSign ...*ManualSign) ([]byte, error) {
|
||||||
u := d.conf.api + pathname
|
u := d.conf.api + pathname
|
||||||
tm, token, reqID := d.generateReqSign(method, pathname, d.Addition.SignKey)
|
|
||||||
|
var tm, token, reqID string
|
||||||
|
|
||||||
|
// 检查是否手动传入签名参数
|
||||||
|
if len(manualSign) > 0 && manualSign[0] != nil {
|
||||||
|
tm = manualSign[0].Tm
|
||||||
|
token = manualSign[0].Token
|
||||||
|
reqID = manualSign[0].ReqID
|
||||||
|
} else {
|
||||||
|
// 自动生成签名参数
|
||||||
|
tm, token, reqID = d.generateReqSign(method, pathname, d.Addition.SignKey)
|
||||||
|
}
|
||||||
|
|
||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
req.SetContext(ctx)
|
req.SetContext(ctx)
|
||||||
req.SetHeaders(map[string]string{
|
req.SetHeaders(map[string]string{
|
||||||
@ -48,7 +64,7 @@ func (d *QuarkOpen) request(ctx context.Context, pathname string, method string,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// 判断 是否需要 刷新 access_token
|
// 判断 是否需要 刷新 access_token
|
||||||
if e.Status == -1 && (e.Errno == 11001 || e.Errno == 14001) {
|
if e.Status == -1 && (e.Errno == 11001 || (e.Errno == 14001 && strings.Contains(e.ErrorInfo, "access_token"))) {
|
||||||
// token 过期
|
// token 过期
|
||||||
err = d.refreshToken()
|
err = d.refreshToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -106,10 +122,25 @@ func (d *QuarkOpen) GetFiles(ctx context.Context, parent string) ([]File, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *QuarkOpen) upPre(ctx context.Context, file model.FileStreamer, parentId, md5, sha1 string) (UpPreResp, error) {
|
func (d *QuarkOpen) upPre(ctx context.Context, file model.FileStreamer, parentId, md5, sha1 string) (UpPreResp, error) {
|
||||||
|
// 获取当前时间
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
// 获取文件大小
|
||||||
|
fileSize := file.GetSize()
|
||||||
|
|
||||||
|
// 手动生成 x-pan-token
|
||||||
|
httpMethod := "POST"
|
||||||
|
apiPath := "/open/v1/file/upload_pre"
|
||||||
|
tm, xPanToken, reqID := d.generateReqSign(httpMethod, apiPath, d.Addition.SignKey)
|
||||||
|
|
||||||
|
// 生成proof相关字段,传入 x-pan-token
|
||||||
|
proofVersion, proofSeed1, proofSeed2, proofCode1, proofCode2, err := d.generateProof(file, xPanToken)
|
||||||
|
if err != nil {
|
||||||
|
return UpPreResp{}, fmt.Errorf("failed to generate proof: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
data := base.Json{
|
data := base.Json{
|
||||||
"file_name": file.GetName(),
|
"file_name": file.GetName(),
|
||||||
"size": file.GetSize(),
|
"size": fileSize,
|
||||||
"format_type": file.GetMimetype(),
|
"format_type": file.GetMimetype(),
|
||||||
"md5": md5,
|
"md5": md5,
|
||||||
"sha1": sha1,
|
"sha1": sha1,
|
||||||
@ -117,15 +148,141 @@ func (d *QuarkOpen) upPre(ctx context.Context, file model.FileStreamer, parentId
|
|||||||
"l_updated_at": now.UnixMilli(),
|
"l_updated_at": now.UnixMilli(),
|
||||||
"pdir_fid": parentId,
|
"pdir_fid": parentId,
|
||||||
"same_path_reuse": true,
|
"same_path_reuse": true,
|
||||||
|
"proof_version": proofVersion,
|
||||||
|
"proof_seed1": proofSeed1,
|
||||||
|
"proof_seed2": proofSeed2,
|
||||||
|
"proof_code1": proofCode1,
|
||||||
|
"proof_code2": proofCode2,
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp UpPreResp
|
var resp UpPreResp
|
||||||
_, err := d.request(ctx, "/open/v1/file/upload_pre", http.MethodPost, func(req *resty.Request) {
|
|
||||||
|
// 使用手动生成的签名参数
|
||||||
|
manualSign := &ManualSign{
|
||||||
|
Tm: tm,
|
||||||
|
Token: xPanToken,
|
||||||
|
ReqID: reqID,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = d.request(ctx, "/open/v1/file/upload_pre", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(data)
|
req.SetBody(data)
|
||||||
}, &resp)
|
}, &resp, manualSign)
|
||||||
|
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateProof 生成夸克云盘文件上传的proof验证信息
|
||||||
|
func (d *QuarkOpen) generateProof(file model.FileStreamer, xPanToken string) (proofVersion, proofSeed1, proofSeed2, proofCode1, proofCode2 string, err error) {
|
||||||
|
// 获取文件大小
|
||||||
|
fileSize := file.GetSize()
|
||||||
|
// 设置proof_version (固定为"v1")
|
||||||
|
proofVersion = "v1"
|
||||||
|
// 生成proof_seed1 - 算法: md5(userid+x-pan-token)
|
||||||
|
proofSeed1 = d.generateProofSeed1(xPanToken)
|
||||||
|
// 生成proof_seed2 - 算法: md5(fileSize)
|
||||||
|
proofSeed2 = d.generateProofSeed2(fileSize)
|
||||||
|
// 生成proof_code1和proof_code2
|
||||||
|
proofCode1, err = d.generateProofCode(file, proofSeed1, fileSize)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", "", fmt.Errorf("failed to generate proof_code1: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proofCode2, err = d.generateProofCode(file, proofSeed2, fileSize)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", "", fmt.Errorf("failed to generate proof_code2: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return proofVersion, proofSeed1, proofSeed2, proofCode1, proofCode2, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateProofSeed1 生成proof_seed1,基于 userId、x-pan-token
|
||||||
|
func (d *QuarkOpen) generateProofSeed1(xPanToken string) string {
|
||||||
|
concatString := d.conf.userId + xPanToken
|
||||||
|
md5Hash := md5.Sum([]byte(concatString))
|
||||||
|
return hex.EncodeToString(md5Hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateProofSeed2 生成proof_seed2,基于 fileSize
|
||||||
|
func (d *QuarkOpen) generateProofSeed2(fileSize int64) string {
|
||||||
|
md5Hash := md5.Sum([]byte(strconv.FormatInt(fileSize, 10)))
|
||||||
|
return hex.EncodeToString(md5Hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProofRange struct {
|
||||||
|
Start int64
|
||||||
|
End int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateProofCode 根据proof_seed和文件大小生成proof_code
|
||||||
|
func (d *QuarkOpen) generateProofCode(file model.FileStreamer, proofSeed string, fileSize int64) (string, error) {
|
||||||
|
// 获取读取范围
|
||||||
|
proofRange, err := d.getProofRange(proofSeed, fileSize)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get proof range: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算需要读取的长度
|
||||||
|
length := proofRange.End - proofRange.Start
|
||||||
|
if length == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用FileStreamer的RangeRead方法读取特定范围的数据
|
||||||
|
reader, err := file.RangeRead(http_range.Range{
|
||||||
|
Start: proofRange.Start,
|
||||||
|
Length: length,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to range read: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if closer, ok := reader.(io.Closer); ok {
|
||||||
|
closer.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 读取数据
|
||||||
|
buf := make([]byte, length)
|
||||||
|
n, err := io.ReadFull(reader, buf)
|
||||||
|
if errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
|
return "", fmt.Errorf("can't read data, expected=%d, got=%d", length, n)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64编码
|
||||||
|
return base64.StdEncoding.EncodeToString(buf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getProofRange 根据proof_seed和文件大小计算需要读取的文件范围
|
||||||
|
func (d *QuarkOpen) getProofRange(proofSeed string, fileSize int64) (*ProofRange, error) {
|
||||||
|
if fileSize == 0 {
|
||||||
|
return &ProofRange{}, nil
|
||||||
|
}
|
||||||
|
// 对 proofSeed 进行 MD5 处理,取前16个字符
|
||||||
|
md5Hash := md5.Sum([]byte(proofSeed))
|
||||||
|
tmpStr := hex.EncodeToString(md5Hash[:])[:16]
|
||||||
|
// 转为 uint64
|
||||||
|
tmpInt, err := strconv.ParseUint(tmpStr, 16, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse hex string: %w", err)
|
||||||
|
}
|
||||||
|
// 计算索引位置
|
||||||
|
index := tmpInt % uint64(fileSize)
|
||||||
|
|
||||||
|
pr := &ProofRange{
|
||||||
|
Start: int64(index),
|
||||||
|
End: int64(index) + 8,
|
||||||
|
}
|
||||||
|
// 确保 End 不超过文件大小
|
||||||
|
if pr.End > fileSize {
|
||||||
|
pr.End = fileSize
|
||||||
|
}
|
||||||
|
|
||||||
|
return pr, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *QuarkOpen) _getPartInfo(stream model.FileStreamer, partSize int64) []base.Json {
|
func (d *QuarkOpen) _getPartInfo(stream model.FileStreamer, partSize int64) []base.Json {
|
||||||
// 计算分片信息
|
// 计算分片信息
|
||||||
partInfo := make([]base.Json, 0)
|
partInfo := make([]base.Json, 0)
|
||||||
@ -239,6 +396,13 @@ func (d *QuarkOpen) upFinish(ctx context.Context, pre UpPreResp, partInfo []base
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ManualSign 用于手动签名URL的结构体
|
||||||
|
type ManualSign struct {
|
||||||
|
Tm string
|
||||||
|
Token string
|
||||||
|
ReqID string
|
||||||
|
}
|
||||||
|
|
||||||
func (d *QuarkOpen) generateReqSign(method string, pathname string, signKey string) (string, string, string) {
|
func (d *QuarkOpen) generateReqSign(method string, pathname string, signKey string) (string, string, string) {
|
||||||
// 生成时间戳 (13位毫秒级)
|
// 生成时间戳 (13位毫秒级)
|
||||||
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
|
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
|
||||||
|
Reference in New Issue
Block a user