Attachments & Uploads
Overview
Section titled “Overview”The Crm::AttachmentsComponent is a modern, consolidated ViewComponent that replaces the legacy three-panel attachment system with a single, clean interface.
Features
Section titled “Features”- ✅ Single Panel Design: Consolidates upload form, publication search, and attachments grid into one compact panel
- ✅ Turbo-Powered: Uses Turbo Streams and Turbo Frames for seamless updates without page refreshes
- ✅ Modal Publication Picker: Clean modal interface for searching and attaching publications
- ✅ Multiple File Upload: Support for selecting and uploading multiple files at once
- ✅ Responsive Grid: Bootstrap 5 responsive grid showing 1-6 columns depending on screen size
- ✅ Reusable: Can be used with any model that has the
attachableconcern
Architecture
Section titled “Architecture”Crm::AttachmentsComponent├── Action Buttons (Upload File, Add Publication)├── Hidden File Upload Form (with Stimulus controller)├── Turbo Frame: Attachments Grid│ └── Individual Upload Cards (Crm::UploadCardComponent)└── Publication Picker Modal (Turbo Frame for search results)Basic Usage
Section titled “Basic Usage”<%= render Crm::AttachmentsComponent.new(context_object: @communication) %>With Options
Section titled “With Options”<%= render Crm::AttachmentsComponent.new( context_object: @communication, wrapped: true, # Wrap in simple_panel (default: true) skip_publications: false, # Hide publication picker (default: false) multiple_files_allowed: true, # Allow multiple file selection (default: true) template_options_for_select: nil, # Optional template radio buttons category_options_for_select: nil # Optional category radio buttons) %>Unwrapped (No Panel)
Section titled “Unwrapped (No Panel)”For embedding in custom layouts:
<%= render Crm::AttachmentsComponent.new( context_object: @customer, wrapped: false) %>Migration from Legacy System
Section titled “Migration from Legacy System”Before (Legacy - 3 Panels)
Section titled “Before (Legacy - 3 Panels)”<%= attachable_panel(@communication) %><%= attachable_form @communication %>This generated:
- Upload File panel with file input and preview
- Search Library panel with search form
- Attachments grid (separate)
After (Modern - 1 Panel)
Section titled “After (Modern - 1 Panel)”<%= render Crm::AttachmentsComponent.new(context_object: @communication) %>This generates:
- Single “Attachments” panel
- Two action buttons (Upload File, Add Publication) in header
- Hidden file input (triggered by button, no preview, no base64 encoding)
- Modal for publication search (lazy-loaded)
- Same responsive attachments grid
Key Architectural Decisions
Section titled “Key Architectural Decisions”-
No Image Preview: The component intentionally does NOT use the
image-previewStimulus controller or SimpleForm’simage_fileinput type. This prevents:- Base64 encoding of images in the HTML
- Unnecessary DOM bloat
- Memory issues with large files
- Complexity in the upload flow
-
Plain File Input: Uses
file_field_taginstead of SimpleForm’sf.input :attachment, as: :image_fileto avoid automatic preview generation. -
Dual Controller Pattern:
crm-attachmentshandles UI interactions (button clicks)file-uploadhandles the actual upload logic- This separation of concerns makes the code more maintainable
-
Capture Phase Event Handling: The
file-uploadcontroller usesaddEventListener(..., true)to ensure it processes file selection before any other handlers that might interfere.
Component Structure
Section titled “Component Structure”app/components/crm/├── attachments_component.rb # Main component class├── attachments_component.html.erb # Component template└── upload_card_component.rb # Individual upload card └── upload_card_component.html.erb
app/views/crm/attachments/└── _publication_modal.html.erb # Modal with Turbo Frame
app/javascript/controllers/├── crm_attachments_controller.js # Handles upload button click└── file_upload_controller.js # Handles file selection & upload
app/views/attachable/├── _picker.html.erb # Publication search results├── attach.turbo_stream.erb # Response for file uploads└── remove_attachment.turbo_stream.erb # Response for removalsTurbo Integration
Section titled “Turbo Integration”Turbo Frames
Section titled “Turbo Frames”-
Attachments Frame:
attachments-frame-{id}- Wraps the entire attachments grid
- Enables Turbo even though Turbo Drive is globally disabled
-
Publication Picker Frame:
publication-picker-frame-{id}- Loads publication search interface lazily when modal opens
- Updates with search results without closing modal
Turbo Streams
Section titled “Turbo Streams”-
Upload Response:
attach.turbo_stream.erb- Appends new upload card(s) to attachments grid
- Shows error flash messages for failed uploads
-
Remove Response:
remove_attachment.turbo_stream.erb- Removes upload card from DOM by ID
Stimulus Controllers
Section titled “Stimulus Controllers”crm-attachments Controller
Section titled “crm-attachments Controller”Purpose: Manages the attachment panel interactions (button clicks only)
Targets:
fileInput: The hidden file input element
Actions:
openFileUpload: Triggers click on hidden file input to open file picker
Example:
<button data-action="click->crm-attachments#openFileUpload"> Upload File</button>Note: This controller only handles opening the file picker. The actual upload is delegated to the file-upload controller.
file-upload Controller
Section titled “file-upload Controller”Purpose: Handles file selection and AJAX upload (attached to the form element)
Actions:
- Listens for file input change events (using capture phase to prevent conflicts)
- Prevents duplicate uploads with
isUploadingflag - Shows progress spinners for each file being uploaded
- Sends files via
fetchwithAccept: text/vnd.turbo-stream.html - Processes Turbo Stream responses to append uploaded files to grid
- Cleans up progress spinners and resets form after upload
Important:
- The form must NOT include the
image-previewcontroller to avoid base64 encoding - Uses plain
file_field_taginstead of SimpleForm’simage_fileinput type - Event listener uses capture phase (
addEventListener(..., true)) to ensure it runs before any other handlers
Publication Picker Modal
Section titled “Publication Picker Modal”Features
Section titled “Features”- Lazy Loading: Content loaded only when modal is opened
- Turbo Frame: Search form and results stay within modal
- Pagination: Navigate through results without closing modal
- Responsive Grid: 1-2 columns on mobile/desktop
- Already Attached: Shows checkmark for publications already attached
- Turbo Stream Attach: Clicking “Attach” adds publication without closing modal
Search Flow
Section titled “Search Flow”- User clicks “Add Publication” button
- Modal opens, Turbo Frame loads search interface (
search_libraryaction) - User enters search term and submits
- Turbo Frame updates with results (stays in modal)
- User clicks “Attach” on a publication
- Turbo Stream appends publication to attachments grid
- Publication shows checkmark in modal
Upload Card Component
Section titled “Upload Card Component”The Crm::UploadCardComponent renders each individual attachment:
Features
Section titled “Features”- 1:1 Aspect Ratio: Perfect square images using Bootstrap
ratio-1x1 - Object-fit Cover: Images fill the square without distortion
- Responsive: Works in 1-6 column grids
- Literature Support: Shows publication cover images for PDFs
- Footer Actions: Consistent button placement with Remove button
Structure
Section titled “Structure”<div class="col" id="upload_{id}"> <div class="card h-100"> <input type="hidden" name="...upload_ids[]" value="{id}"> <div class="ratio ratio-1x1 overflow-hidden"> <a href="{preview_url}"> <img src="{thumbnail}" class="w-100 h-100 object-fit-cover"> </a> </div> <div class="card-footer text-center"> <div class="small text-truncate">{filename}</div> <button type="submit" class="btn btn-sm btn-outline-danger"> Remove </button> </div> </div></div>Controller Integration
Section titled “Controller Integration”Attachable Concern
Section titled “Attachable Concern”The Attachable concern provides the necessary controller actions:
concern :attachable do collection do post :attach # Upload files or attach publications delete :remove_attachment # Remove attachment get :search_library # Search publications (for modal) endendRequired Routes
Section titled “Required Routes”resources :communications, concerns: [:attachable]This generates:
POST /communications/attachDELETE /communications/remove_attachmentGET /communications/search_library
Benefits Over Legacy System
Section titled “Benefits Over Legacy System”Space Efficiency
Section titled “Space Efficiency”- Before: 3 separate panels, ~500px vertical space
- After: 1 panel with modal, ~300px vertical space
- Savings: ~40% less vertical space
User Experience
Section titled “User Experience”-
Before:
- Scroll between upload form and library search
- File preview takes up space
- Search results inline, push content down
-
After:
- All actions accessible from one panel header
- No preview, direct upload
- Search in modal, doesn’t affect page layout
- Cleaner, more professional appearance
Performance
Section titled “Performance”- Before: All panels load on page load
- After: Publication search lazy-loaded only when needed
Maintainability
Section titled “Maintainability”- Before: Logic spread across helpers, partials, and legacy JS
- After: Encapsulated in ViewComponent with clear responsibilities
Testing
Section titled “Testing”To test the component:
-
Upload Files:
- Click “Upload File” button
- Select one or multiple files
- Verify files appear in grid without page refresh
- Verify progress spinners show during upload
-
Attach Publications:
- Click “Add Publication” button
- Modal should open with search interface
- Search for a publication
- Click “Attach” on a result
- Verify publication appears in grid
- Verify publication shows checkmark in modal
-
Remove Attachments:
- Click “Remove” on any attachment
- Confirm deletion
- Verify attachment disappears without page refresh
-
New Records (no ID yet):
- Test on
/communications/new - Upload files should work
- Remove should work (deletes upload from DB)
- Test on
Future Enhancements
Section titled “Future Enhancements”- Drag & drop file upload
- Reordering attachments
- Bulk delete
- Download all as ZIP
- Image cropping/editing
- Attachment categories/tags
- Version history for attachments
Related Documentation
Section titled “Related Documentation”- Turbo Streams
- ViewComponents
- File Upload System
- Publication Management
Attachment Component - Quick Reference
Section titled “Attachment Component - Quick Reference”Basic Usage
Section titled “Basic Usage”<%# Simple usage - wrapped in card panel %><%= render Crm::AttachmentsComponent.new(context_object: @communication) %>
<%# Unwrapped - no card panel %><%= render Crm::AttachmentsComponent.new( context_object: @customer, wrapped: false) %>
<%# With options %><%= render Crm::AttachmentsComponent.new( context_object: @room_configuration, skip_publications: true, # Hide publication picker multiple_files_allowed: false, # Single file only template_options_for_select: [ # PDF template options ['Letter Portrait', 'letter_portrait'], ['Letter Landscape', 'letter_landscape'] ], category_options_for_select: [ # File categories ['Quote', 'quote'], ['Invoice', 'invoice'] ]) %>Required Controller Setup
Section titled “Required Controller Setup”Your controller must include the Attachable concern:
class CommunicationsController < ApplicationController include Controllers::Attachable
# ... your actions ...endRequired Routes
Section titled “Required Routes”resources :communications, concerns: [:attachable]This generates:
POST /communications/attach- Upload files or attach publicationsDELETE /communications/remove_attachment- Remove attachmentGET /communications/search_library- Search publications
Component Parameters
Section titled “Component Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
context_object | ActiveRecord | required | The object to attach files to |
wrapped | Boolean | true | Wrap in card panel with header |
skip_publications | Boolean | false | Hide publication picker button |
multiple_files_allowed | Boolean | true | Allow multiple file selection |
template_options_for_select | Array | nil | PDF template radio buttons |
category_options_for_select | Array | nil | File category radio buttons |
Model Requirements
Section titled “Model Requirements”Your model must have the attachable concern:
class Communication < ApplicationRecord include Models::Attachable
# This provides: # - has_many :uploads # - upload_ids getter/setterendJavaScript Controllers
Section titled “JavaScript Controllers”crm-attachments
Section titled “crm-attachments”- Purpose: Opens file picker
- Location:
app/javascript/controllers/crm_attachments_controller.js - Actions:
openFileUpload
file-upload
Section titled “file-upload”- Purpose: Handles file upload
- Location:
app/javascript/controllers/file_upload_controller.js - Auto-attached: Yes (via form data-controller)
Turbo Stream Responses
Section titled “Turbo Stream Responses”Upload Response (attach.turbo_stream.erb)
Section titled “Upload Response (attach.turbo_stream.erb)”<% @uploads.each do |upload| %> <%= turbo_stream.append "attachments-#{@context_object.id}" do %> <%= render partial: '/attachable/upload', locals: { upload: upload, context_object: @context_object, context_class: @context_class } %> <% end %><% end %>Remove Response (remove_attachment.turbo_stream.erb)
Section titled “Remove Response (remove_attachment.turbo_stream.erb)”<%= turbo_stream.remove "upload_#{@upload.id}" %>Common Issues & Solutions
Section titled “Common Issues & Solutions”Issue: Upload button does nothing
Section titled “Issue: Upload button does nothing”Cause: JavaScript not compiled
Solution: Run yarn build
Issue: Files upload but don’t appear
Section titled “Issue: Files upload but don’t appear”Cause: Turbo Stream response not being processed
Solution: Check that controller action responds with format.turbo_stream
Issue: Base64 images in HTML
Section titled “Issue: Base64 images in HTML”Cause: Using SimpleForm’s image_file input type
Solution: Use plain file_field_tag (already fixed in component)
Issue: Multiple uploads at once
Section titled “Issue: Multiple uploads at once”Cause: isUploading flag not working
Solution: Check that file-upload controller is properly connected
Issue: Publication modal doesn’t load
Section titled “Issue: Publication modal doesn’t load”Cause: Missing search_library action or route
Solution: Ensure controller includes Attachable concern and routes include :attachable
Debugging
Section titled “Debugging”Enable Console Logging
Section titled “Enable Console Logging”The controllers already have debug logging:
// In browser console, you'll see:📎 CRM Attachments controller connected📎 File upload controller connected📎 Opening file picker📎 Files selected: 2📎 Submitting upload...📎 Upload response received 200📎 Upload completeCheck Turbo Stream Response
Section titled “Check Turbo Stream Response”In Network tab, look for the POST to /attach:
- Response should have
Content-Type: text/vnd.turbo-stream.html - Response body should contain
<turbo-stream action="append">
Check Upload Records
Section titled “Check Upload Records”# In rails consoleCommunication.find(123).uploads# Should show the uploaded filesStyling
Section titled “Styling”The component uses Bootstrap 5 classes:
<!-- Card panel (when wrapped=true) --><div class="card w-100 attachments-panel"> <div class="card-header">...</div> <div class="card-body"> <!-- Responsive grid --> <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-6 g-4"> <!-- Upload cards --> </div> </div></div>Grid Breakpoints
Section titled “Grid Breakpoints”- Mobile: 1 column
- Small (576px+): 2 columns
- Medium (768px+): 3 columns
- Large (992px+): 4 columns
- XL (1200px+): 6 columns
Performance Tips
Section titled “Performance Tips”- Don’t use image previews: The component intentionally avoids base64 previews to keep HTML lightweight
- Lazy load publications: The publication picker is lazy-loaded only when the modal opens
- Use Turbo Streams: Avoid full page refreshes by using Turbo Stream responses
- Limit file sizes: Set
max_file_sizein your form validation
Security Considerations
Section titled “Security Considerations”- Authorization: The
Attachableconcern uses CanCanCan to check permissions - File validation: Upload model should validate file types and sizes
- CSRF protection: Automatically handled by Rails and included in fetch requests
- Sanitization: File names are escaped to prevent XSS
Testing
Section titled “Testing”Minitest Example
Section titled “Minitest Example”class CommunicationsControllerAttachTest < ActionDispatch::IntegrationTest test "attaches file to communication" do communication = create(:communication) file = fixture_file_upload("test.pdf", "application/pdf")
post attach_path, params: { context_object_id: communication.id, context_class: "Communication", upload: { attachment: [file] } }
assert_equal 1, communication.uploads.count assert_response :success endendSystem Test Example
Section titled “System Test Example”class AttachmentUploadTest < ApplicationSystemTestCase test "user uploads file" do communication = create(:communication) visit edit_communication_path(communication)
click_button "Upload File" attach_file "upload[attachment][]", "test/fixtures/files/test.pdf", make_visible: true
assert_text "test.pdf" assert_equal 1, communication.reload.uploads.count endendMigration from Legacy System
Section titled “Migration from Legacy System”Before (3 panels)
Section titled “Before (3 panels)”<%= attachable_panel(@communication) %><%= attachable_form @communication %>After (1 component)
Section titled “After (1 component)”<%= render Crm::AttachmentsComponent.new(context_object: @communication) %>Benefits
Section titled “Benefits”- 40% less vertical space
- Cleaner UI
- Better performance
- Easier to maintain
Related Documentation
Section titled “Related Documentation”- Full Component Documentation
- Upload Flow Diagram
- Turbo Streams Guide
- ViewComponents Guide
Attachment Upload Flow
Section titled “Attachment Upload Flow”Architecture Overview
Section titled “Architecture Overview”┌─────────────────────────────────────────────────────────────────┐│ Crm::AttachmentsComponent ││ ││ ┌────────────────────────────────────────────────────────────┐ ││ │ Card Header │ ││ │ ┌──────────────────┐ ┌──────────────────────────────┐ │ ││ │ │ Upload File Btn │ │ Add Publication Btn │ │ ││ │ │ (crm-attachments)│ │ (opens modal) │ │ ││ │ └──────────────────┘ └──────────────────────────────┘ │ ││ └────────────────────────────────────────────────────────────┘ ││ ││ ┌────────────────────────────────────────────────────────────┐ ││ │ Hidden Form (file-upload controller) │ ││ │ ┌──────────────────────────────────────────────────────┐ │ ││ │ │ <input type="file" multiple> │ │ ││ │ │ NO image-preview controller │ │ ││ │ │ NO base64 encoding │ │ ││ │ └──────────────────────────────────────────────────────┘ │ ││ └────────────────────────────────────────────────────────────┘ ││ ││ ┌────────────────────────────────────────────────────────────┐ ││ │ Turbo Frame: attachments-frame-{id} │ ││ │ ┌────────────────────────────────────────────────────┐ │ ││ │ │ Attachments Grid: #attachments-{id} │ │ ││ │ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │ ││ │ │ │ Card │ │ Card │ │ Card │ │ Card │ ... │ │ ││ │ │ └──────┘ └──────┘ └──────┘ └──────┘ │ │ ││ │ └────────────────────────────────────────────────────┘ │ ││ └────────────────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────────────┘Upload Flow Sequence
Section titled “Upload Flow Sequence”┌──────────┐ ┌──────────────────┐ ┌─────────────┐ ┌────────────┐│ User │ │ crm-attachments │ │ file-upload │ │ Server ││ │ │ Controller │ │ Controller │ │ │└────┬─────┘ └────────┬─────────┘ └──────┬──────┘ └─────┬──────┘ │ │ │ │ │ 1. Click "Upload │ │ │ │ File" button │ │ │ ├────────────────────>│ │ │ │ │ │ │ │ │ 2. Find file input │ │ │ │ and trigger click()│ │ │ ├──────────────────────>│ │ │ │ │ │ │ 3. OS file picker │ │ │ │ opens │ │ │ │<───────────────────── │ │ │ │ │ │ │ 4. User selects │ │ │ │ file(s) │ │ │ │─────────────────────────────────────────────>│ │ │ │ │ │ │ │ │ 5. handleFileSelection() │ │ │ (capture phase) │ │ │ │ │ │ │ │ 6. Show progress │ │ │ │ spinners │ │ │ │ │ │ 7. Progress │ │ │ │ spinners appear │ │ │ │<─────────────────────────────────────────────┤ │ │ │ │ │ │ │ │ 8. Submit via fetch│ │ │ │ with FormData │ │ │ ├────────────────────>│ │ │ │ │ │ │ │ │ 9. Create Upload │ │ │ │ records │ │ │ │ │ │ │ 10. Turbo Stream │ │ │ │ response │ │ │ │<────────────────────┤ │ │ │ │ │ │ │ 11. Render stream │ │ │ │ (append cards) │ │ │ │ │ │ 12. Upload cards │ │ │ │ appear in grid │ │ │ │<─────────────────────────────────────────────┤ │ │ │ │ │ │ │ │ 13. Remove progress│ │ │ │ spinners │ │ │ │ │ │ │ │ 14. Reset form │ │ │ │ │ │ 15. Ready for │ │ │ │ next upload │ │ │ │<─────────────────────────────────────────────┤ │ │ │ │ │Controller Responsibilities
Section titled “Controller Responsibilities”crm-attachments Controller
Section titled “crm-attachments Controller”// ONLY handles UI interactionsopenFileUpload(event) { event.preventDefault() const fileInput = this.element.querySelector('input[type="file"]') fileInput.click() // Opens OS file picker}file-upload Controller
Section titled “file-upload Controller”// Handles ALL upload logicconnect() { this.setupFileUpload()}
setupFileUpload() { // Use capture phase to run before other handlers fileInput.addEventListener('change', handler, true)}
handleFileSelection(event) { // 1. Prevent duplicates // 2. Show progress UI // 3. Submit via fetch // 4. Process Turbo Stream response // 5. Clean up}Key Technical Decisions
Section titled “Key Technical Decisions”1. No Image Preview Controller
Section titled “1. No Image Preview Controller”Problem: SimpleForm’s image_file input automatically adds image-preview controller which:
- Reads files as base64 data URLs
- Embeds large base64 strings in HTML
- Causes DOM bloat and memory issues
Solution: Use plain file_field_tag without any preview controller
2. Capture Phase Event Handling
Section titled “2. Capture Phase Event Handling”Problem: Multiple event listeners on the same input can conflict
Solution: Use capture phase (addEventListener(..., true)) to ensure file-upload controller runs first
// Capture phase (runs first, from root to target)fileInput.addEventListener('change', handler, true)
// Bubble phase (runs after, from target to root)fileInput.addEventListener('change', handler, false) // default3. Dual Controller Pattern
Section titled “3. Dual Controller Pattern”Problem: Mixing UI interactions with upload logic creates complex, hard-to-maintain code
Solution: Separate concerns:
crm-attachments: UI interactions (button clicks)file-upload: Upload logic (file handling, XHR, Turbo Streams)
Turbo Stream Response
Section titled “Turbo Stream Response”Server responds with:
<% @uploads.each do |upload| %> <%= turbo_stream.append "attachments-#{@context_object.id}" do %> <%= render partial: '/attachable/upload', locals: { upload: upload, context_object: @context_object } %> <% end %><% end %>This appends new upload cards to the grid without page refresh.
Progress Feedback
Section titled “Progress Feedback”While uploading, temporary progress elements are shown:
<div class="col upload-in-progress"> <div class="card h-100"> <div class="card-body text-center"> <i class="fa-solid fa-spinner fa-spin fa-2x mb-2"></i> <p class="small mb-0">Uploading filename.pdf...</p> </div> </div></div>These are removed when the Turbo Stream response arrives with the actual upload cards.
Error Handling
Section titled “Error Handling”- File Selection Cancelled: No action taken
- Upload in Progress: Duplicate submissions prevented by
isUploadingflag - Server Error: Progress spinners removed, alert shown to user
- Network Error: Caught by fetch
.catch(), alert shown to user
Testing the Flow
Section titled “Testing the Flow”Manual Test
Section titled “Manual Test”- Open browser console
- Navigate to a page with the attachment component
- Click “Upload File”
- Watch console logs:
📎 CRM Attachments controller connected📎 File upload controller connected📎 Opening file picker📎 handleFileSelection triggered📎 Files selected: 2📎 Submitting upload...📎 Upload response received 200📎 Upload complete
Verify No Base64
Section titled “Verify No Base64”- Upload a file
- View page source (Cmd+U)
- Search for “data:image” or “base64”
- Should find NONE in the attachments section
Performance Characteristics
Section titled “Performance Characteristics”Before (with image-preview)
Section titled “Before (with image-preview)”- 1 MB image → ~1.3 MB base64 in HTML
- Multiple images → Megabytes of DOM bloat
- Slow page rendering
- High memory usage
After (without image-preview)
Section titled “After (without image-preview)”- 1 MB image → ~200 bytes in HTML (just the upload card)
- Multiple images → Minimal DOM impact
- Fast page rendering
- Low memory usage
Browser Compatibility
Section titled “Browser Compatibility”- ✅ Chrome/Edge (Chromium)
- ✅ Firefox
- ✅ Safari
- ✅ Mobile browsers (iOS Safari, Chrome Mobile)
Requires:
fetchAPI (all modern browsers)FormDataAPI (all modern browsers)- Turbo 7+ (included in project)
Presigned URL Uploads with Uppy and Dragonfly
Section titled “Presigned URL Uploads with Uppy and Dragonfly”This document explains how to use the new presigned URL upload functionality that allows Uppy to upload files directly to Wasabi S3 storage while still using Dragonfly and the Upload model for backend processing.
Overview
Section titled “Overview”The presigned URL upload system provides:
- Direct uploads to Wasabi S3 storage (bypassing Rails server for file transfer)
- Better performance for large files and multiple uploads
- Reduced server load during file uploads
- Maintained compatibility with existing Dragonfly and Upload model workflows
Architecture
Section titled “Architecture”- Client requests presigned URL from Rails backend
- Rails generates presigned URL using AWS SDK for Wasabi S3
- Uppy uploads file directly to Wasabi using presigned URL
- Rails creates Upload record and processes file with Dragonfly after successful upload
Option 1: New S3 Uploader Controller
Section titled “Option 1: New S3 Uploader Controller”Use the dedicated S3 uploader controller for new implementations:
<%= uppy_s3_uploader( presigned_url_endpoint: presigned_url_uploads_path, upload_complete_endpoint: upload_complete_uploads_path, max_files: 5, max_file_size: 50.megabytes, allowed_file_types: ['image/*'], hidden_field_name: 'upload_ids', resource_type: 'Customer', resource_id: @customer.id, category: 'photo') %>Option 2: Enhanced Existing Uploader
Section titled “Option 2: Enhanced Existing Uploader”Use the existing uploader with S3 mode for backward compatibility:
<%= uppy_uploader( endpoint: upload_uploads_path, # Still needed for XHR fallback presigned_url_endpoint: presigned_url_uploads_path, upload_complete_endpoint: upload_complete_uploads_path, mode: 's3', # Enable S3 mode max_files: 5, max_file_size: 50.megabytes, allowed_file_types: ['image/*'], hidden_field_name: 'upload_ids', resource_type: 'Customer', resource_id: @customer.id, category: 'photo') %>Option 3: Helper Methods
Section titled “Option 3: Helper Methods”Use the provided helper methods for common scenarios:
<!-- RMA image uploads with S3 --><%= rma_image_uploader_s3(@rma) %>
<!-- Large file uploads with S3 --><%= large_file_uploader_s3 %>
<!-- Dedicated S3 helpers --><%= rma_image_s3_uploader(@rma) %><%= large_file_s3_uploader %>Configuration
Section titled “Configuration”Required Dependencies
Section titled “Required Dependencies”The system requires the @uppy/aws-s3 package, which has been added to package.json:
{ "@uppy/aws-s3": "^5.0.0"}Environment Variables
Section titled “Environment Variables”The system uses existing Dragonfly S3 configuration:
S3_DATASTORE_ACCESS_KEYS3_DATASTORE_SECRET_KEY- Wasabi endpoint and bucket configuration
Routes
Section titled “Routes”New routes have been added to both CRM and WWW namespaces:
# CRM routesresources :uploads do collection do get :presigned_url post :upload_complete endend
# WWW routesnamespace :www do resources :uploads, only: [] do collection do get :presigned_url post :upload_complete end endendAPI Endpoints
Section titled “API Endpoints”GET /uploads/presigned_url
Section titled “GET /uploads/presigned_url”Generates a presigned URL for direct S3 upload.
Parameters:
file_name(required): Name of the file to uploadcontent_type(required): MIME type of the filefile_size(required): Size of the file in bytescategory(optional): Upload category (default: ‘photo’)resource_type(optional): Associated resource typeresource_id(optional): Associated resource ID
Response:
{ "presigned_url": "https://s3.us-central-1.wasabisys.com/bucket/key?signature=...", "key": "secure_assets/development/2024/01/15/10/30/45/uuid/filename.jpg", "bucket": "heatwave-assets-usc1", "region": "us-central-1"}POST /uploads/upload_complete
Section titled “POST /uploads/upload_complete”Creates Upload record after successful S3 upload.
Parameters:
key(required): S3 key of the uploaded filefile_name(required): Original filenamecontent_type(required): MIME typefile_size(required): File size in bytescategory(optional): Upload categoryresource_type(optional): Associated resource typeresource_id(optional): Associated resource ID
Response:
{ "message": "Upload completed successfully.", "files_list": "[123]", "upload_ids": [123]}Benefits
Section titled “Benefits”Performance
Section titled “Performance”- Faster uploads: Files go directly to S3, bypassing Rails server
- Reduced server load: No file data passes through Rails application
- Better scalability: S3 handles the heavy lifting for file transfers
Reliability
Section titled “Reliability”- Resumable uploads: Uppy can resume interrupted uploads
- Progress tracking: Real-time upload progress
- Error handling: Comprehensive error handling and retry logic
Compatibility
Section titled “Compatibility”- Dragonfly integration: Files are processed by Dragonfly after upload
- Upload model: Standard Upload records are created
- Existing workflows: Compatible with existing file management systems
Migration Guide
Section titled “Migration Guide”From XHR to S3 Mode
Section titled “From XHR to S3 Mode”-
Update helper calls to include S3 parameters:
<!-- Before --><%= uppy_uploader(endpoint: upload_uploads_path) %><!-- After --><%= uppy_uploader(endpoint: upload_uploads_path,presigned_url_endpoint: presigned_url_uploads_path,upload_complete_endpoint: upload_complete_uploads_path,mode: 's3') %> -
Use helper methods for common scenarios:
<!-- Before --><%= rma_image_uploader(@rma) %><!-- After --><%= rma_image_uploader_s3(@rma) %> -
Test thoroughly to ensure compatibility with existing workflows
Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”- CORS errors: Ensure Wasabi bucket has proper CORS configuration
- Presigned URL errors: Check AWS credentials and permissions
- Upload completion failures: Verify backend endpoints are accessible
Debugging
Section titled “Debugging”Enable debug mode in Uppy controllers:
// In the controllerthis.uppy = new Uppy({ debug: true, // Enable debug logging // ... other options})Check browser console for detailed error messages and network requests.
Security Considerations
Section titled “Security Considerations”- Presigned URLs expire after 1 hour
- File size limits are enforced (500MB maximum)
- Content type validation prevents malicious file uploads
- CSRF protection is maintained for all endpoints
- Authorization is required for all upload operations
Future Enhancements
Section titled “Future Enhancements”- Multipart uploads for very large files
- Upload progress integration with existing UI
- Batch upload optimization
- Custom metadata support
- Upload validation before S3 upload