feat: add chunked desktop client release uploads
Deployment Config CI / Deployment Config (push) Successful in 28s
Frontend CI / Frontend (push) Successful in 2m12s
Backend CI / Backend (push) Successful in 14m44s

This commit is contained in:
2026-06-01 01:31:54 +08:00
parent c5da0cf507
commit 76df672133
14 changed files with 1195 additions and 57 deletions
+15 -2
View File
@@ -72,12 +72,25 @@ func (c *aliyunClient) Validate() error {
}
func (c *aliyunClient) PutBytes(ctx context.Context, objectKey string, content []byte, contentType string) error {
return c.PutReader(ctx, objectKey, bytes.NewReader(content), int64(len(content)), contentType)
}
func (c *aliyunClient) PutReader(
ctx context.Context,
objectKey string,
reader io.Reader,
size int64,
contentType string,
) error {
if err := c.Validate(); err != nil {
return err
}
if strings.TrimSpace(objectKey) == "" {
return fmt.Errorf("object key is required")
}
if reader == nil {
return fmt.Errorf("object reader is required")
}
exists, err := c.client.IsBucketExist(c.bucket)
if err != nil {
@@ -113,11 +126,11 @@ func (c *aliyunClient) PutBytes(ctx context.Context, objectKey string, content [
if err != nil {
return err
}
if err := bucket.PutObject(objectKey, bytes.NewReader(content), options...); err != nil {
if err := bucket.PutObject(objectKey, reader, options...); err != nil {
c.logError(ctx, "aliyun put object failed",
zap.String("bucket", c.bucket),
zap.String("object_key", objectKey),
zap.Int("content_size", len(content)),
zap.Int64("content_size", size),
zap.String("object_acl", strings.TrimSpace(c.cfg.ObjectACL)),
zap.String("content_type", contentType),
zap.String("aliyun_error_code", aliyunErrorCode(err)),
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"io"
"strings"
"time"
@@ -27,6 +28,10 @@ type Client interface {
DownloadURL(objectKey string) (string, error)
}
type ReaderClient interface {
PutReader(ctx context.Context, objectKey string, reader io.Reader, size int64, contentType string) error
}
type ManagementClient interface {
Client
List(ctx context.Context, in ListInput) (*ListResult, error)
@@ -106,6 +111,10 @@ func (c disabledClient) PutBytes(context.Context, string, []byte, string) error
return c.Validate()
}
func (c disabledClient) PutReader(context.Context, string, io.Reader, int64, string) error {
return c.Validate()
}
func (c disabledClient) GetBytes(context.Context, string) ([]byte, error) {
return nil, c.Validate()
}
+15 -2
View File
@@ -103,6 +103,16 @@ func (c *minioClient) PutBytes(
objectKey string,
content []byte,
contentType string,
) error {
return c.PutReader(ctx, objectKey, bytes.NewReader(content), int64(len(content)), contentType)
}
func (c *minioClient) PutReader(
ctx context.Context,
objectKey string,
reader io.Reader,
size int64,
contentType string,
) error {
if err := c.Validate(); err != nil {
return err
@@ -110,6 +120,9 @@ func (c *minioClient) PutBytes(
if strings.TrimSpace(objectKey) == "" {
return fmt.Errorf("object key is required")
}
if reader == nil {
return fmt.Errorf("object reader is required")
}
exists, err := c.client.BucketExists(ctx, c.bucket)
if err != nil {
@@ -131,14 +144,14 @@ func (c *minioClient) PutBytes(
}
}
_, err = c.client.PutObject(ctx, c.bucket, objectKey, bytes.NewReader(content), int64(len(content)), minio.PutObjectOptions{
_, err = c.client.PutObject(ctx, c.bucket, objectKey, reader, size, minio.PutObjectOptions{
ContentType: contentType,
})
if err != nil {
c.logError(ctx, "minio put object failed",
zap.String("bucket", c.bucket),
zap.String("object_key", objectKey),
zap.Int("content_size", len(content)),
zap.Int64("content_size", size),
zap.Error(err),
)
return fmt.Errorf("put minio object: %w", err)
@@ -2,6 +2,7 @@ package objectstorage
import (
"context"
"io"
"sync"
)
@@ -31,6 +32,19 @@ func (c *ReloadableClient) PutBytes(ctx context.Context, objectKey string, conte
return c.snapshot().PutBytes(ctx, objectKey, content, contentType)
}
func (c *ReloadableClient) PutReader(ctx context.Context, objectKey string, reader io.Reader, size int64, contentType string) error {
client := c.snapshot()
readerClient, ok := client.(ReaderClient)
if !ok {
content, err := io.ReadAll(reader)
if err != nil {
return err
}
return client.PutBytes(ctx, objectKey, content, contentType)
}
return readerClient.PutReader(ctx, objectKey, reader, size, contentType)
}
func (c *ReloadableClient) GetBytes(ctx context.Context, objectKey string) ([]byte, error) {
return c.snapshot().GetBytes(ctx, objectKey)
}
+15 -11
View File
@@ -99,17 +99,21 @@ var routeDocs = map[string]routeDoc{
"DELETE /api/ops/object-storage/folders": {"删除对象存储目录", "按前缀递归删除目录下对象。"},
// --- Ops:桌面客户端版本 ---
"GET /api/ops/desktop-client/releases": {"桌面客户端版本列表", "分页查询桌面客户端版本配置。"},
"POST /api/ops/desktop-client/releases/upload": {"上传桌面客户端包", "Multipart 上传桌面客户端安装包或更新包。"},
"POST /api/ops/desktop-client/releases": {"新建桌面客户端版本", "创建桌面客户端版本配置。"},
"PATCH /api/ops/desktop-client/releases/:id": {"更新桌面客户端版本", "更新桌面客户端版本配置。"},
"GET /api/ops/desktop-client/releases/:id/download-url": {"解析桌面客户端下载地址", "解析指定桌面客户端版本的下载地址。"},
"POST /api/ops/desktop-client/releases/:id/enabled": {"切换桌面客户端版本状态", "启用或禁用指定桌面客户端版本。"},
"DELETE /api/ops/desktop-client/releases/:id": {"删除桌面客户端版本", "删除指定桌面客户端版本配置。"},
"GET /api/ops/desktop-client/bug-reports": {"客户端 Bug 列表", "分页查询桌面客户端崩溃和用户反馈记录。"},
"GET /api/ops/desktop-client/bug-reports/:id": {"客户端 Bug 详情", "查看桌面客户端崩溃元数据、运行快照和 dump 对象。"},
"PATCH /api/ops/desktop-client/bug-reports/:id": {"更新客户端 Bug 状态", "调整客户端 Bug 状态、级别和处理备注。"},
"GET /api/ops/desktop-client/bug-reports/:id/dump-url": {"解析客户端 dump 下载地址", "生成指定客户端 Bug dump 文件的短期下载地址。"},
"GET /api/ops/desktop-client/releases": {"桌面客户端版本列表", "分页查询桌面客户端版本配置。"},
"POST /api/ops/desktop-client/releases/upload": {"上传桌面客户端包(兼容)", "兼容旧版单请求 Multipart 上传;新版前端使用分片上传。"},
"POST /api/ops/desktop-client/release-uploads": {"初始化客户端包分片上传", "创建分片上传会话,返回 upload_id、分片大小和分片数量。"},
"POST /api/ops/desktop-client/release-uploads/:upload_id/chunks/:chunk_index": {"上传客户端包分片", "Multipart 上传单个客户端安装包或更新包分片。"},
"POST /api/ops/desktop-client/release-uploads/:upload_id/complete": {"完成客户端包分片上传", "校验全部分片,流式写入 OSS,并返回 Object Key、文件大小和 SHA256。"},
"DELETE /api/ops/desktop-client/release-uploads/:upload_id": {"取消客户端包分片上传", "取消分片上传会话并清理临时分片。"},
"POST /api/ops/desktop-client/releases": {"新建客户端版本", "创建桌面客户端版本配置。"},
"PATCH /api/ops/desktop-client/releases/:id": {"更新客户端版本", "更新桌面客户端版本配置。"},
"GET /api/ops/desktop-client/releases/:id/download-url": {"解析桌面客户端下载地址", "解析指定桌面客户端版本的下载地址。"},
"POST /api/ops/desktop-client/releases/:id/enabled": {"切换客户端版本状态", "启用或禁用指定桌面客户端版本。"},
"DELETE /api/ops/desktop-client/releases/:id": {"删除客户端版本", "删除指定桌面客户端版本配置。"},
"GET /api/ops/desktop-client/bug-reports": {"客户端 Bug 列表", "分页查询桌面客户端崩溃和用户反馈记录。"},
"GET /api/ops/desktop-client/bug-reports/:id": {"客户端 Bug 详情", "查看桌面客户端崩溃元数据、运行快照和 dump 对象。"},
"PATCH /api/ops/desktop-client/bug-reports/:id": {"更新客户端 Bug 状态", "调整客户端 Bug 状态、级别和处理备注。"},
"GET /api/ops/desktop-client/bug-reports/:id/dump-url": {"解析客户端 dump 下载地址", "生成指定客户端 Bug dump 文件的短期下载地址。"},
// --- Ops:合规 ---
"GET /api/ops/compliance/dictionaries": {"合规词库列表", "分页查询内容合规词库。"},
@@ -419,6 +419,7 @@ func isMultipartRoute(route gin.RouteInfo) bool {
path == "/api/ops/site-domain-mappings/import" ||
path == "/api/ops/object-storage/objects/upload" ||
path == "/api/ops/desktop-client/releases/upload" ||
path == "/api/ops/desktop-client/release-uploads/:upload_id/chunks/:chunk_index" ||
path == "/api/tenant/knowledge/items/file" ||
path == "/api/tenant/kol/manage/profile/avatar" ||
path == "/api/tenant/articles/:id/images"