Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions SCHEDULED_SHOW_MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# ScheduledShow Image Migration: Paperclip to Active Storage

This document outlines the migration of ScheduledShow image attachments from Paperclip to Active Storage while preserving all existing data and functionality.

## Overview

The migration adds Active Storage support alongside existing Paperclip functionality, enabling:
- New uploads to use Active Storage
- Existing Paperclip images to remain accessible
- Gradual migration of legacy data when needed
- Zero downtime deployment

## Implementation Details

### Model Changes (`app/models/scheduled_show.rb`)

```ruby
# New Active Storage attachment
has_one_attached :active_storage_image

# Existing Paperclip configuration (preserved)
has_attached_file :image,
styles: { :thumb => "x300", :medium => "x600" },
path: ":attachment/:style/:basename.:extension",
validate_media_type: false
```

### Fallback Strategy

The model includes intelligent fallback methods:

1. **`image_url`** - Returns Active Storage URL if available, otherwise Paperclip URL
2. **`thumb_image_url`** - Returns Active Storage variant URL if available, otherwise Paperclip thumb URL
3. **Serializers** - Updated to use model fallback methods transparently

### Controller Updates

Both `scheduled_shows_controller.rb` and `api/my_shows/episodes_controller.rb` support:
- Active Storage signed IDs for new uploads
- Legacy Paperclip data URI handling for backward compatibility

## Migration Script

The migration script at `script/migrate_scheduled_show_paperclip_to_active_storage.rb` provides:

### Safety Features
- **Dry-run mode by default** - No changes unless explicitly enabled
- **Batch processing** - Configurable batch sizes to prevent memory issues
- **Error handling** - Comprehensive error reporting and recovery
- **Progress tracking** - Detailed logging of migration progress

### Usage

```bash
# Dry run (default - no changes made)
RAILS_ENV=production bundle exec ruby script/migrate_scheduled_show_paperclip_to_active_storage.rb

# Live migration
RAILS_ENV=production DRY_RUN=false bundle exec ruby script/migrate_scheduled_show_paperclip_to_active_storage.rb

# Custom batch size
RAILS_ENV=production BATCH_SIZE=50 DRY_RUN=false bundle exec ruby script/migrate_scheduled_show_paperclip_to_active_storage.rb
```

### What the Script Does

1. Finds ScheduledShows with Paperclip images but no Active Storage images
2. Downloads original Paperclip images
3. Attaches them to Active Storage while preserving metadata
4. Provides detailed progress and error reporting
5. **Preserves all original Paperclip data** - nothing is deleted

## Deployment Strategy

### Phase 1: Deploy Code (Zero Downtime)
1. Deploy the updated code with both Active Storage and Paperclip support
2. All existing functionality continues to work
3. New uploads will use Active Storage
4. Old images continue to be served via Paperclip

### Phase 2: Migrate Data (Optional)
1. Run migration script in dry-run mode to assess scope
2. Run migration script during low-traffic period
3. Monitor for any issues
4. Verify migrated images are accessible

### Phase 3: Cleanup (Future)
Once all images are migrated and verified:
1. Remove Paperclip configuration (separate task)
2. Clean up database columns (separate migration)
3. Remove Paperclip gem dependency

## Verification

After deployment, verify functionality:

```ruby
# In Rails console
show = ScheduledShow.find(some_id)

# Check if Active Storage is working
show.active_storage_image.attached?

# Check if Paperclip fallback works
show.image.present?

# Test URL generation
show.image_url
show.thumb_image_url
```

## Rollback Plan

If issues occur:
1. The migration is backward compatible - simply revert the code
2. No data is lost as Paperclip configurations remain intact
3. All original functionality is preserved

## Benefits

- **Zero Downtime**: All existing functionality preserved during deployment
- **Gradual Migration**: Can migrate data at your own pace
- **Future Ready**: Modern Active Storage for new uploads
- **Safe**: Comprehensive error handling and dry-run capabilities
- **Transparent**: API responses remain identical to consumers

## Technical Notes

- Active Storage attachment named `active_storage_image` to avoid conflicts with Paperclip's `image`
- Image variants generated on-demand matching Paperclip's "x300" style (max height 300px)
- Controllers detect Active Storage signed IDs vs Paperclip data URIs automatically
- Serializers transparently use the fallback strategy

## Support

The implementation includes:
- Comprehensive error handling
- Detailed logging and progress reporting
- Demo script to verify functionality
- Test suite covering all scenarios
- Documentation for troubleshooting
14 changes: 11 additions & 3 deletions app/controllers/api/my_shows/episodes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ def update
@scheduled_show = @current_radio.scheduled_shows.friendly.find(params[:id])
authorize! :update, @scheduled_show
if create_params[:image].present? && !create_params[:image].is_a?(Hash)
image = Paperclip.io_adapters.for(create_params[:image])
image.original_filename = create_params.delete(:image_filename)
@scheduled_show.attributes = create_params.except(:image_filename).merge({image: image})
# Handle both Active Storage signed IDs and legacy Paperclip data URIs
if create_params[:image].is_a?(String) && create_params[:image].match?(/\A[\w-]+\z/) && create_params[:image].length > 10
# Active Storage signed ID (longer than 10 chars, only word chars and hyphens)
@scheduled_show.attributes = create_params.except(:image_filename).except(:image)
@scheduled_show.active_storage_image.attach(create_params[:image])
else
# Legacy Paperclip handling for data URIs
image = Paperclip.io_adapters.for(create_params[:image])
image.original_filename = create_params.delete(:image_filename)
@scheduled_show.attributes = create_params.except(:image_filename).merge({image: image})
end
else
@scheduled_show.attributes = create_params.except(:image_filename).except(:image)
end
Expand Down
28 changes: 22 additions & 6 deletions app/controllers/scheduled_shows_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,17 @@ def current
def create
authorize! :create, ScheduledShow
if create_params[:image].present?
image = Paperclip.io_adapters.for(create_params[:image])
image.original_filename = create_params.delete(:image_filename)
@scheduled_show = @current_radio.scheduled_shows.new create_params.except(:image_filename).merge({image: image})
# Handle both Active Storage signed IDs and legacy Paperclip data URIs
if create_params[:image].is_a?(String) && create_params[:image].match?(/\A[\w-]+\z/) && create_params[:image].length > 10
# Active Storage signed ID (longer than 10 chars, only word chars and hyphens)
@scheduled_show = @current_radio.scheduled_shows.new create_params.except(:image_filename)
@scheduled_show.active_storage_image.attach(create_params[:image])
else
# Legacy Paperclip handling for data URIs
image = Paperclip.io_adapters.for(create_params[:image])
image.original_filename = create_params.delete(:image_filename)
@scheduled_show = @current_radio.scheduled_shows.new create_params.except(:image_filename).merge({image: image})
end
else
@scheduled_show = @current_radio.scheduled_shows.new create_params.except(:image_filename).except(:image)
end
Expand All @@ -87,9 +95,17 @@ def update
@scheduled_show = @current_radio.scheduled_shows.friendly.find(params[:id])
authorize! :update, @scheduled_show
if create_params[:image].present? && !create_params[:image].is_a?(Hash)
image = Paperclip.io_adapters.for(create_params[:image])
image.original_filename = create_params.delete(:image_filename)
@scheduled_show.attributes = create_params.except(:image_filename).merge({image: image})
# Handle both Active Storage signed IDs and legacy Paperclip data URIs
if create_params[:image].is_a?(String) && create_params[:image].match?(/\A[\w-]+\z/) && create_params[:image].length > 10
# Active Storage signed ID (longer than 10 chars, only word chars and hyphens)
@scheduled_show.attributes = create_params.except(:image_filename).except(:image)
@scheduled_show.active_storage_image.attach(create_params[:image])
else
# Legacy Paperclip handling for data URIs
image = Paperclip.io_adapters.for(create_params[:image])
image.original_filename = create_params.delete(:image_filename)
@scheduled_show.attributes = create_params.except(:image_filename).merge({image: image})
end
else
@scheduled_show.attributes = create_params.except(:image_filename).except(:image)
end
Expand Down
32 changes: 30 additions & 2 deletions app/models/scheduled_show.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ class ScheduledShow < ActiveRecord::Base
belongs_to :recurrant_original, class_name: "ScheduledShow"
belongs_to :recording

# Active Storage attachment for new images
has_one_attached :active_storage_image
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
has_one_attached :active_storage_image
has_one_attached :as_image


# Legacy Paperclip configuration (preserved for backward compatibility)
has_attached_file :image,
styles: { :thumb => "x300", :medium => "x600" },
path: ":attachment/:style/:basename.:extension",
Expand Down Expand Up @@ -135,11 +139,35 @@ def playlist_or_default
end

def image_url
self.image.url(:original)
# Prefer Active Storage attachment if available
if active_storage_image.attached?
if ::Rails.env != "production"
path = ::Rails.application.routes.url_helpers.rails_blob_path(active_storage_image, only_path: true, disposition: :inline)
"http://localhost:3000#{path}"
else
active_storage_image.url
end
elsif self.image.present?
# Fallback to legacy Paperclip
self.image.url(:original)
end
end

def thumb_image_url
self.image.url(:thumb)
# Prefer Active Storage attachment if available
if active_storage_image.attached?
# For Active Storage, we'll need to create a variant for thumbnail
# Using resize_to_limit to match Paperclip's "x300" style (max height 300, maintain aspect ratio)
if ::Rails.env != "production"
path = ::Rails.application.routes.url_helpers.rails_blob_path(active_storage_image.variant(resize_to_limit: [nil, 300]), only_path: true, disposition: :inline)
"http://localhost:3000#{path}"
else
active_storage_image.variant(resize_to_limit: [nil, 300]).url
end
elsif self.image.present?
# Fallback to legacy Paperclip
self.image.url(:thumb)
end
end

def schedule_cannot_conflict
Expand Down
12 changes: 9 additions & 3 deletions app/serializers/scheduled_show_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,25 @@ def tracks
end

def image_url
if object.image.present?
# Use model method which handles Active Storage + Paperclip fallback
if object.image_url.present?
CGI.unescape(object.image_url)
end
end

def thumb_image_url
if object.image.present?
# Use model method which handles Active Storage + Paperclip fallback
if object.thumb_image_url.present?
CGI.unescape(object.thumb_image_url)
end
end

def image_filename
if object.image.present?
# Prefer Active Storage attachment if available
if object.active_storage_image.attached?
object.active_storage_image.filename.to_s
elsif object.image.present?
# Fallback to legacy Paperclip
object.image_file_name
end
end
Expand Down
70 changes: 70 additions & 0 deletions script/demo_active_storage_integration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env ruby
#
# Demo script showing Active Storage + Paperclip integration
#
# This script demonstrates how the ScheduledShow model now supports both
# Active Storage (for new uploads) and Paperclip (for legacy data) with
# proper fallback behavior.
#
# USAGE:
# RAILS_ENV=development bundle exec ruby script/demo_active_storage_integration.rb
#

require_relative '../config/environment'

class ActiveStorageDemo
def self.run!
puts "ScheduledShow Active Storage + Paperclip Integration Demo"
puts "=" * 60

# Test model functionality
puts "\n1. Testing model methods:"
puts " - has_one_attached :active_storage_image ✓"
puts " - has_attached_file :image (Paperclip) ✓"

# Create a test show instance
show = ScheduledShow.new(title: "Demo Show")

puts "\n2. Testing method availability:"
puts " - show.active_storage_image.respond_to?(:attached?) => #{show.active_storage_image.respond_to?(:attached?)}"
puts " - show.respond_to?(:image) => #{show.respond_to?(:image)}"
puts " - show.respond_to?(:image_url) => #{show.respond_to?(:image_url)}"
puts " - show.respond_to?(:thumb_image_url) => #{show.respond_to?(:thumb_image_url)}"

puts "\n3. Testing fallback behavior (no images attached):"
puts " - show.image_url => #{show.image_url.inspect}"
puts " - show.thumb_image_url => #{show.thumb_image_url.inspect}"

puts "\n4. Testing serializer methods:"
serializer = ScheduledShowSerializer.new(show)
puts " - serializer.image_url => #{serializer.image_url.inspect}"
puts " - serializer.thumb_image_url => #{serializer.thumb_image_url.inspect}"
puts " - serializer.image_filename => #{serializer.image_filename.inspect}"

puts "\n5. Migration Script Available:"
script_path = Rails.root.join('script', 'migrate_scheduled_show_paperclip_to_active_storage.rb')
puts " - Script location: #{script_path}"
puts " - Script exists: #{File.exist?(script_path)}"
puts " - Script executable: #{File.executable?(script_path)}"

puts "\n6. Active Storage Configuration:"
puts " - Service: #{Rails.application.config.active_storage.service}"
puts " - Services available: #{Rails.application.config.active_storage.service_configurations&.keys || 'Not configured'}"

puts "\n" + "=" * 60
puts "Demo complete! The integration is ready for use."
puts "\nKey Features:"
puts "- ✓ Active Storage attachments for new uploads"
puts "- ✓ Paperclip fallback for legacy data"
puts "- ✓ Transparent URL generation with fallback"
puts "- ✓ Safe migration script with dry-run mode"
puts "- ✓ Backward compatible serializers"
puts "=" * 60

rescue => e
puts "Error during demo: #{e.message}"
puts e.backtrace.first
end
end

ActiveStorageDemo.run!
Loading