Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate from Paperclip (EOL) to Active Storage #1114

Merged
merged 8 commits into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ Design

.passenger
.vagrant
storage/
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,5 @@ gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
gem 'activejob'
gem 'ransack_ui'
gem 'bootstrap', '5.0.0'
gem 'mini_magick'
gem 'image_processing', '~> 1.2'
24 changes: 8 additions & 16 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ GEM
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
chronic (0.10.2)
climate_control (0.2.0)
coderay (1.1.3)
coffee-rails (5.0.0)
coffee-script (>= 2.2.0)
Expand Down Expand Up @@ -199,6 +198,9 @@ GEM
htmlentities (4.3.4)
i18n (1.13.0)
concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
jquery-migrate-rails (1.2.1)
jquery-rails (4.5.1)
rails-dom-testing (>= 1, < 3)
Expand All @@ -221,12 +223,7 @@ GEM
marcel (1.0.2)
matrix (0.4.2)
method_source (1.0.0)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2021.0901)
mimemagic (0.3.10)
nokogiri (~> 1)
rake
mini_magick (4.12.0)
mini_mime (1.1.2)
mini_portile2 (2.8.2)
mini_racer (0.6.4)
Expand All @@ -250,12 +247,6 @@ GEM
paper_trail (12.0.0)
activerecord (>= 5.2)
request_store (~> 1.1)
paperclip (6.1.0)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
mime-types
mimemagic (~> 0.3.0)
terrapin (~> 0.6.0)
parallel (1.23.0)
parser (3.2.2.1)
ast (~> 2.4.1)
Expand Down Expand Up @@ -382,6 +373,8 @@ GEM
rubocop-ast (1.28.1)
parser (>= 3.2.1.0)
ruby-progressbar (1.13.0)
ruby-vips (2.1.4)
ffi (~> 1.12)
rubyzip (2.3.2)
sass (3.7.4)
sass-listen (~> 4.0.0)
Expand Down Expand Up @@ -417,8 +410,6 @@ GEM
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
temple (0.8.2)
terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0)
thor (1.2.2)
tilt (2.0.10)
timecop (0.9.6)
Expand Down Expand Up @@ -483,13 +474,14 @@ DEPENDENCIES
guard-rspec
haml
headless
image_processing (~> 1.2)
jquery-migrate-rails
jquery-rails
jquery-ui-rails
mini_magick
mini_racer
nokogiri (>= 1.8.1)
paper_trail (~> 12.0.0)
paperclip
pg
premailer
pry-rails
Expand Down
24 changes: 22 additions & 2 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -300,16 +300,36 @@ def get_browser_timezone_offset
raw "$.get('#{timezone_path}', {offset: (new Date()).getTimezoneOffset()});" unless session[:timezone_offset]
end

STYLES = { large: "75x75#", medium: "50x50#", small: "25x25#", thumb: "16x16#" }.freeze

# Convert STYLE symbols to 'w x h' format for Gravatar and Rails
# e.g. size_from_style(:size => :large) -> '75x75'
# Allow options to contain :width and :height override keys
#----------------------------------------------------------------------------
def size_from_style!(options)
if options[:width] && options[:height]
options[:size] = %i[width height].map { |d| options[d] }.join("x")
options.delete(:width)
options.delete(:height)
elsif STYLES.keys.include?(options[:size])
options[:size] = STYLES[options[:size]].sub(/\#\z/, '')
end
options
end

# Entities can have associated avatars or gravatars. Only calls Gravatar
# in production env. Gravatar won't serve default images if they are not
# publically available: https://en.gravatar.com/site/implement/images
#----------------------------------------------------------------------------
def avatar_for(model, args = {})
args = { class: 'gravatar', size: :large }.merge(args)
if model.respond_to?(:avatar) && model.avatar.present?
image_tag(model.avatar.image.url(args.delete(:size)), args)
args = size_from_style!(args) # convert size format :large => '75x75'
size = args[:size].split('x').map(&:to_i) # convert '75x75' into [75, 75]

image_tag model.avatar.image.variant(resize_to_limit: size)
else
args = Avatar.size_from_style!(args) # convert size format :large => '75x75'
args = size_from_style!(args) # convert size format :large => '75x75'
gravatar_image_tag(model.email, args)
end
end
Expand Down
30 changes: 1 addition & 29 deletions app/models/polymorphic/avatar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,39 +21,11 @@
#

class Avatar < ActiveRecord::Base
STYLES = { large: "75x75#", medium: "50x50#", small: "25x25#", thumb: "16x16#" }.freeze

belongs_to :user
belongs_to :entity, polymorphic: true

# We want to store avatars in separate directories based on entity type
# (i.e. /avatar/User/, /avatars/Lead/, etc.), so we are adding :entity_type
# interpolation to the Paperclip::Interpolations module. Also, Paperclip
# doesn't seem to care preserving styles hash so we must use STYLES.dup.
#----------------------------------------------------------------------------
Paperclip::Interpolations.module_eval do
def entity_type(attachment, _style_name = nil)
attachment.instance.entity_type
end
end
has_attached_file :image, styles: STYLES.dup, url: "/avatars/:entity_type/:id/:style_:filename", default_url: "/assets/avatar.jpg"
validates_attachment :image, presence: true,
content_type: { content_type: ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'] }

# Convert STYLE symbols to 'w x h' format for Gravatar and Rails
# e.g. Avatar.size_from_style(:size => :large) -> '75x75'
# Allow options to contain :width and :height override keys
#----------------------------------------------------------------------------
def self.size_from_style!(options)
if options[:width] && options[:height]
options[:size] = %i[width height].map { |d| options[d] }.join("x")
options.delete(:width)
options.delete(:height)
elsif Avatar::STYLES.keys.include?(options[:size])
options[:size] = Avatar::STYLES[options[:size]].sub(/\#\z/, '')
end
options
end
has_one_attached :image

ActiveSupport.run_load_hooks(:fat_free_crm_avatar, self)
end
1 change: 1 addition & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

# Pick the frameworks you want:
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "sprockets/railtie"
Expand Down
3 changes: 3 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,8 @@
# Checks for improperly declared sprockets dependencies.
# Raises helpful error messages.
config.assets.raise_runtime_errors = true

# Store files locally.
config.active_storage.service = :local
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
def change
# Use Active Record's configured type for primary and foreign keys
primary_key_type, foreign_key_type = primary_and_foreign_key_types

create_table :active_storage_blobs, id: primary_key_type do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.string :service_name, null: false
t.bigint :byte_size, null: false
t.string :checksum, null: false
t.datetime :created_at, null: false

t.index [ :key ], unique: true
end

create_table :active_storage_attachments, id: primary_key_type do |t|
t.string :name, null: false
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
t.references :blob, null: false, type: foreign_key_type

t.datetime :created_at, null: false

t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end

create_table :active_storage_variant_records, id: primary_key_type do |t|
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
t.string :variation_digest, null: false

t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end

private
def primary_and_foreign_key_types
config = Rails.configuration.generators
setting = config.options[config.orm][:primary_key_type]
primary_key_type = setting || :primary_key
foreign_key_type = setting || :bigint
[primary_key_type, foreign_key_type]
end
end
89 changes: 89 additions & 0 deletions db/migrate/20230526212613_convert_to_active_storage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
class ConvertToActiveStorage < ActiveRecord::Migration[5.2]
require 'open-uri'

def up

Check notice

Code scanning / Rubocop

Avoid methods longer than 10 lines of code.

Metrics/MethodLength: Method has too many lines. [53/36]
# postgres
get_blob_id = 'LASTVAL()'
# mariadb
# get_blob_id = 'LAST_INSERT_ID()'
# sqlite
# get_blob_id = 'LAST_INSERT_ROWID()'

active_storage_blob_statement = ActiveRecord::Base.connection.raw_connection.prepare('active_storage_blob_statement', <<-SQL)
INSERT INTO active_storage_blobs (
key, filename, content_type, metadata, byte_size, checksum, created_at
) VALUES ($1, $2, $3, '{}', $4, $5, $6)
SQL

active_storage_attachment_statement = ActiveRecord::Base.connection.raw_connection.prepare('active_storage_attachment_statement', <<-SQL)
INSERT INTO active_storage_attachments (
name, record_type, record_id, blob_id, created_at
) VALUES ($1, $2, $3, #{get_blob_id}, $4)
SQL

Rails.application.eager_load!
models = ActiveRecord::Base.descendants.reject { |model| model.abstract_class? || model == ActionMailbox::InboundEmail || model == ActionText::RichText}

transaction do
models.each do |model|
attachments = model.column_names.map do |c|
if c =~ /(.+)_file_name$/
$1
end
end.compact

if attachments.blank?
next
end

model.find_each.each do |instance|
attachments.each do |attachment|
if instance.send(attachment).path.blank?
next
end

ActiveRecord::Base.connection.execute_prepared(
'active_storage_blob_statement', [
key(instance, attachment),
instance.send("#{attachment}_file_name"),
instance.send("#{attachment}_content_type"),
instance.send("#{attachment}_file_size"),
checksum(instance.send(attachment)),
instance.updated_at.iso8601
])

ActiveRecord::Base.connection.execute_prepared(
'active_storage_attachment_statement', [
attachment,
model.name,
instance.id,
instance.updated_at.iso8601,
])
end
end
end
end
end

def down
raise ActiveRecord::IrreversibleMigration
end

private

def key(instance, attachment)
SecureRandom.uuid
# Alternatively:
# instance.send("#{attachment}_file_name")
end

def checksum(attachment)
# local files stored on disk:
url = attachment.path
Digest::MD5.base64digest(File.read(url))

# remote files stored on another person's computer:
# url = attachment.url
# Digest::MD5.base64digest(Net::HTTP.get(URI(url)))
end
end
Loading