mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-05 13:06:36 +08:00
优化,排查项目问题
This commit is contained in:
@@ -23,9 +23,14 @@ type OpenClient struct {
|
||||
RefreshTokenStr string
|
||||
executor *QueueExecutor
|
||||
|
||||
// tokenMu 保护令牌刷新:业务请求中途 access_token 失效时自动刷新重试,
|
||||
// 多 goroutine(同步列表 + 下载队列)并发下只允许一次刷新进行。
|
||||
tokenMu sync.Mutex
|
||||
// OnTokenRefreshed 在 access_token 刷新成功后回调(参数为新令牌对),
|
||||
// 供上层持久化新令牌使用;nil 安全,且在 tokenMu 释放后调用以避免死锁。
|
||||
OnTokenRefreshed func(accessToken, refreshToken string)
|
||||
|
||||
// tokenMu 保护 AccessToken / RefreshTokenStr 的并发读写:业务请求中途
|
||||
// access_token 失效时自动刷新重试,多 goroutine(同步列表 + 下载队列)
|
||||
// 并发下只允许一次刷新进行。
|
||||
tokenMu sync.RWMutex
|
||||
}
|
||||
|
||||
// default115HTTPClient 创建带有防 405 重定向保护的 http.Client。
|
||||
@@ -57,12 +62,40 @@ func NewOpenClient(appID, accessToken, refreshToken string) *OpenClient {
|
||||
}
|
||||
}
|
||||
|
||||
// SetAuthToken 更新认证令牌。
|
||||
// SetAuthToken 更新认证令牌(并发安全)。
|
||||
func (c *OpenClient) SetAuthToken(accessToken, refreshToken string) {
|
||||
c.tokenMu.Lock()
|
||||
c.setAuthTokenLocked(accessToken, refreshToken)
|
||||
c.tokenMu.Unlock()
|
||||
}
|
||||
|
||||
// setAuthTokenLocked 无锁更新令牌,调用方必须已持有 tokenMu 写锁
|
||||
// (tryRefreshTokenLocked 等已持锁流程内部使用,避免重入死锁)。
|
||||
func (c *OpenClient) setAuthTokenLocked(accessToken, refreshToken string) {
|
||||
c.AccessToken = accessToken
|
||||
c.RefreshTokenStr = refreshToken
|
||||
}
|
||||
|
||||
// currentAccessToken 返回当前 access_token(并发安全)。
|
||||
func (c *OpenClient) currentAccessToken() string {
|
||||
c.tokenMu.RLock()
|
||||
defer c.tokenMu.RUnlock()
|
||||
return c.AccessToken
|
||||
}
|
||||
|
||||
// currentRefreshToken 返回当前 refresh_token(并发安全)。
|
||||
func (c *OpenClient) currentRefreshToken() string {
|
||||
c.tokenMu.RLock()
|
||||
defer c.tokenMu.RUnlock()
|
||||
return c.RefreshTokenStr
|
||||
}
|
||||
|
||||
// CurrentAccessToken 返回当前 access_token 快照(并发安全),
|
||||
// 供上层在无锁环境下安全读取(如 Ping 时探测令牌是否存在)。
|
||||
func (c *OpenClient) CurrentAccessToken() string {
|
||||
return c.currentAccessToken()
|
||||
}
|
||||
|
||||
// RespState 兼容 115 不同端点返回的 state 类型(proapi 返回布尔、passport 返回数字)。
|
||||
type RespState bool
|
||||
|
||||
@@ -191,6 +224,14 @@ func (c *OpenClient) doJSON(ctx context.Context, method, rawURL string, form map
|
||||
if access {
|
||||
// 刷新失败(或已刷新仍失败)时返回明确错误
|
||||
lastErr = NewOpenAPIResponseError(base.Code, base.Errno, base.Message, base.Error, "115: access_token 校验失败且刷新未成功")
|
||||
} else {
|
||||
// 未携带令牌的请求(登录/刷新流程)命中 token 类错误码:
|
||||
// 必须返回显式 error,避免调用方把 (resp, nil) 当作成功处理
|
||||
msg := base.Message
|
||||
if msg == "" {
|
||||
msg = base.Error
|
||||
}
|
||||
lastErr = fmt.Errorf("115: 认证失败(code=%d): %s", base.Code, msg)
|
||||
}
|
||||
return &base, lastErr
|
||||
}
|
||||
@@ -242,8 +283,11 @@ func (c *OpenClient) buildRequestWithUA(ctx context.Context, method, rawURL stri
|
||||
if method == http.MethodPost && len(form) > 0 {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
if access && c.AccessToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.AccessToken)
|
||||
if access {
|
||||
// RLock 读取令牌,避免与刷新流程的写入产生数据竞争
|
||||
if accessToken := c.currentAccessToken(); accessToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
}
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
@@ -261,33 +305,57 @@ func (c *OpenClient) doAuthJSONWithUA(ctx context.Context, method, rawURL string
|
||||
// tryRefreshTokenLocked 并发安全地刷新 access_token;成功返回 true(调用方
|
||||
// 应使用内存中的新 token 重试原请求)。
|
||||
//
|
||||
// 拿到写锁后在锁内读取 oldAccess,与持锁期间的当前值对比:若已被其他
|
||||
// goroutine 刷新过则直接复用新 token,避免并发请求连环轮转消耗 115 的
|
||||
// 一次性 refresh_token。全程持写锁读写 token 字段,无 TOCTOU 窗口。
|
||||
//
|
||||
// 对"refresh_token 本身已失效/被吊销"(IsRefreshTokenDead,如 40140114/116/119/120)
|
||||
// 这类不可恢复的错误直接放弃并清空内存 token(提示需重新授权)。
|
||||
// 对其它失败(网络瞬时抖动、刷新接口可重试错误码等)做指数退避重试几次再放弃,
|
||||
// 避免同步长任务中途 token 到期时恰好撞上一个短暂的刷新失败就整体失败。
|
||||
func (c *OpenClient) tryRefreshTokenLocked(ctx context.Context) bool {
|
||||
c.tokenMu.Lock()
|
||||
defer c.tokenMu.Unlock()
|
||||
// 在已持有写锁内读取当前 token 作为"刷新前快照",消除双重加锁窗口:
|
||||
// 若在拿锁期间已有其他 goroutine 完成刷新,refreshTokenWhileLocked
|
||||
// 内的 c.AccessToken != oldAccess 判断会立即命中并返回复用。
|
||||
oldAccess := c.AccessToken
|
||||
newToken, ok := c.refreshTokenWhileLocked(ctx, oldAccess)
|
||||
c.tokenMu.Unlock()
|
||||
// 回调必须在 tokenMu 释放后调用,避免上层在回调内访问客户端时死锁
|
||||
if ok && newToken != nil && c.OnTokenRefreshed != nil {
|
||||
c.OnTokenRefreshed(newToken.AccessToken, newToken.RefreshToken)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// refreshTokenWhileLocked 在已持有 tokenMu 写锁的前提下执行刷新。
|
||||
// 返回 (新令牌, 是否成功);命中"他人已刷新"捷径时新令牌为 nil。
|
||||
func (c *OpenClient) refreshTokenWhileLocked(ctx context.Context, oldAccess string) (*TokenData, bool) {
|
||||
if c.AccessToken != oldAccess {
|
||||
// 其他 goroutine 刚刷新过:直接复用内存中的新 token 重试原请求
|
||||
return nil, true
|
||||
}
|
||||
refreshToken := c.RefreshTokenStr
|
||||
for attempt := 0; attempt < refreshAttempts; attempt++ {
|
||||
token, err := c.RefreshToken(c.RefreshTokenStr)
|
||||
token, err := c.doRefreshToken(refreshToken)
|
||||
if err == nil {
|
||||
c.SetAuthToken(token.AccessToken, token.RefreshToken)
|
||||
return true
|
||||
c.setAuthTokenLocked(token.AccessToken, token.RefreshToken)
|
||||
return token, true
|
||||
}
|
||||
if IsRefreshTokenDead(err) {
|
||||
c.SetAuthToken("", "")
|
||||
return false
|
||||
c.setAuthTokenLocked("", "")
|
||||
return nil, false
|
||||
}
|
||||
// 可恢复失败:退避后重试。ctx 取消时立即放弃。
|
||||
if attempt < refreshAttempts-1 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
return nil, false
|
||||
case <-time.After(refreshBackoff(attempt)):
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// refreshAttempts 是刷新 access_token 失败时的最大尝试次数(含首次)。
|
||||
@@ -312,7 +380,14 @@ func isTokenCode(code int) bool {
|
||||
}
|
||||
|
||||
// openList 解析 data 为对象或数组(StructOrArray 语义)。
|
||||
// 115 部分接口在鉴权/业务异常时会返回 data:null 或 data:{},此时若直接
|
||||
// 反序列化会得到零值元素 + nil error,调用方会把空数据当成功处理;
|
||||
// 这里对 null/空对象显式报错。
|
||||
func openList[T any](raw json.RawMessage) ([]T, error) {
|
||||
trimmed := bytes.TrimSpace(raw)
|
||||
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) || bytes.Equal(trimmed, []byte("{}")) {
|
||||
return nil, fmt.Errorf("115: data 为空(%s)", string(trimmed))
|
||||
}
|
||||
var single T
|
||||
if err := json.Unmarshal(raw, &single); err == nil {
|
||||
return []T{single}, nil
|
||||
@@ -324,12 +399,16 @@ func openList[T any](raw json.RawMessage) ([]T, error) {
|
||||
return nil, fmt.Errorf("115: data 既不是对象也不是数组")
|
||||
}
|
||||
|
||||
// openFirstList 取 data 的第一个元素。
|
||||
// openFirstList 取 data 的第一个元素;data 为空(null/空数组)时返回显式错误,
|
||||
// 避免调用方拿到 (nil, nil) 后解引用空指针。
|
||||
func openFirstList[T any](raw json.RawMessage) (*T, error) {
|
||||
items, err := openList[T](raw)
|
||||
if err != nil || len(items) == 0 {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil, fmt.Errorf("115: data 为空数组")
|
||||
}
|
||||
return &items[0], nil
|
||||
}
|
||||
|
||||
|
||||
@@ -317,13 +317,18 @@ func appendCallbackParams(rawURL string, params url.Values) (string, error) {
|
||||
return callbackURL.String(), nil
|
||||
}
|
||||
|
||||
// oauthHTTPClient 是 OAuth 授权服务专用 HTTP 客户端。http.DefaultClient 无超时,
|
||||
// 授权服务无响应时会永久阻塞授权/轮询协程,这里统一 30s 超时(ctx 仍经
|
||||
// NewRequestWithContext 传导,可提前取消)。
|
||||
var oauthHTTPClient = &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
func httpGetJSON(ctx context.Context, endpoint string) (map[string]any, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", DefaultUA)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := oauthHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -300,6 +300,11 @@ func (c *OpenClient) GetQrCode() (*QrCodeDataReturn, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 关键字段缺失时显式报错:空 uid/sign 会导致后续扫码轮询必然失败,
|
||||
// 不能把残缺响应当成功返回给界面。
|
||||
if code.Uid == "" || code.Sign == "" {
|
||||
return nil, fmt.Errorf("115: 设备码响应缺少 uid/sign,无法发起扫码授权")
|
||||
}
|
||||
return &QrCodeDataReturn{QrCodeData: *code, CodeVerifier: codeVerifier}, nil
|
||||
}
|
||||
|
||||
@@ -352,6 +357,10 @@ func (c *OpenClient) GetToken(qrCode *QrCodeDataReturn) (*TokenData, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 空凭证绝不能 SetAuthToken 后当成功返回:界面会显示"授权成功"但账号不可用
|
||||
if token.AccessToken == "" || token.RefreshToken == "" {
|
||||
return nil, fmt.Errorf("115: 设备码换 token 返回空凭证(access_token/refresh_token 缺失)")
|
||||
}
|
||||
c.SetAuthToken(token.AccessToken, token.RefreshToken)
|
||||
return token, nil
|
||||
}
|
||||
@@ -359,11 +368,30 @@ func (c *OpenClient) GetToken(qrCode *QrCodeDataReturn) (*TokenData, error) {
|
||||
// RefreshToken 刷新访问令牌。
|
||||
func (c *OpenClient) RefreshToken(refreshToken string) (*TokenData, error) {
|
||||
if refreshToken == "" {
|
||||
refreshToken = c.RefreshTokenStr
|
||||
refreshToken = c.currentRefreshToken()
|
||||
}
|
||||
if refreshToken == "" {
|
||||
return nil, fmt.Errorf("没有可用的 refresh_token")
|
||||
}
|
||||
token, err := c.doRefreshToken(refreshToken)
|
||||
if err != nil {
|
||||
// refresh_token 已失效时清空内存令牌(提示需重新授权)
|
||||
if IsRefreshTokenDead(err) {
|
||||
c.SetAuthToken("", "")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if token.AccessToken == "" || token.RefreshToken == "" {
|
||||
return nil, fmt.Errorf("115: 刷新返回空凭证(access_token/refresh_token 缺失)")
|
||||
}
|
||||
c.SetAuthToken(token.AccessToken, token.RefreshToken)
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// doRefreshToken 调用 115 刷新接口换取新令牌,不修改客户端内存状态;
|
||||
// 拆出无状态方法供 tryRefreshTokenLocked(已持 tokenMu 写锁)复用,
|
||||
// 避免在持锁期间重入 SetAuthToken 造成死锁。
|
||||
func (c *OpenClient) doRefreshToken(refreshToken string) (*TokenData, error) {
|
||||
params := map[string]string{"refresh_token": refreshToken}
|
||||
resp, err := c.doJSON(context.Background(), "POST", PassportAPIBase+"/open/refreshToken", params, false, 0)
|
||||
if err != nil && resp == nil {
|
||||
@@ -373,18 +401,9 @@ func (c *OpenClient) RefreshToken(refreshToken string) (*TokenData, error) {
|
||||
return nil, err
|
||||
}
|
||||
if !resp.State {
|
||||
apiErr := NewOpenAPIResponseError(resp.Code, resp.Errno, resp.Message, resp.Error, "115 开放平台刷新访问凭证失败")
|
||||
if IsRefreshTokenDead(apiErr) {
|
||||
c.SetAuthToken("", "")
|
||||
}
|
||||
return nil, apiErr
|
||||
return nil, NewOpenAPIResponseError(resp.Code, resp.Errno, resp.Message, resp.Error, "115 开放平台刷新访问凭证失败")
|
||||
}
|
||||
token, err := openFirstList[TokenData](resp.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.SetAuthToken(token.AccessToken, token.RefreshToken)
|
||||
return token, nil
|
||||
return openFirstList[TokenData](resp.Data)
|
||||
}
|
||||
|
||||
// ─── 用户信息 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
@@ -106,14 +107,23 @@ func (u *OSSMultipartUploader) UploadFile(ctx context.Context, input OSSMultipar
|
||||
return result.CallbackResult, nil
|
||||
}
|
||||
|
||||
// UploadedPart 是 OSS 已上传分片的定位信息(断点续传时复用 ETag 用)。
|
||||
type UploadedPart struct {
|
||||
PartNumber int32
|
||||
Size int64
|
||||
ETag string
|
||||
}
|
||||
|
||||
// UploadFileWithResult 上传文件并返回 multipart 结果。
|
||||
func (u *OSSMultipartUploader) UploadFileWithResult(ctx context.Context, input OSSMultipartUploadInput) (OSSMultipartUploadResult, error) {
|
||||
// 任一失败路径(分片上传失败 / callback 校验失败 / Complete 失败 / 文件打开失败等)
|
||||
// 都会经 defer 统一 AbortMultipartUpload 丢弃本次 Initiate 出的 multipart
|
||||
// (abort 失败仅记日志),避免 OSS 分片永久泄漏;成功路径不 Abort。
|
||||
func (u *OSSMultipartUploader) UploadFileWithResult(ctx context.Context, input OSSMultipartUploadInput) (result OSSMultipartUploadResult, err error) {
|
||||
if input.PartRetryMax <= 0 {
|
||||
input.PartRetryMax = 3
|
||||
}
|
||||
partSize := input.PartSize
|
||||
totalParts := 0
|
||||
var err error
|
||||
if partSize <= 0 {
|
||||
partSize, totalParts, err = CalculateMultipartPartSize(input.FileSize)
|
||||
if err != nil {
|
||||
@@ -124,28 +134,45 @@ func (u *OSSMultipartUploader) UploadFileWithResult(ctx context.Context, input O
|
||||
}
|
||||
|
||||
uploadId := input.UploadId
|
||||
if uploadId == "" {
|
||||
initResult, err := u.client.InitiateMultipartUpload(ctx, &oss.InitiateMultipartUploadRequest{
|
||||
// ownUploadId 标记 uploadId 是否为本调用 Initiate 出来的:仅自建的
|
||||
// multipart 在失败时由本函数 Abort;调用方显式传入的 uploadId(断点续传)
|
||||
// 失败后保留现场,由调用方决定重试或清理。
|
||||
ownUploadId := uploadId == ""
|
||||
if ownUploadId {
|
||||
initResult, initErr := u.client.InitiateMultipartUpload(ctx, &oss.InitiateMultipartUploadRequest{
|
||||
Bucket: oss.Ptr(input.Bucket),
|
||||
Key: oss.Ptr(input.Object),
|
||||
RequestCommon: oss.RequestCommon{
|
||||
Parameters: map[string]string{"sequential": "1"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return OSSMultipartUploadResult{}, fmt.Errorf("初始化 OSS multipart 失败:%w", err)
|
||||
if initErr != nil {
|
||||
return OSSMultipartUploadResult{}, fmt.Errorf("初始化 OSS multipart 失败:%w", initErr)
|
||||
}
|
||||
if initResult.UploadId == nil || *initResult.UploadId == "" {
|
||||
return OSSMultipartUploadResult{}, fmt.Errorf("初始化 OSS multipart 返回空 upload_id")
|
||||
}
|
||||
uploadId = *initResult.UploadId
|
||||
}
|
||||
defer func() {
|
||||
if err == nil || !ownUploadId || uploadId == "" {
|
||||
return
|
||||
}
|
||||
// 失败路径统一 Abort 丢弃已上传分片;ctx 可能已取消,脱离其取消信号尽力清理
|
||||
abortCtx := context.WithoutCancel(ctx)
|
||||
if _, abortErr := u.client.AbortMultipartUpload(abortCtx, &oss.AbortMultipartUploadRequest{
|
||||
Bucket: oss.Ptr(input.Bucket),
|
||||
Key: oss.Ptr(input.Object),
|
||||
UploadId: oss.Ptr(uploadId),
|
||||
}); abortErr != nil {
|
||||
log.Printf("115: 中止 OSS multipart 失败(upload_id=%s,可能残留分片):%v", uploadId, abortErr)
|
||||
}
|
||||
}()
|
||||
|
||||
existingPartMap := make(map[int32]int64)
|
||||
existingParts, err := u.ListUploadedParts(ctx, input.Bucket, input.Object, uploadId)
|
||||
if err == nil {
|
||||
existingPartMap := make(map[int32]UploadedPart)
|
||||
if existingParts, listErr := u.ListUploadedParts(ctx, input.Bucket, input.Object, uploadId); listErr == nil {
|
||||
for _, part := range existingParts {
|
||||
existingPartMap[part.PartNumber] = part.Size
|
||||
existingPartMap[part.PartNumber] = part
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,13 +191,20 @@ func (u *OSSMultipartUploader) UploadFileWithResult(ctx context.Context, input O
|
||||
if length < 0 {
|
||||
length = 0
|
||||
}
|
||||
if existingSize, ok := existingPartMap[int32(partNumber)]; ok && existingSize == length {
|
||||
// 断点续传:分片已完整上传(大小一致即代表分片大小未变)时直接复用
|
||||
// ListParts 返回的 ETag,跳过重传,也不再重复累加统计
|
||||
if existing, ok := existingPartMap[int32(partNumber)]; ok && existing.Size == length && existing.ETag != "" {
|
||||
uploadedBytes += length
|
||||
uploadedParts++
|
||||
completeParts = append(completeParts, oss.UploadPart{
|
||||
PartNumber: int32(partNumber),
|
||||
ETag: oss.Ptr(existing.ETag),
|
||||
})
|
||||
continue
|
||||
}
|
||||
etag, err := u.uploadPartWithRetry(ctx, input, uploadId, int32(partNumber), file, offset, length)
|
||||
if err != nil {
|
||||
return OSSMultipartUploadResult{}, err
|
||||
etag, uploadErr := u.uploadPartWithRetry(ctx, input, uploadId, int32(partNumber), file, offset, length)
|
||||
if uploadErr != nil {
|
||||
return OSSMultipartUploadResult{}, uploadErr
|
||||
}
|
||||
uploadedBytes += length
|
||||
uploadedParts++
|
||||
@@ -224,29 +258,34 @@ func (u *OSSMultipartUploader) UploadFileWithResult(ctx context.Context, input O
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListUploadedParts 查询 OSS 已上传分片。
|
||||
func (u *OSSMultipartUploader) ListUploadedParts(ctx context.Context, bucket, object, uploadId string) ([]struct {
|
||||
PartNumber int32
|
||||
Size int64
|
||||
}, error) {
|
||||
parts := []struct {
|
||||
PartNumber int32
|
||||
Size int64
|
||||
}{}
|
||||
result, err := u.client.ListParts(ctx, &oss.ListPartsRequest{
|
||||
Bucket: oss.Ptr(bucket),
|
||||
Key: oss.Ptr(object),
|
||||
UploadId: oss.Ptr(uploadId),
|
||||
MaxParts: 1000,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询 OSS 已上传分片失败:%w", err)
|
||||
}
|
||||
for _, part := range result.Parts {
|
||||
parts = append(parts, struct {
|
||||
PartNumber int32
|
||||
Size int64
|
||||
}{PartNumber: part.PartNumber, Size: part.Size})
|
||||
// ListUploadedParts 查询 OSS 已上传分片(MaxParts 上限 1000,超过时按
|
||||
// NextPartNumberMarker 自动翻页取全量,否则断点续传只能看到前 1000 片)。
|
||||
func (u *OSSMultipartUploader) ListUploadedParts(ctx context.Context, bucket, object, uploadId string) ([]UploadedPart, error) {
|
||||
parts := []UploadedPart{}
|
||||
var marker int32
|
||||
for {
|
||||
result, err := u.client.ListParts(ctx, &oss.ListPartsRequest{
|
||||
Bucket: oss.Ptr(bucket),
|
||||
Key: oss.Ptr(object),
|
||||
UploadId: oss.Ptr(uploadId),
|
||||
MaxParts: 1000,
|
||||
PartNumberMarker: marker,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询 OSS 已上传分片失败:%w", err)
|
||||
}
|
||||
for _, part := range result.Parts {
|
||||
etag := ""
|
||||
if part.ETag != nil {
|
||||
etag = *part.ETag
|
||||
}
|
||||
parts = append(parts, UploadedPart{PartNumber: part.PartNumber, Size: part.Size, ETag: etag})
|
||||
}
|
||||
if !result.IsTruncated || result.NextPartNumberMarker <= marker {
|
||||
// 防御:marker 不前进时终止循环,避免异常响应导致死循环
|
||||
break
|
||||
}
|
||||
marker = result.NextPartNumberMarker
|
||||
}
|
||||
return parts, nil
|
||||
}
|
||||
|
||||
@@ -262,7 +262,10 @@ func (c *OpenClient) Upload(ctx context.Context, filePath, parentCID, signKey, s
|
||||
}
|
||||
switch status {
|
||||
case UploadInitStatusRapidUploaded:
|
||||
// 秒传成功
|
||||
// 秒传成功:必须带远端文件定位信息,否则视为异常响应
|
||||
if initResult.FileId == "" || initResult.PickCode == "" {
|
||||
return nil, fmt.Errorf("115: 秒传成功但缺少 file_id/pick_code(status=%d)", status)
|
||||
}
|
||||
return &UploadCompleteResult{FileId: initResult.FileId, PickCode: initResult.PickCode}, nil
|
||||
case UploadInitStatusSignFailed:
|
||||
return nil, fmt.Errorf("115: 签名验证后失败")
|
||||
@@ -271,7 +274,8 @@ func (c *OpenClient) Upload(ctx context.Context, filePath, parentCID, signKey, s
|
||||
case UploadInitStatusNeedUpload:
|
||||
// 真实上传:OSS multipart
|
||||
default:
|
||||
return &UploadCompleteResult{FileId: initResult.FileId, PickCode: initResult.PickCode}, nil
|
||||
// 未知状态不能当成功返回(会静默丢文件),显式报错便于排查
|
||||
return nil, fmt.Errorf("115: 未知的 upload/init 状态 %d", status)
|
||||
}
|
||||
|
||||
if initResult.Bucket == "" || initResult.Object == "" {
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
package cloud115
|
||||
|
||||
import "math/rand"
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
const randCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
// RandomString 生成指定长度的随机字符串(PKCE code_verifier 等)。
|
||||
// RandomString 生成指定长度的密码学安全随机字符串(PKCE code_verifier、
|
||||
// OAuth state 等安全敏感场景)。必须使用 crypto/rand:math/rand 未播种时
|
||||
// 序列可预测,会造成 PKCE 防御失效。
|
||||
func RandomString(length int) string {
|
||||
b := make([]byte, length)
|
||||
max := big.NewInt(int64(len(randCharset)))
|
||||
for i := range b {
|
||||
b[i] = randCharset[rand.Intn(len(randCharset))]
|
||||
n, err := rand.Int(rand.Reader, max)
|
||||
if err != nil {
|
||||
// 仅在系统熵源不可用时发生;静默降级为弱随机不可接受,直接暴露
|
||||
panic(fmt.Errorf("115: 生成安全随机字符串失败:%w", err))
|
||||
}
|
||||
b[i] = randCharset[n.Int64()]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user