Skip to content

Conversation

@zudsniper
Copy link

@zudsniper zudsniper commented Nov 15, 2025

Summary

Adds SVG MIME support across the app, enhances file operations including a new replace-file workflow, and refines how files are previewed, downloaded, and renamed. Also improves server-side MIME inference and URL handling to support inline dispositions and cache busting.

Changes

  • Core MIME & URL handling

    • Add svg: image/svg+xml to MIME mapping and support mime inference from filenames (including double extensions like log.gz).
    • buildFileAccessUrl now supports a disposition parameter and appends cache-version and disposition query params when appropriate.
    • Thumbnail/preview URLs now use disposition inline to better support inline previews.
  • Backend / Worker updates

    • GetObject now infers and sets Content-Type when missing, using a MIME map (including svg).
    • Supports Content-Disposition based on the requested disposition query param (inline vs attachment) with a safe filename.
    • Minor improvements to ensure correct handling of double-extensions and safe filenames.
  • Upload API improvements

    • apiHandler.uploadObjects now accepts options for customMetadata and httpMetadata, infers content type from the file name, and passes metadata to the server.
    • Automatically includes a cache-version timestamp unless skipCacheVersion is requested.
  • UI: File Context & Browse enhancements

    • FileContextMenu: added a Replace file... option for file items; kept Rename and other existing actions.
    • FileBrowser.vue: implemented a full replace-file workflow:
      • Hidden file input for replacement
      • Support for single and bulk-replace (bulk requires a single file selection)
      • Replaces target file via uploadObjects with progress notifications
      • Respects read-only bucket mode and provides user feedback when replacement isn’t allowed
    • FileOptions.vue: enhanced rename logic and comparisons for items:
      • Added getComparableName and getParentKey helpers
      • Robust duplicate checks and safer new-key calculation (handles folders and trailing slashes)
  • Preview & Gallery improvements

    • FilePreview.vue: manage object URLs more safely; revoke object URLs on cleanup and unmount; infer content-type for blob/object URL creation when downloading previews.
    • MediaGallery.vue: ensure built URLs include disposition inline where appropriate for previews.
  • Context menus & bulk ops

    • FileContextMenu.vue: added a dedicated Replace file... option and simplified bulk operation detection to reflect multi-select usage.
    • Bulk actions now include Rename and Replace controls when applicable.

Testing plan

  • SVG handling

    • Upload and preview SVGs; verify correct content-type in responses and proper rendering in file previews.
    • Open SVGs in media/gallery views; ensure inline disposition previews render without errors.
  • Replace-file workflow

    • Single-file replacement: select a file, trigger Replace, choose a new file, monitor upload progress, verify the file is replaced (key unchanged, content updated).
    • Read-only bucket: ensure replacement is blocked with an appropriate notification.
    • Bulk replace: attempt with multiple selections should be blocked; single-file expectations should work and show progress.
  • Rename & duplicates

    • Rename files and folders; ensure names with trailing slashes and folder paths are handled correctly.
    • Duplicate name checks should be case-insensitive and ignore path delimiters where appropriate.
  • Preview/Download behavior

    • Preview/download thumbnails for various types (svg, png, jpg, etc.) use correct MIME types.
    • Object URL cleanup after previews and on component unmount.
  • API & URL behavior

    • Access URLs include v (cache-version) when applicable and disposition when provided.
    • Uploads attach correct httpMetadata/contentType inferred from filenames.

Migration / Compatibility

  • No breaking API changes for existing clients; new behavior is opt-in via URL disposition for previews and via file replace UI in the dashboard.
  • SVGs and other common formats will now render with correct MIME types across previews and direct access.

Notes

  • This patch introduces new entry points for replacing files and inline disposition handling. If you rely on server-side tooling to infer MIME types, you may want to re-run compatibility checks against svg and log.gz scenarios.

🌿 Generated by Terry


ℹ️ Tag @terragon-labs to ask questions and address PR feedback

📎 Task: https://www.terragonlabs.com/task/d3197c9f-aae9-47f6-9e2a-5036ed7067c4

Summary by CodeRabbit

  • New Features

    • Added file replacement capability via context menu and bulk actions.
    • Added F2 keyboard shortcut to rename selected files.
    • Improved MIME type inference for file uploads and previews.
    • Enhanced file display with content-disposition handling (inline vs. attachment).
  • Bug Fixes

    • Improved file preview and media gallery URL generation with proper content disposition.

- Implement file replace functionality in FileBrowser component including UI and handler logic.
- Add event handling and bulk replace support for selected files.
- Extend FileContextMenu with replace file option.
- Enhance appUtils to infer MIME types and add disposition to file access URLs.
- Update worker getObject handler to support inline Content-Disposition and infer content-type from filename.
- Improve FilePreview and MediaGallery components to use inline disposition for better media rendering.
- Refactor FileOptions rename logic for consistent naming handling.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@coderabbitai
Copy link

coderabbitai bot commented Nov 15, 2025

Walkthrough

This PR introduces file replacement capability across the dashboard, enhances MIME type inference for content-type handling, and extends URL building to support content-disposition parameters. Changes span utility functions, UI components, and backend worker modules to enable inline/attachment disposition handling and improved metadata management throughout the file access pipeline.

Changes

Cohort / File(s) Summary
MIME type and URL utilities
packages/dashboard/src/appUtils.js
Added MIME_TYPE_MAP and DOUBLE_EXTENSIONS for file extension mapping; introduced inferMimeTypeFromFilename() utility. Enhanced buildFileAccessUrl to accept disposition parameter and encode cache-version and disposition as query parameters. Expanded uploadObjects API with options parameter supporting customMetadata, skipCacheVersion, and inferred contentType via httpMetadata. Exported new inferMimeTypeFromFilename function.
File browser replace workflow
packages/dashboard/src/components/files/FileBrowser.vue
Integrated "Replace File" capability into context menus, bulk actions, and mobile UI. Added replaceInput and replaceTarget refs, resetReplaceState helper, canRenameSelection and canReplaceSelection computed properties. Implemented handleReplace, handleReplaceFileChange, handleBulkRename, and handleBulkReplace methods with progress notifications and file list refresh.
File rename helpers
packages/dashboard/src/components/files/FileOptions.vue
Introduced getComparableName and getParentKey internal helpers for name normalization and key derivation. Updated rename validation logic to use helpers for consistent comparison of filenames and folder paths.
Context menu and actions
packages/dashboard/src/pages/files/FileContextMenu.vue
Added replaceObject method emitting replaceObject event. Modified isBulkOperation logic to return true only for multiple selections (\>1). Split single "Rename" action into separate Rename and new Replace File... entries.
Preview components with object URL handling
packages/dashboard/src/components/preview/FilePreview.vue, packages/dashboard/src/components/preview/MediaGallery.vue
Added objectUrl state, cleanupObjectUrl method, and beforeUnmount lifecycle to FilePreview. Imported inferMimeTypeFromFilename and create Blobs with detected MIME types for object URL previews. Added disposition: "inline" to MediaGallery URL building.
Backend content-type inference
packages/worker/src/modules/buckets/getObject.ts
Added MIME_TYPE_MAP and DOUBLE_EXTENSIONS; introduced inferContentType function. Read disposition from URL query parameter and apply to Content-Disposition header. Sanitize filename and infer content-type when missing from object headers.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant UI as FileBrowser.vue
    participant Upload as apiHandler.uploadObjects
    participant Cache as Browser Cache
    participant Notify as Notification
    participant Server as Backend

    User->>UI: Click "Replace File..."
    UI->>UI: handleReplace(item)
    activate UI
    UI->>UI: Set replaceTarget = item
    UI->>UI: Trigger hidden file input
    deactivate UI
    
    User->>UI: Select replacement file
    UI->>UI: handleReplaceFileChange(event)
    activate UI
    UI->>Notify: Show "Uploading..." progress
    UI->>Upload: uploadObjects(file, key, bucket, callback, {httpMetadata, customMetadata})
    deactivate UI
    
    Upload->>Upload: Infer MIME type from filename
    Upload->>Upload: Build HTTP metadata with contentType
    Upload->>Server: POST with inferred content-type
    
    Server->>Server: Store file with metadata
    Server-->>Upload: Success
    
    Upload->>Cache: Clear cache version
    Upload-->>UI: Callback with result
    
    UI->>Notify: Show "File replaced" success
    UI->>UI: handleBulkRefresh()
    UI->>UI: resetReplaceState()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • appUtils.js: New MIME type map logic and expanded uploadObjects signature with metadata handling requires careful verification of content-type inference across double extensions (e.g., .log.gz).
  • FileBrowser.vue: Multiple new methods (handleReplace, handleReplaceFileChange, handleBulkRename, handleBulkReplace) and state management warrant thorough testing of the replace workflow, progress notifications, and error handling paths.
  • FileContextMenu.vue: Modified isBulkOperation logic (now selectedRows.length > 1) changes bulk behavior semantics and requires validation that single-file operations still work correctly.
  • FilePreview.vue and MediaGallery.vue: Object URL lifecycle (creation, cleanup, unmount) and disposition parameter propagation need verification to prevent memory leaks and ensure correct header behavior.
  • getObject.ts (worker): Content-Disposition header construction with filename escaping and disposition inference from query parameters should be verified against RFC 5987 and tested with special characters.

Possibly related PRs

  • R2-Explorer#10: Directly modifies FileBrowser.vue with replace-file UI flows and wiring to the updated upload/build URL utilities in this PR.
  • R2-Explorer#9: Modifies the same appUtils.js utilities (buildFileAccessUrl, thumbnail URL helpers, and cache/metadata handling) and related preview URL usage.
  • R2-Explorer#3: Touches FileContextMenu.vue and FileOptions.vue file-operation UI, including changes to bulk-operation gating (isBulkOperation) and emitted actions that align with this PR's modifications.

Poem

🐰 With MIME types mapped and filenames bright,
We hop through the replace-file delight—
Disposition headers, object URLs clean,
The finest file-browser you've ever seen!
bounce bounce

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main changes: SVG support (MIME type mappings), replace-file workflow (new UI and upload features), and enhanced file operations (improved metadata handling, URL building, content disposition).
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch terragon/support-svg-previews-and-file-ops-3k9hh1

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/dashboard/src/components/preview/FilePreview.vue (1)

315-321: Fix preview size guard to use raw bytes instead of formatted size

bytesToMegabytes(file.size) is operating on the human‑readable size string from mapFile (e.g. "10 MB"), so the > 200 check will never behave correctly. Use the raw bytes field instead.

Apply this change:

-    if (bytesToMegabytes(file.size) > 200) {
+    if (bytesToMegabytes(file.sizeRaw ?? file.size) > 200) {

Assuming all callers pass in objects from mapFile, sizeRaw will be defined and the guard will work as intended.

🧹 Nitpick comments (2)
packages/dashboard/src/appUtils.js (1)

21-47: Optional: consider using audio/video specific type for .ogg

Right now .ogg is mapped to "application/ogg". If you primarily serve audio OGG, "audio/ogg" can be a closer match; for video, "video/ogg" would be more accurate. Not critical, but worth aligning with your dominant use case.

packages/dashboard/src/components/files/FileOptions.vue (1)

134-163: Consider adding JSDoc comments to the helpers.

While the helper methods are well-implemented, adding JSDoc comments would improve maintainability and help other developers understand their purpose and edge cases.

Example documentation:

+		/**
+		 * Normalizes an item's name for comparison by trimming whitespace
+		 * and removing trailing slashes from folders.
+		 * @param {Object} item - The item (file or folder) to normalize
+		 * @returns {string} The normalized name, or empty string if item is invalid
+		 */
 		getComparableName(item) {
 			// ... existing code
 		},
+		/**
+		 * Extracts the parent directory path from an item's key.
+		 * For folders, strips the trailing slash before finding the parent.
+		 * @param {Object} item - The item whose parent key to extract
+		 * @returns {string} Parent directory path with trailing slash, or empty string for root-level items
+		 */
 		getParentKey(item) {
 			// ... existing code
 		},
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c80c2d4 and c4ea183.

📒 Files selected for processing (7)
  • packages/dashboard/src/appUtils.js (5 hunks)
  • packages/dashboard/src/components/files/FileBrowser.vue (14 hunks)
  • packages/dashboard/src/components/files/FileOptions.vue (5 hunks)
  • packages/dashboard/src/components/preview/FilePreview.vue (7 hunks)
  • packages/dashboard/src/components/preview/MediaGallery.vue (1 hunks)
  • packages/dashboard/src/pages/files/FileContextMenu.vue (3 hunks)
  • packages/worker/src/modules/buckets/getObject.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/worker/src/modules/buckets/getObject.ts (1)
packages/dashboard/src/appUtils.js (6)
  • MIME_TYPE_MAP (21-47)
  • DOUBLE_EXTENSIONS (49-51)
  • ext (70-70)
  • ext (85-85)
  • ext (503-503)
  • ext (508-508)
packages/dashboard/src/appUtils.js (2)
packages/dashboard/src/stores/main-store.js (3)
  • useMainStore (4-108)
  • useMainStore (4-108)
  • url (64-64)
packages/dashboard/src/boot/axios.js (1)
  • api (14-14)
🔍 Remote MCP Cloudflare Workers Observability

Summary of Relevant Context Found

Based on comprehensive documentation searches, here are key technical considerations for reviewing this PR:

MIME Type and Content-Type Handling

When serving static assets, Workers automatically determines the MIME type of files based on their extension, and attaches a Content-Type header to the response if one is provided during asset upload. The PR's addition of MIME type inference from filenames aligns with this pattern.

Content-Disposition Header Best Practices

R2 supports HTTP header metadata including Content-Disposition through the httpMetadata field, which maps to HTTP headers. The PR's implementation of disposition query parameters for inline vs. attachment serving follows established web standards for file delivery.

R2 Object Storage and Metadata

When uploading objects to R2, metadata is handled through the Metadata Service which ensures strong consistency across the storage system. R2's httpMetadata field supports customMetadata (user-defined key-value pairs stored as x-amz-meta-* headers) and standard headers like Content-Type and Content-Disposition. The PR's approach of passing both customMetadata and httpMetadata in upload operations is consistent with R2's design.

SVG Handling Considerations

Cloudflare sanitizes SVG files using svg-hush to remove scripting, hyperlinks, and cross-origin resource references before serving them. SVG files are inherently scalable and do not require resizing. The PR's SVG MIME type support (image/svg+xml) enables proper content-type delivery for sanitized SVGs.

Query Parameter Patterns

The PR's pattern of appending disposition and cache-version as query parameters (v=... and disposition=...) aligns with how Wrangler allows setting Content-Type and Content-Disposition through optional flags during upload, supporting dynamic HTTP header control.

Double Extension Handling

The inclusion of double-extension support (e.g., log.gz) is relevant because Cloudflare caches SVG and other extensions by file extension, so accurate extension parsing is important for correct MIME type inference and cache behavior.

🔇 Additional comments (19)
packages/dashboard/src/components/preview/MediaGallery.vue (1)

187-193: Inline disposition in gallery URLs looks correct

Passing disposition: "inline" into buildFileAccessUrl for gallery media aligns with preview usage and avoids forcing download; no issues spotted with this change.

packages/dashboard/src/pages/files/FileContextMenu.vue (1)

15-19: Replace + bulk semantics are consistent and well‑scoped

Restricting “Replace file…” and “Rename” to non‑bulk states and defining bulk purely as selectedRows.length > 1 matches the intended UX, and the new replaceObject emitter is cleanly wired for parent handling.

Also applies to: 74-75, 84-86

packages/dashboard/src/components/preview/FilePreview.vue (1)

200-205: Object URL lifecycle + MIME inference are implemented cleanly

The new objectUrl field, cleanupObjectUrl() helper, and calls from openFile, close, and beforeUnmount provide a tight lifecycle for preview URLs and avoid leaks. Using httpMetadata.contentType with a fallback to inferMimeTypeFromFilename(this.file?.name) when constructing the Blob gives sensible content types for image/video/pdf/audio previews.

Also applies to: 226-227, 293-298, 335-336, 368-380, 394-395, 592-594

packages/dashboard/src/appUtils.js (2)

21-72: MIME inference and URL query handling are coherent and backwards‑safe

The new MIME_TYPE_MAP/DOUBLE_EXTENSIONS plus inferMimeTypeFromFilename cover the key formats (including svg and .log.gz) and the buildFileAccessUrl changes cleanly centralize v and disposition query parameters while keeping existing call sites working (3rd arg remains optional). Using disposition: "inline" in getThumbnailUrl matches preview/gallery usage.

Also applies to: 219-258, 515-522


364-402: uploadObjects options + contentType inference align with replace workflow

Allowing uploadObjects to take options with customMetadata/httpMetadata, auto‑injecting a "cache-version" (when not skipped), and inferring contentType from file.type, filename, or existing metadata gives the replace/upload flows enough flexibility while retaining sane defaults.

packages/dashboard/src/components/files/FileBrowser.vue (4)

181-193: Replace action is consistently exposed across grid, list, context, and mobile UIs

Hooking @replaceObject="handleReplace" into all file-context-menu instances and adding a “Replace File…” entry in the mobile sheet keeps the replace capability discoverable in every interaction mode, while remaining file‑only.

Also applies to: 305-316, 333-345, 388-411


513-519: Replace workflow correctly scopes target, preserves metadata, and respects read‑only

The combination of replaceInput/replaceTarget, resetReplaceState, handleReplace, and handleReplaceFileChange:

  • Only allows replacing a single file (not folders or multi‑selection).
  • Guards against read‑only mode both before opening the picker and on change.
  • Reuses the existing key so URLs remain stable.
  • Passes through existing customMetadata/httpMetadata to uploadObjects, so attributes are preserved while cache‑version is refreshed.

Overall the flow is robust and matches the PR goals.

Also applies to: 603-605, 673-682, 1087-1190


1192-1222: Bulk rename/replace helpers provide clear UX constraints

handleBulkRename/handleBulkReplace enforcing a single selected item and surfacing explicit notifications when the selection is invalid avoids ambiguous behavior and keeps the bulk bar aligned with single‑item operations.


465-482: Bulk toolbar and keyboard F2 shortcut are well‑integrated

The new bulk “Rename”/“Replace” buttons, gated by canRenameSelection/canReplaceSelection, plus the F2 keybinding for rename, give predictable affordances without overloading multi‑selection behavior. Wiring these through the existing handleRename/handleReplace paths keeps the implementation DRY.

Also applies to: 673-682, 1368-1426, 1475-1551

packages/worker/src/modules/buckets/getObject.ts (4)

29-31: LGTM!

The DOUBLE_EXTENSIONS mapping is consistent with the dashboard version and correctly handles the common .log.gz case.


33-48: LGTM!

The content-type inference logic is correct: it checks double extensions first (e.g., .log.gz), then falls back to single extensions. The defensive check at line 42 ensures safe handling of filenames without extensions.


96-97: LGTM!

The disposition query parameter extraction is straightforward and correctly handles the case where the parameter is not present (returns null).


103-108: LGTM!

The content-type inference correctly respects existing metadata by only applying inferred types when the header is missing. This preserves explicitly set content-types while providing sensible defaults.

packages/dashboard/src/components/files/FileOptions.vue (6)

134-143: LGTM: Clean normalization helper with proper null handling.

The getComparableName helper correctly normalizes item names by trimming whitespace and stripping trailing slashes from folders. The null/undefined check prevents runtime errors, and the regex pattern /\/+$/ appropriately handles multiple consecutive trailing slashes.


144-163: LGTM: Parent key extraction handles edge cases correctly.

The getParentKey helper correctly derives parent directory paths for both files and folders. The logic properly handles root-level items (returning empty string) and normalizes folder keys before extracting the parent path.


184-190: LGTM: Consistent use of normalization in rename initialization.

Using getComparableName(row) ensures the rename input is initialized with the normalized name (without trailing slashes for folders), improving user experience.


209-223: LGTM: Correct key reconstruction for rename operation.

The rename logic correctly constructs the new key by:

  1. Comparing normalized names to detect actual changes
  2. Extracting the parent key using the helper
  3. Appending the appropriate slash suffix for folders

This ensures renamed items maintain their position in the hierarchy.


506-525: LGTM: Consistent normalization in disabled state check.

The rename button correctly uses getComparableName to determine if the name has actually changed, ensuring the normalized comparison matches the logic used elsewhere.


491-498: LGTM: Duplicate detection verified across the call chain.

The verification confirms the implementation is sound:

  • fetchFile() properly handles pagination (lines 415-480 in appUtils.js) and returns the complete item list
  • sortedItems in FileBrowser.vue contains all sibling items in the current directory
  • FileBrowser.vue correctly passes sortedItems.value to renameObject() as the availableItems parameter
  • The duplicate detection logic properly uses normalized names (getComparableName), case-insensitive comparison, and excludes the item being renamed

All callers examined (FileBrowser.vue line 1080) correctly pass the complete list of sibling items.

Comment on lines +6 to +27
const MIME_TYPE_MAP: Record<string, string> = {
svg: "image/svg+xml",
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
webp: "image/webp",
avif: "image/avif",
mp4: "video/mp4",
webm: "video/webm",
mov: "video/quicktime",
mp3: "audio/mpeg",
ogg: "application/ogg",
wav: "audio/wav",
json: "application/json",
pdf: "application/pdf",
csv: "text/csv",
html: "text/html",
txt: "text/plain",
md: "text/markdown",
log: "text/plain",
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent MIME type coverage and code duplication.

The worker's MIME_TYPE_MAP is missing several entries present in the dashboard version (packages/dashboard/src/appUtils.js): bmp, flac, aac, htm, and gz. This inconsistency means files with these extensions won't receive proper content-type inference on the worker side.

Additionally, the MIME type maps are duplicated across packages. Consider extracting these constants to a shared package/module to ensure consistency and reduce maintenance burden.

Apply this diff to add the missing MIME types:

 const MIME_TYPE_MAP: Record<string, string> = {
 	svg: "image/svg+xml",
 	png: "image/png",
 	jpg: "image/jpeg",
 	jpeg: "image/jpeg",
 	gif: "image/gif",
+	bmp: "image/bmp",
 	webp: "image/webp",
 	avif: "image/avif",
 	mp4: "video/mp4",
 	webm: "video/webm",
 	mov: "video/quicktime",
 	mp3: "audio/mpeg",
 	ogg: "application/ogg",
 	wav: "audio/wav",
+	flac: "audio/flac",
+	aac: "audio/aac",
 	json: "application/json",
 	pdf: "application/pdf",
 	csv: "text/csv",
 	html: "text/html",
+	htm: "text/html",
 	txt: "text/plain",
 	md: "text/markdown",
 	log: "text/plain",
+	gz: "application/gzip",
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const MIME_TYPE_MAP: Record<string, string> = {
svg: "image/svg+xml",
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
webp: "image/webp",
avif: "image/avif",
mp4: "video/mp4",
webm: "video/webm",
mov: "video/quicktime",
mp3: "audio/mpeg",
ogg: "application/ogg",
wav: "audio/wav",
json: "application/json",
pdf: "application/pdf",
csv: "text/csv",
html: "text/html",
txt: "text/plain",
md: "text/markdown",
log: "text/plain",
};
const MIME_TYPE_MAP: Record<string, string> = {
svg: "image/svg+xml",
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
bmp: "image/bmp",
webp: "image/webp",
avif: "image/avif",
mp4: "video/mp4",
webm: "video/webm",
mov: "video/quicktime",
mp3: "audio/mpeg",
ogg: "application/ogg",
wav: "audio/wav",
flac: "audio/flac",
aac: "audio/aac",
json: "application/json",
pdf: "application/pdf",
csv: "text/csv",
html: "text/html",
htm: "text/html",
txt: "text/plain",
md: "text/markdown",
log: "text/plain",
gz: "application/gzip",
};

Comment on lines +110 to 117
const filename = filePath.split("/").pop() || "file";
const safeFilename = filename.replace(/"/g, '\\"') || "file";
const disposition =
requestedDisposition === "inline" ? "inline" : "attachment";
headers.set(
"Content-Disposition",
`attachment; filename="${filePath.split("/").pop()}"`,
`${disposition}; filename="${safeFilename}"`,
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Verify filename encoding compliance with RFC 6266.

The current approach escapes only double quotes in filenames. RFC 6266 specifies that the Content-Disposition header should use percent-encoding (via the filename* parameter) for filenames containing non-ASCII or special characters beyond basic escaping.

While the current implementation handles common cases, please verify whether more robust encoding is needed for:

  • Non-ASCII characters (e.g., Unicode filenames)
  • Special characters beyond double quotes (e.g., semicolons, backslashes)
  • Very long filenames

🌐 Web query:

RFC 6266 Content-Disposition filename encoding requirements for special characters

💡 Result:

Summary — RFC 6266 requirements for encoding filenames with special characters

  • Use filename* (RFC 5987 encoding) when the desired name contains characters not representable in ISO‑8859‑1 (i.e., non‑ASCII / international characters). (datatracker.ietf.org)
  • When sending filename*, also send a fallback filename (plain filename parameter) for legacy UAs; place filename first in the header. Recipients SHOULD prefer filename* when they understand it. (datatracker.ietf.org)
  • filename (non‑star) is limited to ISO‑8859‑1; use token form only if it contains no disallowed characters (use quoted-string otherwise). Avoid non‑ASCII in filename. (datatracker.ietf.org)
  • The RFC5987 form (filename*=charset'lang'%xx...) percent‑encodes octets (UTF‑8 is recommended); use UTF‑8 for filename*. (rfc-editor.org)
  • Practical generation advice / pitfalls from RFC 6266 Appendix D:
    • Avoid embedding literal percent‑hex sequences (e.g. %A9) in filename because some implementations treat them as escapes.
    • Avoid backslash "" in quoted-string (some UAs don’t implement escaping).
    • If possible, provide an ASCII-friendly fallback (e.g., transliteration) for the filename parameter. (httpwg.org)

If you want, I can show exact header examples (attachment; filename="..."; filename*=UTF-8''...) and how to percent‑encode a sample UTF‑8 name.

Citations:




🏁 Script executed:

#!/bin/bash
# Search for getObject usage and understand typical filename patterns
rg -n "getObject" packages/worker/src --type=ts -C 3 | head -50

Length of output: 635


🏁 Script executed:

#!/bin/bash
# Check if there are any filename/key handling tests or documentation
fd -e test.ts -e spec.ts -e docs.md | xargs rg -l "getObject|filename|Content-Disposition" 2>/dev/null | head -10

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Search for any existing filename encoding or sanitization utilities
rg -n "filename|Content-Disposition|RFC|6266|filename\*" packages/worker/src --type=ts -C 2 | head -40

Length of output: 1310


🏁 Script executed:

#!/bin/bash
# Check getObject.ts for additional context about the function and its purpose
cat packages/worker/src/modules/buckets/getObject.ts | head -150

Length of output: 2927




🏁 Script executed:

#!/bin/bash
# Check test patterns for getObject to understand expected usage
cat packages/worker/tests/integration/object.test.ts

Length of output: 13418


🏁 Script executed:

#!/bin/bash
# Search for any documentation or comments about filename handling
rg -n "filePath|filename|disposition" packages/worker/src/modules/buckets/getObject.ts

Length of output: 508


🏁 Script executed:

#!/bin/bash
# Check if there are any utility functions for encoding/sanitizing filenames
fd -type f -name "*.ts" packages/worker/src | xargs rg -l "encode|sanitize" | head -5

Length of output: 387


Fix Content-Disposition header to comply with RFC 6266.

The current implementation has two RFC 6266 compliance issues:

  1. Backslash escaping is non-compliant: RFC 6266 Appendix D explicitly warns that "backslash '' in quoted-string" should be avoided as some user agents don't implement escaping correctly. The replace(/"/g, '\\"') on line 111 violates this.

  2. Missing filename parameter for non-ASCII*: RFC 6266 requires the filename* parameter (RFC 5987 encoding) for filenames with non-ASCII characters. Without it, international filenames won't display correctly. This is relevant because email attachments (which feed into this system via receiveEmail.ts) can contain non-ASCII filenames.

Required changes:

  • Implement RFC 5987 percent-encoding for the filename* parameter
  • Provide an ASCII fallback in the filename parameter (e.g., via transliteration)
  • Use proper quoted-string escaping or token form (avoid backslash escaping per RFC 6266 Appendix D)
  • Update tests to cover non-ASCII filename handling

@zudsniper
Copy link
Author

@claude resolve these (minor) errors

@zudsniper zudsniper merged commit e9a00c5 into main Nov 19, 2025
2 checks passed
@zudsniper zudsniper deleted the terragon/support-svg-previews-and-file-ops-3k9hh1 branch November 19, 2025 13:48
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.

2 participants