feat: Implement direct upload functionality for desktop client releases
Desktop Client Build / Resolve Build Metadata (push) Successful in 52s
Frontend CI / Frontend (push) Successful in 3m49s
Backend CI / Backend (push) Successful in 16m10s
Desktop Client Build / Build Desktop Client (push) Successful in 23m22s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 31s

- Added new endpoints for initiating and completing direct uploads of desktop client packages.
- Introduced request and response structures for direct upload operations.
- Enhanced the upload process to support SHA256 and Content-MD5 validation.
- Updated the frontend to reflect changes in upload button states and progress indicators.
- Modified object storage clients to support presigned PUT URLs with content type and MD5 headers.
- Added tests for direct upload functionality and ensured existing upload processes remain intact.
This commit is contained in:
2026-06-20 12:44:28 +08:00
parent f26802c76e
commit 62d0b22988
15 changed files with 884 additions and 58 deletions
@@ -153,6 +153,23 @@ type publishTaskListOwner struct {
UserID int64
}
const publishTaskVisibleRecordFilterSQL = `
AND (
NOT (t.payload ? 'publish_record_id')
OR EXISTS (
SELECT 1
FROM publish_records pr
WHERE pr.tenant_id = t.tenant_id
AND pr.id = CASE
WHEN (t.payload->>'publish_record_id') ~ '^[0-9]+$'
THEN (t.payload->>'publish_record_id')::bigint
ELSE NULL
END
AND pr.deleted_at IS NULL
)
)
`
func (s *DesktopTaskService) Lease(ctx context.Context, client *repository.DesktopClient, req LeaseDesktopTaskRequest, routeTaskID *uuid.UUID) (*LeaseDesktopTaskResponse, error) {
if client == nil {
return nil, response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required")
@@ -1225,6 +1242,24 @@ func (s *DesktopTaskService) listPublishTasksByStatuses(
offset int,
orderBy string,
) ([]DesktopTaskView, error) {
query, args := buildListPublishTasksByStatusesQuery(owner, statuses, title, limit, offset, orderBy)
rows, err := s.pool.Query(ctx, query, args...)
if err != nil {
return nil, response.ErrInternal(50109, "desktop_publish_tasks_query_failed", "failed to list desktop publish tasks")
}
defer rows.Close()
return scanDesktopTaskRows(rows)
}
func buildListPublishTasksByStatusesQuery(
owner publishTaskListOwner,
statuses []string,
title string,
limit int,
offset int,
orderBy string,
) (string, []any) {
query := `
SELECT
t.desktop_id,
@@ -1263,7 +1298,7 @@ func (s *DesktopTaskService) listPublishTasksByStatuses(
$5 = ''
OR COALESCE(t.payload->>'title', j.title, '') ILIKE '%' || $5 || '%'
)
`
` + publishTaskVisibleRecordFilterSQL
args := []any{owner.TenantID, owner.WorkspaceID, owner.UserID, statuses, title}
if strings.TrimSpace(orderBy) != "" {
@@ -1273,14 +1308,7 @@ func (s *DesktopTaskService) listPublishTasksByStatuses(
query += "\nLIMIT $6 OFFSET $7"
args = append(args, limit, offset)
}
rows, err := s.pool.Query(ctx, query, args...)
if err != nil {
return nil, response.ErrInternal(50109, "desktop_publish_tasks_query_failed", "failed to list desktop publish tasks")
}
defer rows.Close()
return scanDesktopTaskRows(rows)
return query, args
}
func (s *DesktopTaskService) countPublishTasksByStatuses(
@@ -1290,7 +1318,20 @@ func (s *DesktopTaskService) countPublishTasksByStatuses(
title string,
) (int, error) {
var count int
err := s.pool.QueryRow(ctx, `
query, args := buildCountPublishTasksByStatusesQuery(owner, statuses, title)
err := s.pool.QueryRow(ctx, query, args...).Scan(&count)
if err != nil {
return 0, response.ErrInternal(50109, "desktop_publish_tasks_query_failed", "failed to list desktop publish tasks")
}
return count, nil
}
func buildCountPublishTasksByStatusesQuery(
owner publishTaskListOwner,
statuses []string,
title string,
) (string, []any) {
query := `
SELECT COUNT(1)
FROM desktop_tasks AS t
JOIN desktop_publish_jobs AS j
@@ -1306,11 +1347,8 @@ func (s *DesktopTaskService) countPublishTasksByStatuses(
$5 = ''
OR COALESCE(t.payload->>'title', j.title, '') ILIKE '%' || $5 || '%'
)
`, owner.TenantID, owner.WorkspaceID, owner.UserID, statuses, title).Scan(&count)
if err != nil {
return 0, response.ErrInternal(50109, "desktop_publish_tasks_query_failed", "failed to list desktop publish tasks")
}
return count, nil
` + publishTaskVisibleRecordFilterSQL
return query, []any{owner.TenantID, owner.WorkspaceID, owner.UserID, statuses, title}
}
func scanDesktopTaskRows(rows pgx.Rows) ([]DesktopTaskView, error) {
@@ -149,6 +149,60 @@ func TestIsComplianceInvalidArticleVersionError(t *testing.T) {
}
}
func TestBuildListPublishTasksByStatusesQueryHidesDeletedPublishRecords(t *testing.T) {
t.Parallel()
query, args := buildListPublishTasksByStatusesQuery(
publishTaskListOwner{TenantID: 7, WorkspaceID: 11, UserID: 23},
[]string{"succeeded", "failed"},
"合肥",
10,
20,
"t.updated_at DESC",
)
normalized := normalizeSQLForDesktopTaskTest(query)
for _, fragment := range []string{
"NOT (t.payload ? 'publish_record_id') OR EXISTS",
"FROM publish_records pr",
"pr.tenant_id = t.tenant_id",
"t.payload->>'publish_record_id')::bigint",
"pr.deleted_at IS NULL",
"LIMIT $6 OFFSET $7",
} {
if !strings.Contains(normalized, fragment) {
t.Fatalf("list publish tasks query missing %q: %s", fragment, normalized)
}
}
if len(args) != 7 {
t.Fatalf("args len = %d, want 7", len(args))
}
}
func TestBuildCountPublishTasksByStatusesQueryHidesDeletedPublishRecords(t *testing.T) {
t.Parallel()
query, args := buildCountPublishTasksByStatusesQuery(
publishTaskListOwner{TenantID: 7, WorkspaceID: 11, UserID: 23},
[]string{"succeeded", "failed"},
"",
)
normalized := normalizeSQLForDesktopTaskTest(query)
for _, fragment := range []string{
"NOT (t.payload ? 'publish_record_id') OR EXISTS",
"FROM publish_records pr",
"pr.deleted_at IS NULL",
} {
if !strings.Contains(normalized, fragment) {
t.Fatalf("count publish tasks query missing %q: %s", fragment, normalized)
}
}
if len(args) != 5 {
t.Fatalf("args len = %d, want 5", len(args))
}
}
func recoverDesktopTaskSelectColumns(query string) []string {
re := regexp.MustCompile(`(?is)select\s+(.*?)\s+from\s+desktop_tasks`)
match := re.FindStringSubmatch(query)
@@ -165,3 +219,7 @@ func recoverDesktopTaskSelectColumns(query string) []string {
}
return columns
}
func normalizeSQLForDesktopTaskTest(sql string) string {
return strings.Join(strings.Fields(sql), " ")
}