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
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:
@@ -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), " ")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user