From f2fbbcf40a02658c8d63717842fb856662e00a73 Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 19 Oct 2026 15:58:51 +0200 Subject: [PATCH] feat(attachments)!: always return the original from the v2 download route Address review comment: previews moved to their own route, so the download route no longer takes preview_size. BREAKING CHANGE: the v2 attachment download route ignores preview_size. Request previews from GET /tasks/{task}/attachments/{attachment}/preview?size= instead. --- frontend/src/client/generated/sdk.gen.ts | 2 +- frontend/src/client/generated/types.gen.ts | 11 +---------- pkg/routes/api/v2/task_attachments.go | 21 +++------------------ pkg/webtests/huma_task_attachment_test.go | 15 +++++++++++++++ 4 files changed, 20 insertions(+), 29 deletions(-) diff --git a/frontend/src/client/generated/sdk.gen.ts b/frontend/src/client/generated/sdk.gen.ts index dc8109089..9da6cb55c 100644 --- a/frontend/src/client/generated/sdk.gen.ts +++ b/frontend/src/client/generated/sdk.gen.ts @@ -2623,7 +2623,7 @@ export const taskAttachmentsDelete = (optio /** * Download a task attachment * - * Returns the raw bytes of one attachment. Requires read access to the task. Pass preview_size to get a downscaled preview instead, as a JPEG or, when the image has transparency, as a PNG. When no preview can be made of the attachment (not an image, undecodable, or too large) the request fails with 422 and code 4038 instead of returning the original. The Content-Type header carries the mime type of the bytes that are returned. + * Returns the raw bytes of one attachment. Requires read access to the task. For a downscaled image, use the preview endpoint. */ export const taskAttachmentsDownload = (options: Options): RequestResult => (options.client ?? client).get({ security: [{ diff --git a/frontend/src/client/generated/types.gen.ts b/frontend/src/client/generated/types.gen.ts index c645683c2..62c4dad0d 100644 --- a/frontend/src/client/generated/types.gen.ts +++ b/frontend/src/client/generated/types.gen.ts @@ -9288,20 +9288,11 @@ export type TaskAttachmentsDownloadData = { */ attachment: number; }; - query?: { - /** - * If set, return a downscaled preview instead of the original: sm=100px, md=200px, lg=400px, xl=800px. The preview is a JPEG, or a PNG when the image has transparency. Fails with 422 for attachments that have no preview, like non-images. - */ - preview_size?: 'sm' | 'md' | 'lg' | 'xl'; - }; + query?: never; url: '/tasks/{task}/attachments/{attachment}'; }; export type TaskAttachmentsDownloadErrors = { - /** - * preview_size was set but the attachment has no preview (code 4038), or preview_size is invalid. - */ - 422: VikunjaErrorModel; /** * Error */ diff --git a/pkg/routes/api/v2/task_attachments.go b/pkg/routes/api/v2/task_attachments.go index 47bda3688..4be85849d 100644 --- a/pkg/routes/api/v2/task_attachments.go +++ b/pkg/routes/api/v2/task_attachments.go @@ -80,7 +80,7 @@ func RegisterTaskAttachmentRoutes(api huma.API) { Register(api, huma.Operation{ OperationID: "task-attachments-download", Summary: "Download a task attachment", - Description: "Returns the raw bytes of one attachment. Requires read access to the task. Pass preview_size to get a downscaled preview instead, as a JPEG or, when the image has transparency, as a PNG. When no preview can be made of the attachment (not an image, undecodable, or too large) the request fails with 422 and code 4038 instead of returning the original. The Content-Type header carries the mime type of the bytes that are returned.", + Description: "Returns the raw bytes of one attachment. Requires read access to the task. For a downscaled image, use the preview endpoint.", Method: http.MethodGet, Path: "/tasks/{task}/attachments/{attachment}", Tags: tags, @@ -96,10 +96,6 @@ func RegisterTaskAttachmentRoutes(api huma.API) { }, }, }, - "422": { - Description: "preview_size was set but the attachment has no preview (code 4038), or preview_size is invalid.", - Content: defaultErrorResponse(api).Content, - }, }, }, taskAttachmentsDownload) @@ -196,25 +192,14 @@ func taskAttachmentsUpload(ctx context.Context, in *taskAttachmentUploadInput) ( // taskAttachmentsDownload exists because no handler.Do* fits a file body; bytes // stream from the StreamResponse callback without buffering — attachments can be large. func taskAttachmentsDownload(ctx context.Context, in *struct { - TaskID int64 `path:"task" doc:"The id of the task the attachment belongs to."` - AttachmentID int64 `path:"attachment" doc:"The id of the attachment to download."` - PreviewSize string `query:"preview_size" enum:"sm,md,lg,xl" doc:"If set, return a downscaled preview instead of the original: sm=100px, md=200px, lg=400px, xl=800px. The preview is a JPEG, or a PNG when the image has transparency. Fails with 422 for attachments that have no preview, like non-images."` + TaskID int64 `path:"task" doc:"The id of the task the attachment belongs to."` + AttachmentID int64 `path:"attachment" doc:"The id of the attachment to download."` }) (*huma.StreamResponse, error) { a, err := authFromCtx(ctx) if err != nil { return nil, err } - if in.PreviewSize != "" { - preview, err := models.GetTaskAttachmentPreview(ctx, a, in.TaskID, in.AttachmentID, models.GetPreviewSizeFromString(in.PreviewSize)) - if err != nil { - return nil, translateDomainError(err) - } - return &huma.StreamResponse{Body: func(hctx huma.Context) { - webfiles.WriteAttachmentPreview((*humaecho.Unwrap(hctx)).Response(), preview) - }}, nil - } - file, err := models.GetTaskAttachmentForDownload(a, in.TaskID, in.AttachmentID) if err != nil { return nil, translateDomainError(err) diff --git a/pkg/webtests/huma_task_attachment_test.go b/pkg/webtests/huma_task_attachment_test.go index f2321e361..82599fd9d 100644 --- a/pkg/webtests/huma_task_attachment_test.go +++ b/pkg/webtests/huma_task_attachment_test.go @@ -164,6 +164,21 @@ func TestTaskAttachmentsV2(t *testing.T) { assert.Equal(t, "nosniff", rec.Header().Get("X-Content-Type-Options")) }) + t.Run("Download returns the original even when a preview size is passed", func(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + token := humaTokenFor(t, &testuser1) + + var content bytes.Buffer + require.NoError(t, png.Encode(&content, image.NewNRGBA(image.Rect(0, 0, 400, 300)))) + id := uploadOneAttachment(t, e, token, "image.png", content.Bytes()) + + rec := humaRequest(t, e, http.MethodGet, "/api/v2/tasks/1/attachments/"+strconv.FormatInt(id, 10)+"?preview_size=md", "", token, "") + require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String()) + assert.Equal(t, content.Bytes(), rec.Body.Bytes()) + assert.Contains(t, rec.Header().Get("Content-Disposition"), "image.png") + }) + t.Run("Delete", func(t *testing.T) { e, err := setupTestEnv() require.NoError(t, err)