Skip to content

feat: replace direct AWS SDK uploads with pushduck#68

Open
abhay-ramesh wants to merge 1 commit intomichaelshimeles:mainfrom
abhay-ramesh:feat/pushduck-file-uploads
Open

feat: replace direct AWS SDK uploads with pushduck#68
abhay-ramesh wants to merge 1 commit intomichaelshimeles:mainfrom
abhay-ramesh:feat/pushduck-file-uploads

Conversation

@abhay-ramesh
Copy link
Copy Markdown

@abhay-ramesh abhay-ramesh commented Mar 27, 2026

Summary

  • Replaces the manual @aws-sdk/client-s3 upload flow with pushduck — same Cloudflare R2 backend, cleaner abstraction
  • Adds app/api/upload/route.ts — a type-safe pushduck router using the cloudflareR2 provider, reusing the existing env vars (CLOUDFLARE_ACCOUNT_ID, R2_UPLOAD_IMAGE_*). Session auth is enforced via auth.api.getSession() (better-auth), matching the pattern in app/api/subscription/route.ts
  • Rewrites app/dashboard/upload/page.tsx to use createUploadClient from pushduck/client: real presigned-URL uploads with per-file progress, no simulated progress intervals, no manual FormData construction
  • Annotates lib/upload-image.ts with a @deprecated notice pointing to the new router — the function itself is preserved so any other server-side callers still compile
  • Adds pushduck: "^0.4.0" to package.json

What changes for users

Before After
Files POSTed as multipart/form-data to /api/upload-image Files uploaded directly to R2 via presigned URLs (no proxying through the server)
Progress simulated with setInterval Real upload progress from the pushduck hook
No auth check on uploads better-auth session required — returns 401 for unauthenticated requests
5 MB hard limit 10 MB limit (configurable in the router)

No new env vars required

All existing R2 env vars are reused as-is:

CLOUDFLARE_ACCOUNT_ID=
R2_UPLOAD_IMAGE_ACCESS_KEY_ID=
R2_UPLOAD_IMAGE_SECRET_ACCESS_KEY=
R2_UPLOAD_IMAGE_BUCKET_NAME=images

Test plan

  • Start the dev server with the existing R2 env vars set
  • Sign in and visit /dashboard/upload
  • Upload a PNG — confirm real progress bar and "Done" status appear
  • Confirm the returned URL opens in a new tab
  • Try uploading while signed out — confirm a 401 is returned and an error toast appears
  • Try uploading a non-image file — confirm client-side rejection toast fires before any network request

Note

Replace direct AWS SDK uploads with pushduck in the image upload API and UI

  • Adds a pushduck router in app/api/upload/route.ts configured for Cloudflare R2, restricting uploads to images ≤10MB and prefixing stored files with uploads/{userId}/{timestamp}-{filename}. Requests without a valid session are rejected.
  • Refactors app/dashboard/upload/page.tsx to use the typed pushduck client, replacing manual fetch('/api/upload-image') calls with uploadFiles() and showing live per-file progress from client hooks.
  • Marks lib/upload-image.ts as deprecated in favor of the new router.
  • Behavioral Change: the remove button on uploaded files now reloads the page instead of removing the file from local state.

Macroscope summarized b6cf208.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 27, 2026

Greptile Summary

This PR replaces the server-proxied multipart/form-data upload flow with pushduck-managed presigned-URL uploads. The client now sends files directly to Cloudflare R2, eliminating server memory and bandwidth overhead. Auth is enforced at presigned-URL generation time via better-auth, and real per-file upload progress replaces the old simulated setInterval approach.\n\nKey changes:\n- app/api/upload/route.ts — new pushduck router with cloudflareR2 provider, 10 MB limit, and better-auth session middleware.\n- app/dashboard/upload/page.tsx — UI rewritten around createUploadClient with real progress tracking and per-file status list.\n- lib/upload-image.ts — marked @deprecated but preserved for existing server-side callers.\n- package.json — adds pushduck ^0.4.0.\n\nIssues found:\n- The "\u00d7" dismiss button on uploaded file cards now calls window.location.reload() instead of removing just that card — a clear UX regression.\n- file.name is embedded in the S3 key without sanitization.\n- The success toast counts files that passed client-side validation rather than files confirmed by R2.

Confidence Score: 4/5

Safe to merge after fixing the window.location.reload() regression on the dismiss button

One P1 finding: the X button calls window.location.reload() instead of dismissing the card, which is an observable UX regression. Remaining issues are P2 quality improvements that do not break the core upload flow.

app/dashboard/upload/page.tsx line 311 — the dismiss button reload regression must be addressed before merge.

Important Files Changed

Filename Overview
app/api/upload/route.ts New pushduck upload router with Cloudflare R2 config, session auth middleware, and key generation — file.name used unsanitized in the S3 key.
app/dashboard/upload/page.tsx Upload UI rewritten to use pushduck client hooks with real progress; the "×" dismiss button regresses to window.location.reload() instead of removing the card from state.
lib/upload-image.ts No functional changes; @deprecated JSDoc annotation added pointing to the new pushduck router.
package.json Adds pushduck: "^0.4.0" dependency; existing @aws-sdk/client-s3 is retained for the deprecated legacy helper.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: app/dashboard/upload/page.tsx
Line: 311

Comment:
**"×" dismiss button reloads the page instead of removing the card**

The `onClick` handler was changed from `() => removeFile(file.id)` (which removed just that card from state) to `() => window.location.reload()` (which reloads the entire page). This is a hard regression: clicking "×" on any uploaded file now discards every upload in the gallery, resets all component state, and triggers a full page navigation — none of which a user expects from a dismiss/close button.

A better approach would be to track dismissed IDs in a `Set` via `useState` and filter them out of `uploadedFiles`, avoiding the full reload entirely.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: app/api/upload/route.ts
Line: 30-31

Comment:
**Unsanitized `file.name` used directly in the S3 object key**

`file.name` is inserted into the storage key without any sanitization. A filename containing `/` or `\` characters can produce misleading key structures in R2.

Consider stripping path-separator characters before embedding the name:

```ts
generateKey: ({ file, metadata }) => {
  const safeName = file.name.replace(/[/\\]/g, "-");
  return `uploads/${metadata.userId}/${Date.now()}-${safeName}`;
},
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: app/dashboard/upload/page.tsx
Line: 62-71

Comment:
**Success toast counts attempted files, not actually-succeeded files**

`fileArray.length` is the number of files that passed client-side validation, not the number successfully stored in R2. If pushduck resolves `uploadFiles` with some files marked `status: "error"` without rejecting the promise, the toast may overcount successes.

Consider counting only files with `status === "success"` to form the message.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat: replace direct AWS SDK uploads wit..." | Re-trigger Greptile

size="sm"
variant="ghost"
onClick={() => removeFile(file.id)}
onClick={() => window.location.reload()}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 "×" dismiss button reloads the page instead of removing the card

The onClick handler was changed from () => removeFile(file.id) (which removed just that card from state) to () => window.location.reload() (which reloads the entire page). This is a hard regression: clicking "×" on any uploaded file now discards every upload in the gallery, resets all component state, and triggers a full page navigation — none of which a user expects from a dismiss/close button.

A better approach would be to track dismissed IDs in a Set via useState and filter them out of uploadedFiles, avoiding the full reload entirely.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/dashboard/upload/page.tsx
Line: 311

Comment:
**"×" dismiss button reloads the page instead of removing the card**

The `onClick` handler was changed from `() => removeFile(file.id)` (which removed just that card from state) to `() => window.location.reload()` (which reloads the entire page). This is a hard regression: clicking "×" on any uploaded file now discards every upload in the gallery, resets all component state, and triggers a full page navigation — none of which a user expects from a dismiss/close button.

A better approach would be to track dismissed IDs in a `Set` via `useState` and filter them out of `uploadedFiles`, avoiding the full reload entirely.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread app/api/upload/route.ts
Comment on lines +30 to +31
generateKey: ({ file, metadata }) =>
`uploads/${metadata.userId}/${Date.now()}-${file.name}`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unsanitized file.name used directly in the S3 object key

file.name is inserted into the storage key without any sanitization. A filename containing / or \ characters can produce misleading key structures in R2.

Consider stripping path-separator characters before embedding the name:

generateKey: ({ file, metadata }) => {
  const safeName = file.name.replace(/[/\\]/g, "-");
  return `uploads/${metadata.userId}/${Date.now()}-${safeName}`;
},
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/api/upload/route.ts
Line: 30-31

Comment:
**Unsanitized `file.name` used directly in the S3 object key**

`file.name` is inserted into the storage key without any sanitization. A filename containing `/` or `\` characters can produce misleading key structures in R2.

Consider stripping path-separator characters before embedding the name:

```ts
generateKey: ({ file, metadata }) => {
  const safeName = file.name.replace(/[/\\]/g, "-");
  return `uploads/${metadata.userId}/${Date.now()}-${safeName}`;
},
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +62 to 71
try {
await uploadFiles(fileArray);
toast.success(
fileArray.length === 1
? `${fileArray[0].name} uploaded successfully`
: `${fileArray.length} files uploaded successfully`,
);
} catch {
toast.error("Upload failed. Please try again.");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Success toast counts attempted files, not actually-succeeded files

fileArray.length is the number of files that passed client-side validation, not the number successfully stored in R2. If pushduck resolves uploadFiles with some files marked status: "error" without rejecting the promise, the toast may overcount successes.

Consider counting only files with status === "success" to form the message.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/dashboard/upload/page.tsx
Line: 62-71

Comment:
**Success toast counts attempted files, not actually-succeeded files**

`fileArray.length` is the number of files that passed client-side validation, not the number successfully stored in R2. If pushduck resolves `uploadFiles` with some files marked `status: "error"` without rejecting the promise, the toast may overcount successes.

Consider counting only files with `status === "success"` to form the message.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant