Attachments & Uploads
Overview
The Crm::AttachmentsComponent is a modern, consolidated ViewComponent that replaces the legacy three-panel attachment system with a single, clean interface.
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
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)
Usage
Basic Usage
<%= render Crm::AttachmentsComponent.new(context_object: @communication) %>
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)
For embedding in custom layouts:
<%= render Crm::AttachmentsComponent.new(
context_object: @customer,
wrapped: false
) %>
Migration from Legacy System
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)
<%= 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
-
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
Files
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 removals
Turbo Integration
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
-
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
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
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
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
- 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
The Crm::UploadCardComponent renders each individual attachment:
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
<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
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)
end
end
Required Routes
resources :communications, concerns: [:attachable]
This generates:
POST /communications/attachDELETE /communications/remove_attachmentGET /communications/search_library
Benefits Over Legacy System
Space Efficiency
- Before: 3 separate panels, ~500px vertical space
- After: 1 panel with modal, ~300px vertical space
- Savings: ~40% less vertical space
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
- Before: All panels load on page load
- After: Publication search lazy-loaded only when needed
Maintainability
- Before: Logic spread across helpers, partials, and legacy JS
- After: Encapsulated in ViewComponent with clear responsibilities
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
- Drag & drop file upload
- Reordering attachments
- Bulk delete
- Download all as ZIP
- Image cropping/editing
- Attachment categories/tags
- Version history for attachments
Related Documentation
Attachment Component - Quick Reference
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
Your controller must include the Attachable concern:
class CommunicationsController < ApplicationController
include Controllers::Attachable
# ... your actions ...
end
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
| 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
Your model must have the attachable concern:
class Communication < ApplicationRecord
include Models::Attachable
# This provides:
# - has_many :uploads
# - upload_ids getter/setter
end
JavaScript Controllers
crm-attachments
- Purpose: Opens file picker
- Location:
app/javascript/controllers/crm_attachments_controller.js - Actions:
openFileUpload
file-upload
- Purpose: Handles file upload
- Location:
app/javascript/controllers/file_upload_controller.js - Auto-attached: Yes (via form data-controller)
Turbo Stream Responses
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)
<%= turbo_stream.remove "upload_#{@upload.id}" %>
Common Issues & Solutions
Issue: Upload button does nothing
Cause: JavaScript not compiled
Solution: Run yarn build
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
Cause: Using SimpleForm's image_file input type
Solution: Use plain file_field_tag (already fixed in component)
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
Cause: Missing search_library action or route
Solution: Ensure controller includes Attachable concern and routes include :attachable
Debugging
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 complete
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
# In rails console
Communication.find(123).uploads
# Should show the uploaded files
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
- Mobile: 1 column
- Small (576px+): 2 columns
- Medium (768px+): 3 columns
- Large (992px+): 4 columns
- XL (1200px+): 6 columns
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
- 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
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
end
end
System Test Example
class AttachmentUploadTest < ApplicationSystemTestCase
test "user uploads file" do
communication = create(:communication)
visit edit_communication_path(communication)
"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
end
end
Migration from Legacy System
Before (3 panels)
<%= attachable_panel(@communication) %>
<%= attachable_form @communication %>
After (1 component)
<%= render Crm::AttachmentsComponent.new(context_object: @communication) %>
Benefits
- 40% less vertical space
- Cleaner UI
- Better performance
- Easier to maintain
Related Documentation
Attachment Upload Flow
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
┌──────────┐ ┌──────────────────┐ ┌─────────────┐ ┌────────────┐
│ 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
crm-attachments Controller
// ONLY handles UI interactions
openFileUpload(event) {
event.preventDefault()
const fileInput = this.element.querySelector('input[type="file"]')
fileInput.click() // Opens OS file picker
}
file-upload Controller
// Handles ALL upload logic
connect() {
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
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
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) // default
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
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
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
- 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
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
- Upload a file
- View page source (Cmd+U)
- Search for "data:image" or "base64"
- Should find NONE in the attachments section
Performance Characteristics
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)
- 1 MB image → ~200 bytes in HTML (just the upload card)
- Multiple images → Minimal DOM impact
- Fast page rendering
- Low memory usage
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
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
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
- 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
Usage
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
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
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
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
The system uses existing Dragonfly S3 configuration:
S3_DATASTORE_ACCESS_KEYS3_DATASTORE_SECRET_KEY- Wasabi endpoint and bucket configuration
Routes
New routes have been added to both CRM and WWW namespaces:
# CRM routes
resources :uploads do
collection do
get :presigned_url
post :upload_complete
end
end
# WWW routes
namespace :www do
resources :uploads, only: [] do
collection do
get :presigned_url
post :upload_complete
end
end
end
API Endpoints
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
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
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
- Resumable uploads: Uppy can resume interrupted uploads
- Progress tracking: Real-time upload progress
- Error handling: Comprehensive error handling and retry logic
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
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
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
Enable debug mode in Uppy controllers:
// In the controller
this.uppy = new Uppy({
debug: true, // Enable debug logging
// ... other options
})
Check browser console for detailed error messages and network requests.
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
- Multipart uploads for very large files
- Upload progress integration with existing UI
- Batch upload optimization
- Custom metadata support
- Upload validation before S3 upload