refactor(uploads): drop legacy multipart code paths
Backend CI / Test (push) Has been cancelled
Backend CI / Contract Tests (push) Has been cancelled
Backend CI / Build (push) Has been cancelled
Backend CI / Lint (push) Has been cancelled
Backend CI / Secret Scanning (push) Has been cancelled

The presigned-URL upload flow (POST /api/uploads/presign + direct B2 POST
+ upload_ids[] in entity creation) is now the only image upload path. The
legacy multipart routes and DTO fields used by older clients are removed:

Removed:
  - POST /api/uploads/image/        (legacy multipart upload → URL)
  - POST /api/uploads/document/     (legacy multipart upload → URL)
  - POST /api/uploads/completion/   (legacy multipart upload → URL)
  - Multipart branch in POST /api/task-completions/ (now JSON-only)
  - CreateTaskCompletionRequest.ImageURLs DTO field
  - UpdateTaskCompletionRequest.ImageURLs DTO field
  - CreateDocumentRequest.ImageURLs DTO field
  - Service-layer ImageURLs loops in task_service.CreateCompletion,
    task_service.UpdateCompletion, document_service.CreateDocument
  - Tests exercising the removed paths
  - Now-unused imports (strings/time/decimal) in task_handler.go

Kept:
  - DELETE /api/uploads/  (orphan-cleanup endpoint, still useful)
  - POST /api/uploads/presign/  (the new path)
  - POST /api/documents/:id/images/  (uses storage_service.Upload directly,
    same multipart pattern but separate code path; deferred for now)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-05-01 15:19:21 -07:00
parent 29c9014a33
commit b7f83293b8
9 changed files with 43 additions and 261 deletions
+14 -62
View File
@@ -3,11 +3,8 @@ package handlers
import (
"net/http"
"strconv"
"strings"
"time"
"github.com/labstack/echo/v4"
"github.com/shopspring/decimal"
"github.com/treytartt/honeydue-api/internal/apperrors"
"github.com/treytartt/honeydue-api/internal/dto/requests"
@@ -393,7 +390,18 @@ func (h *TaskHandler) GetCompletion(c echo.Context) error {
}
// CreateCompletion handles POST /api/task-completions/
// Supports both JSON and multipart form data (for image uploads)
//
// JSON-only. Image attachments arrive via the presigned-URL flow:
//
// 1. Client POSTs /api/uploads/presign for each image and uploads bytes
// directly to B2 using the returned policy.
// 2. Client POSTs the resulting upload_ids[] in this request body.
// 3. The service claims those pending_uploads rows and creates the
// associated TaskCompletionImage rows.
//
// The legacy multipart path (with the API server proxying image bytes)
// was removed alongside the 1 MB BodyLimit middleware that was rejecting
// it anyway. See deploy-k3s/manifests/b2-lifecycle.md.
func (h *TaskHandler) CreateCompletion(c echo.Context) error {
user, err := middleware.MustGetAuthUser(c)
if err != nil {
@@ -402,65 +410,9 @@ func (h *TaskHandler) CreateCompletion(c echo.Context) error {
userNow := middleware.GetUserNow(c)
var req requests.CreateTaskCompletionRequest
contentType := c.Request().Header.Get("Content-Type")
// Check if this is a multipart form request (image upload)
if strings.HasPrefix(contentType, "multipart/form-data") {
// Parse multipart form
if err := c.Request().ParseMultipartForm(32 << 20); err != nil { // 32MB max
return apperrors.BadRequest("error.failed_to_parse_form")
}
// Parse task_id (required)
taskIDStr := c.FormValue("task_id")
if taskIDStr == "" {
return apperrors.BadRequest("error.task_id_required")
}
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_task_id_value")
}
req.TaskID = uint(taskID)
// Parse notes (optional)
req.Notes = c.FormValue("notes")
// Parse actual_cost (optional)
if costStr := c.FormValue("actual_cost"); costStr != "" {
cost, err := decimal.NewFromString(costStr)
if err == nil {
req.ActualCost = &cost
}
}
// Parse completed_at (optional)
if completedAtStr := c.FormValue("completed_at"); completedAtStr != "" {
if t, err := time.Parse(time.RFC3339, completedAtStr); err == nil {
req.CompletedAt = &t
}
}
// Handle multiple image uploads from various field names
if h.storageService != nil && c.Request().MultipartForm != nil {
for _, fieldName := range []string{"images", "image", "photo", "files"} {
files := c.Request().MultipartForm.File[fieldName]
for _, file := range files {
result, err := h.storageService.Upload(c.Request().Context(), file, "completions")
if err != nil {
return apperrors.BadRequest("error.failed_to_upload_image")
}
req.ImageURLs = append(req.ImageURLs, result.URL)
}
}
}
} else {
// Standard JSON request
if err := c.Bind(&req); err != nil {
return apperrors.BadRequest("error.invalid_request")
}
if err := c.Bind(&req); err != nil {
return apperrors.BadRequest("error.invalid_request")
}
if err := c.Validate(&req); err != nil {
return err
}