mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
Merge pull request #9119 from mkllnk/active-storage-part1
Store files with Active Storage in addition to Paperclip
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,6 +15,7 @@ db/*.csv
|
||||
log/*.log
|
||||
log/*.log.lck
|
||||
log/*.log.*
|
||||
/storage
|
||||
tmp/
|
||||
.idea/*
|
||||
\#*
|
||||
|
||||
5
Gemfile
5
Gemfile
@@ -8,6 +8,11 @@ gem 'dotenv-rails', require: 'dotenv/rails-now' # Load ENV vars before other gem
|
||||
|
||||
gem 'rails', '>= 6.1.4'
|
||||
|
||||
# Active Storage
|
||||
gem "active_storage_validations"
|
||||
gem "aws-sdk-s3", require: false
|
||||
gem "image_processing"
|
||||
|
||||
gem 'activemerchant', '>= 1.78.0'
|
||||
gem 'rexml'
|
||||
gem 'angular-rails-templates', '>= 0.3.0'
|
||||
|
||||
31
Gemfile.lock
31
Gemfile.lock
@@ -101,6 +101,11 @@ GEM
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_model_serializers (0.8.4)
|
||||
activemodel (>= 3.0)
|
||||
active_storage_validations (0.9.7)
|
||||
activejob (>= 5.2.0)
|
||||
activemodel (>= 5.2.0)
|
||||
activestorage (>= 5.2.0)
|
||||
activesupport (>= 5.2.0)
|
||||
activejob (6.1.4.4)
|
||||
activesupport (= 6.1.4.4)
|
||||
globalid (>= 0.3.6)
|
||||
@@ -158,11 +163,27 @@ GEM
|
||||
awesome_nested_set (3.4.0)
|
||||
activerecord (>= 4.0.0, < 7.0)
|
||||
awesome_print (1.9.2)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.570.0)
|
||||
aws-sdk (1.67.0)
|
||||
aws-sdk-v1 (= 1.67.0)
|
||||
aws-sdk-core (3.130.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.55.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.113.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sdk-v1 (1.67.0)
|
||||
json (~> 1.4)
|
||||
nokogiri (~> 1)
|
||||
aws-sigv4 (1.4.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
axlsx_styler (1.1.0)
|
||||
activesupport (>= 3.1)
|
||||
caxlsx (>= 2.0.2)
|
||||
@@ -331,9 +352,13 @@ GEM
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-js (3.9.0)
|
||||
i18n (>= 0.6.6)
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
immigrant (0.3.6)
|
||||
activerecord (>= 3.0)
|
||||
ipaddress (0.8.3)
|
||||
jmespath (1.6.1)
|
||||
jquery-rails (4.4.0)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
@@ -373,6 +398,7 @@ GEM
|
||||
mimemagic (0.4.3)
|
||||
nokogiri (~> 1)
|
||||
rake
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.8.0)
|
||||
mini_racer (0.4.0)
|
||||
@@ -560,6 +586,8 @@ GEM
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
ruby-progressbar (1.11.0)
|
||||
ruby-rc4 (0.1.5)
|
||||
ruby-vips (2.1.4)
|
||||
ffi (~> 1.12)
|
||||
ruby2_keywords (0.0.4)
|
||||
rubyzip (2.3.2)
|
||||
rufus-scheduler (3.7.0)
|
||||
@@ -683,6 +711,7 @@ PLATFORMS
|
||||
DEPENDENCIES
|
||||
actionpack-action_caching
|
||||
active_model_serializers (= 0.8.4)
|
||||
active_storage_validations
|
||||
activemerchant (>= 1.78.0)
|
||||
activerecord-import
|
||||
activerecord-postgresql-adapter
|
||||
@@ -697,6 +726,7 @@ DEPENDENCIES
|
||||
awesome_nested_set
|
||||
awesome_print
|
||||
aws-sdk (= 1.67.0)
|
||||
aws-sdk-s3
|
||||
bigdecimal (= 3.0.2)
|
||||
bootsnap
|
||||
bugsnag
|
||||
@@ -736,6 +766,7 @@ DEPENDENCIES
|
||||
hiredis
|
||||
i18n
|
||||
i18n-js (~> 3.9.0)
|
||||
image_processing
|
||||
immigrant
|
||||
jquery-rails (= 4.4.0)
|
||||
jquery-ui-rails (~> 4.2)
|
||||
|
||||
67
app/models/concerns/has_migrating_file.rb
Normal file
67
app/models/concerns/has_migrating_file.rb
Normal file
@@ -0,0 +1,67 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HasMigratingFile
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
@migrating_models = []
|
||||
|
||||
def self.migrating_models
|
||||
@migrating_models
|
||||
end
|
||||
|
||||
included do
|
||||
HasMigratingFile.migrating_models.push(name)
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def has_one_migrating(name, paperclip_options = {})
|
||||
# Active Storage declaration
|
||||
has_one_attached name
|
||||
|
||||
# Backup Active Storage methods before they get overridden by Paperclip.
|
||||
alias_method "active_storage_#{name}", name
|
||||
alias_method "active_storage_#{name}=", "#{name}="
|
||||
|
||||
# Paperclip declaration
|
||||
#
|
||||
# This will define the `name` and `name=` methods as well.
|
||||
has_attached_file name, paperclip_options
|
||||
|
||||
# Paperclip callback to duplicate file with Active Storage
|
||||
#
|
||||
# We store files with Paperclip *and* Active Storage while we migrate
|
||||
# old Paperclip files to Active Storage. This enables availability
|
||||
# during the migration.
|
||||
public_send("after_#{name}_post_process") do
|
||||
path = processed_local_file_path(name)
|
||||
if public_send(name).errors.blank? && path.present?
|
||||
attach_file(name, File.open(path))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def attach_file(name, io)
|
||||
attachable = {
|
||||
io: io,
|
||||
filename: public_send("#{name}_file_name"),
|
||||
content_type: public_send("#{name}_content_type"),
|
||||
identify: false,
|
||||
}
|
||||
public_send("active_storage_#{name}=", attachable)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def processed_local_file_path(name)
|
||||
attachment = public_send(name)
|
||||
|
||||
temporary = attachment.queued_for_write[:original]
|
||||
|
||||
if temporary&.path.present?
|
||||
temporary.path
|
||||
else
|
||||
attachment.path
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,7 @@
|
||||
require 'spree/core/s3_support'
|
||||
|
||||
class Enterprise < ApplicationRecord
|
||||
include HasMigratingFile
|
||||
include Spree::Core::S3Support
|
||||
|
||||
SELLS = %w(unspecified none own any).freeze
|
||||
@@ -72,12 +73,12 @@ class Enterprise < ApplicationRecord
|
||||
tag_rule[:preferred_customer_tags].blank?
|
||||
}
|
||||
|
||||
has_attached_file :logo,
|
||||
has_one_migrating :logo,
|
||||
styles: { medium: "300x300>", small: "180x180>", thumb: "100x100>" },
|
||||
url: '/images/enterprises/logos/:id/:style/:basename.:extension',
|
||||
path: 'public/images/enterprises/logos/:id/:style/:basename.:extension'
|
||||
|
||||
has_attached_file :promo_image,
|
||||
has_one_migrating :promo_image,
|
||||
styles: {
|
||||
large: ["1200x260#", :jpg],
|
||||
medium: ["720x156#", :jpg],
|
||||
@@ -88,7 +89,7 @@ class Enterprise < ApplicationRecord
|
||||
validates_attachment_content_type :logo, content_type: %r{\Aimage/.*\Z}
|
||||
validates_attachment_content_type :promo_image, content_type: %r{\Aimage/.*\Z}
|
||||
|
||||
has_attached_file :terms_and_conditions,
|
||||
has_one_migrating :terms_and_conditions,
|
||||
url: '/files/enterprises/terms_and_conditions/:id/:basename.:extension',
|
||||
path: 'public/files/enterprises/terms_and_conditions/:id/:basename.:extension'
|
||||
validates_attachment_content_type :terms_and_conditions,
|
||||
|
||||
@@ -4,6 +4,7 @@ require 'open_food_network/locking'
|
||||
require 'spree/core/s3_support'
|
||||
|
||||
class EnterpriseGroup < ApplicationRecord
|
||||
include HasMigratingFile
|
||||
include PermalinkGenerator
|
||||
include Spree::Core::S3Support
|
||||
|
||||
@@ -27,12 +28,12 @@ class EnterpriseGroup < ApplicationRecord
|
||||
|
||||
delegate :phone, :address1, :address2, :city, :zipcode, :state, :country, to: :address
|
||||
|
||||
has_attached_file :logo,
|
||||
has_one_migrating :logo,
|
||||
styles: { medium: "100x100" },
|
||||
url: '/images/enterprise_groups/logos/:id/:style/:basename.:extension',
|
||||
path: 'public/images/enterprise_groups/logos/:id/:style/:basename.:extension'
|
||||
|
||||
has_attached_file :promo_image,
|
||||
has_one_migrating :promo_image,
|
||||
styles: { large: ["1200x260#", :jpg] },
|
||||
url: '/images/enterprise_groups/promo_images/:id/:style/:basename.:extension',
|
||||
path: 'public/images/enterprise_groups/promo_images/:id/:style/:basename.:extension'
|
||||
|
||||
@@ -4,6 +4,8 @@ require 'spree/core/s3_support'
|
||||
|
||||
module Spree
|
||||
class Image < Asset
|
||||
include HasMigratingFile
|
||||
|
||||
validates_attachment_presence :attachment
|
||||
validate :no_attachment_errors
|
||||
|
||||
@@ -13,7 +15,7 @@ module Spree
|
||||
# - product: used in the BackOffice: Product Image upload modal in the Bulk Product Edit page
|
||||
# and Product image edit page
|
||||
# - large: used in the FrontOffice: product modal
|
||||
has_attached_file :attachment,
|
||||
has_one_migrating :attachment,
|
||||
styles: { mini: "48x48#", small: "227x227#",
|
||||
product: "240x240>", large: "600x600>" },
|
||||
default_style: :product,
|
||||
|
||||
@@ -5,6 +5,10 @@ module Spree
|
||||
class FileConfiguration < Configuration
|
||||
def self.preference(name, type, *args)
|
||||
if type == :file
|
||||
# Active Storage blob id:
|
||||
super "#{name}_blob_id", :integer, *args
|
||||
|
||||
# Paperclip attachment attributes:
|
||||
super "#{name}_file_name", :string, *args
|
||||
super "#{name}_content_type", :string, *args
|
||||
super "#{name}_file_size", :integer, *args
|
||||
@@ -17,7 +21,12 @@ module Spree
|
||||
|
||||
def get_preference(key)
|
||||
if !has_preference?(key) && has_attachment?(key)
|
||||
# Call Paperclip's attachment method:
|
||||
public_send key
|
||||
elsif key.ends_with?("_blob")
|
||||
# Find referenced Active Storage blob:
|
||||
blob_id = super("#{key}_id")
|
||||
ActiveStorage::Blob.find_by(id: blob_id)
|
||||
else
|
||||
super key
|
||||
end
|
||||
@@ -37,7 +46,11 @@ module Spree
|
||||
# errors if respond_to? isn't correct, so we override it here.
|
||||
def respond_to?(method, include_all = false)
|
||||
name = method.to_s.delete('=')
|
||||
super(self.class.preference_getter_method(name), include_all) || super(method, include_all)
|
||||
reference_name = "#{name}_id"
|
||||
|
||||
super(self.class.preference_getter_method(name), include_all) ||
|
||||
super(reference_name, include_all) ||
|
||||
super(method, include_all)
|
||||
end
|
||||
|
||||
def has_attachment?(name)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TermsOfServiceFile < ApplicationRecord
|
||||
has_attached_file :attachment
|
||||
include HasMigratingFile
|
||||
|
||||
has_one_migrating :attachment
|
||||
|
||||
validates :attachment, presence: true
|
||||
|
||||
@@ -19,4 +21,9 @@ class TermsOfServiceFile < ApplicationRecord
|
||||
def self.updated_at
|
||||
current&.updated_at || Time.zone.now
|
||||
end
|
||||
|
||||
def touch(_)
|
||||
# Ignore Active Storage changing the timestamp during migrations.
|
||||
# This can be removed once we got rid of Paperclip.
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@ require_relative 'boot'
|
||||
require "rails"
|
||||
[
|
||||
"active_record/railtie",
|
||||
#"active_storage/engine",
|
||||
"active_storage/engine",
|
||||
"action_controller/railtie",
|
||||
"action_view/railtie",
|
||||
"action_mailer/railtie",
|
||||
@@ -238,5 +238,7 @@ module Openfoodnetwork
|
||||
Rails.application.routes.default_url_options[:host] = ENV["SITE_URL"]
|
||||
|
||||
Rails.autoloaders.main.ignore(Rails.root.join('app/webpacker'))
|
||||
|
||||
config.active_storage.service = ENV["S3_BUCKET"].present? ? :amazon : :local
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,4 +51,6 @@ Openfoodnetwork::Application.configure do
|
||||
config.active_support.deprecation = :stderr
|
||||
|
||||
config.active_job.queue_adapter = :test
|
||||
|
||||
config.active_storage.service = :test
|
||||
end
|
||||
|
||||
@@ -82,6 +82,30 @@ en:
|
||||
using_producer_stock_settings_but_count_on_hand_set: "must be blank because using producer stock settings"
|
||||
on_demand_but_count_on_hand_set: "must be blank if on demand"
|
||||
limited_stock_but_no_count_on_hand: "must be specified because forcing limited stock"
|
||||
|
||||
# Used by active_storage_validations
|
||||
errors:
|
||||
messages:
|
||||
content_type_invalid: "has an invalid content type"
|
||||
file_size_out_of_range: "size %{file_size} is not between required range"
|
||||
limit_out_of_range: "total number is out of range"
|
||||
image_metadata_missing: "is not a valid image"
|
||||
dimension_min_inclusion: "must be greater than or equal to %{width} x %{height} pixel."
|
||||
dimension_max_inclusion: "must be less than or equal to %{width} x %{height} pixel."
|
||||
dimension_width_inclusion: "width is not included between %{min} and %{max} pixel."
|
||||
dimension_height_inclusion: "height is not included between %{min} and %{max} pixel."
|
||||
dimension_width_greater_than_or_equal_to: "width must be greater than or equal to %{length} pixel."
|
||||
dimension_height_greater_than_or_equal_to: "height must be greater than or equal to %{length} pixel."
|
||||
dimension_width_less_than_or_equal_to: "width must be less than or equal to %{length} pixel."
|
||||
dimension_height_less_than_or_equal_to: "height must be less than or equal to %{length} pixel."
|
||||
dimension_width_equal_to: "width must be equal to %{length} pixel."
|
||||
dimension_height_equal_to: "height must be equal to %{length} pixel."
|
||||
aspect_ratio_not_square: "must be a square image"
|
||||
aspect_ratio_not_portrait: "must be a portrait image"
|
||||
aspect_ratio_not_landscape: "must be a landscape image"
|
||||
aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}"
|
||||
aspect_ratio_unknown: "has an unknown aspect ratio"
|
||||
|
||||
stripe:
|
||||
error_code:
|
||||
incorrect_number: "The card number is incorrect."
|
||||
|
||||
21
config/storage.yml
Normal file
21
config/storage.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
local:
|
||||
service: Disk
|
||||
root: <%= Rails.root.join("storage") %>
|
||||
|
||||
test:
|
||||
service: Disk
|
||||
root: <%= Rails.root.join("tmp/storage") %>
|
||||
|
||||
test_amazon:
|
||||
service: S3
|
||||
access_key_id: "A...A"
|
||||
secret_access_key: "H...H"
|
||||
bucket: "ofn"
|
||||
region: "us-east-1"
|
||||
|
||||
amazon:
|
||||
service: S3
|
||||
access_key_id: <%= ENV["S3_ACCESS_KEY"] %>
|
||||
secret_access_key: <%= ENV["S3_SECRET"] %>
|
||||
bucket: <%= ENV["S3_BUCKET"] %>
|
||||
region: <%= ENV.fetch("S3_REGION", "us-east-1") %>
|
||||
@@ -0,0 +1,36 @@
|
||||
# This migration comes from active_storage (originally 20170806125915)
|
||||
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :active_storage_blobs 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 do |t|
|
||||
t.string :name, null: false
|
||||
t.references :record, null: false, polymorphic: true, index: false
|
||||
t.references :blob, null: false
|
||||
|
||||
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 do |t|
|
||||
t.belongs_to :blob, null: false, index: false
|
||||
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
|
||||
end
|
||||
30
db/schema.rb
30
db/schema.rb
@@ -15,6 +15,34 @@ ActiveRecord::Schema.define(version: 2022_04_10_162955) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
t.bigint "record_id", null: false
|
||||
t.bigint "blob_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
|
||||
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "active_storage_blobs", force: :cascade 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"], name: "index_active_storage_blobs_on_key", unique: true
|
||||
end
|
||||
|
||||
create_table "active_storage_variant_records", force: :cascade do |t|
|
||||
t.bigint "blob_id", null: false
|
||||
t.string "variation_digest", null: false
|
||||
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "adjustment_metadata", force: :cascade do |t|
|
||||
t.integer "adjustment_id"
|
||||
t.integer "enterprise_id"
|
||||
@@ -1183,6 +1211,8 @@ ActiveRecord::Schema.define(version: 2022_04_10_162955) do
|
||||
t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id"
|
||||
end
|
||||
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "adjustment_metadata", "enterprises", name: "adjustment_metadata_enterprise_id_fk"
|
||||
add_foreign_key "adjustment_metadata", "spree_adjustments", column: "adjustment_id", name: "adjustment_metadata_adjustment_id_fk", on_delete: :cascade
|
||||
add_foreign_key "coordinator_fees", "enterprise_fees", name: "coordinator_fees_enterprise_fee_id_fk"
|
||||
|
||||
130
lib/tasks/from_paperclip_to_active_storage.rake
Normal file
130
lib/tasks/from_paperclip_to_active_storage.rake
Normal file
@@ -0,0 +1,130 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
namespace :from_paperclip_to_active_storage do
|
||||
# This migration can't be a pure database migration because we need to know
|
||||
# the location of current files which is computed by Paperclip depending on
|
||||
# the `url` option.
|
||||
desc "Copy data to Active Storage tables referencing Paperclip files"
|
||||
task migrate: :environment do
|
||||
Rails.application.eager_load!
|
||||
|
||||
HasMigratingFile.migrating_models.each do |model_name|
|
||||
puts "Migrating #{model_name}"
|
||||
migrate_model(model_name.constantize)
|
||||
end
|
||||
end
|
||||
|
||||
# We have a special class called ContentConfiguration which is not a model
|
||||
# and therfore can't use the normal Active Storage magic.
|
||||
#
|
||||
# It uses `Spree::Preference`s to store all the Paperclip attributes. These
|
||||
# files are stored locally and we can replace them with preferences pointing
|
||||
# to an Active Storage blob.
|
||||
desc "Associate ContentConfig to ActiveStorage blobs"
|
||||
task copy_content_config: :environment do
|
||||
[
|
||||
:logo,
|
||||
:logo_mobile,
|
||||
:logo_mobile_svg,
|
||||
:home_hero,
|
||||
:footer_logo,
|
||||
].each do |name|
|
||||
migrate_content_config_file(name)
|
||||
end
|
||||
end
|
||||
|
||||
def migrate_model(model)
|
||||
duplicated_attachment_names(model).each do |name|
|
||||
migrate_attachment(model, name)
|
||||
end
|
||||
end
|
||||
|
||||
def migrate_attachment(model, name)
|
||||
records_to_migrate = missing_active_storage_attachment(model, name)
|
||||
|
||||
print " - #{name} (#{records_to_migrate.count}) "
|
||||
|
||||
records_to_migrate.find_each do |record|
|
||||
attach_paperclip(name, record)
|
||||
end
|
||||
|
||||
puts ""
|
||||
end
|
||||
|
||||
def attach_paperclip(name, record)
|
||||
paperclip = record.public_send(name)
|
||||
|
||||
if paperclip.respond_to?(:s3_object)
|
||||
attachment = storage_record_for(name, paperclip)
|
||||
record.public_send("#{name}_attachment=", attachment)
|
||||
print "."
|
||||
elsif File.exist?(paperclip.path)
|
||||
record.attach_file(name, File.open(paperclip.path))
|
||||
record.save!
|
||||
print "."
|
||||
else
|
||||
print "x"
|
||||
end
|
||||
rescue StandardError => e
|
||||
puts "x"
|
||||
puts e.message
|
||||
end
|
||||
|
||||
# Creates an Active Storage record pointing to the same file Paperclip
|
||||
# stored on AWS S3. Getting the checksum requires a HEAD request.
|
||||
# In my tests, I could process 100 records per minute this way.
|
||||
def storage_record_for(name, paperclip)
|
||||
blob = ActiveStorage::Blob.new(
|
||||
key: paperclip.path(:original),
|
||||
filename: paperclip.original_filename,
|
||||
content_type: paperclip.content_type,
|
||||
metadata: {},
|
||||
byte_size: paperclip.size,
|
||||
checksum: paperclip.s3_object.etag,
|
||||
created_at: paperclip.updated_at,
|
||||
)
|
||||
ActiveStorage::Attachment.new(
|
||||
name: name,
|
||||
blob: blob,
|
||||
created_at: paperclip.updated_at,
|
||||
)
|
||||
end
|
||||
|
||||
def migrate_content_config_file(name)
|
||||
paperclip = ContentConfig.public_send(name)
|
||||
|
||||
return if ContentConfig.public_send("#{name}_blob_id")
|
||||
return if paperclip.path.blank? || !paperclip.exists?
|
||||
|
||||
blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: File.open(paperclip.path),
|
||||
filename: paperclip.original_filename,
|
||||
content_type: paperclip.content_type,
|
||||
identify: false,
|
||||
)
|
||||
|
||||
ContentConfig.public_send("#{name}_blob_id=", blob.id)
|
||||
puts "Copied #{name}"
|
||||
end
|
||||
|
||||
def duplicated_attachment_names(model)
|
||||
paperclip_attachments = model.attachment_definitions.keys.map(&:to_s)
|
||||
active_storage_attachments = model.attachment_reflections.keys
|
||||
|
||||
only_paperclip = paperclip_attachments - active_storage_attachments
|
||||
only_active_storage = active_storage_attachments - paperclip_attachments
|
||||
both = paperclip_attachments & active_storage_attachments
|
||||
|
||||
puts "WARNING: not migrating #{only_paperclip}" if only_paperclip.present?
|
||||
puts "WARNING: no source for #{only_active_storage}" if only_active_storage.present?
|
||||
|
||||
both
|
||||
end
|
||||
|
||||
# Records with Paperclip but without an Active storage attachment yet
|
||||
def missing_active_storage_attachment(model, attachment)
|
||||
model.where.not("#{attachment}_file_name" => [nil, ""]).
|
||||
left_outer_joins("#{attachment}_attachment".to_sym).
|
||||
where(active_storage_attachments: { id: nil })
|
||||
end
|
||||
end
|
||||
110
spec/lib/tasks/from_paperclip_to_active_storage_rake_spec.rb
Normal file
110
spec/lib/tasks/from_paperclip_to_active_storage_rake_spec.rb
Normal file
@@ -0,0 +1,110 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
require "rake"
|
||||
|
||||
describe "from_paperclip_to_active_storage.rake" do
|
||||
include FileHelper
|
||||
|
||||
let(:file) { Rack::Test::UploadedFile.new(black_logo_file, 'image/png') }
|
||||
let(:s3_config) {
|
||||
{
|
||||
url: ":s3_alias_url",
|
||||
storage: :s3,
|
||||
s3_credentials: {
|
||||
access_key_id: "A...A",
|
||||
secret_access_key: "H...H",
|
||||
},
|
||||
s3_headers: { "Cache-Control" => "max-age=31557600" },
|
||||
bucket: "ofn",
|
||||
s3_protocol: "https",
|
||||
s3_host_alias: "ofn.s3.us-east-1.amazonaws.com",
|
||||
|
||||
# This is for easier testing:
|
||||
path: "/:id/:style/:basename.:extension",
|
||||
}
|
||||
}
|
||||
|
||||
before(:all) do
|
||||
Rake.application.rake_require "tasks/from_paperclip_to_active_storage"
|
||||
Rake::Task.define_task(:environment)
|
||||
end
|
||||
|
||||
describe ":migrate" do
|
||||
it "creates Active Storage records for existing images on disk" do
|
||||
image = Spree::Image.create!(attachment: file)
|
||||
image.attachment_attachment.delete
|
||||
image.attachment_blob.delete
|
||||
|
||||
expect {
|
||||
run_task "from_paperclip_to_active_storage:migrate"
|
||||
}.to change {
|
||||
image.reload.active_storage_attachment.attached?
|
||||
}.to(true)
|
||||
end
|
||||
|
||||
it "creates Active Storage records for images on AWS S3" do
|
||||
attachment_definition = Spree::Image.attachment_definitions[:attachment]
|
||||
allow(Spree::Image).to receive(:attachment_definitions).and_return(
|
||||
attachment: attachment_definition.merge(s3_config)
|
||||
)
|
||||
allow(Rails.application.config.active_storage).
|
||||
to receive(:service).and_return(:test_amazon)
|
||||
|
||||
stub_request(:put, /amazonaws/).to_return(status: 200, body: "", headers: {})
|
||||
stub_request(:head, /amazonaws/).to_return(
|
||||
status: 200, body: "",
|
||||
headers: {
|
||||
"ETag" => "md5sum000test000example"
|
||||
}
|
||||
)
|
||||
stub_request(:put, /amazonaws/).to_return(status: 200, body: "", headers: {})
|
||||
|
||||
image = Spree::Image.create!(attachment: file)
|
||||
image.attachment_attachment.delete
|
||||
image.attachment_blob.delete
|
||||
|
||||
expect {
|
||||
run_task "from_paperclip_to_active_storage:migrate"
|
||||
}.to change {
|
||||
image.reload.active_storage_attachment.attached?
|
||||
}.to(true)
|
||||
|
||||
expect(image.attachment_blob.checksum).to eq "md5sum000test000example"
|
||||
end
|
||||
end
|
||||
|
||||
describe ":copy_content_config" do
|
||||
it "doesn't copy default images" do
|
||||
run_task "from_paperclip_to_active_storage:copy_content_config"
|
||||
|
||||
expect(ContentConfig.logo_blob).to eq nil
|
||||
end
|
||||
|
||||
it "copies uploaded images" do
|
||||
ContentConfig.logo = file
|
||||
ContentConfig.logo.save
|
||||
|
||||
run_task "from_paperclip_to_active_storage:copy_content_config"
|
||||
|
||||
expect(ContentConfig.logo_blob).to be_a ActiveStorage::Blob
|
||||
end
|
||||
|
||||
it "doesn't copy twice" do
|
||||
ContentConfig.logo = file
|
||||
ContentConfig.logo.save
|
||||
|
||||
expect {
|
||||
run_task "from_paperclip_to_active_storage:copy_content_config"
|
||||
run_task "from_paperclip_to_active_storage:copy_content_config"
|
||||
}.to change {
|
||||
ActiveStorage::Blob.count
|
||||
}.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
def run_task(name)
|
||||
Rake::Task[name].reenable
|
||||
Rake.application.invoke_task(name)
|
||||
end
|
||||
end
|
||||
@@ -21,6 +21,18 @@ module Spree
|
||||
expect(attachment.file?).to eq true
|
||||
expect(attachment.url).to match %r"^/spree/products/[0-9]+/product/logo-black\.png\?[0-9]+$"
|
||||
end
|
||||
|
||||
it "duplicates the image with Active Storage" do
|
||||
image = Spree::Image.create!(
|
||||
attachment: file,
|
||||
viewable: product.master,
|
||||
)
|
||||
|
||||
attachment = image.active_storage_attachment
|
||||
url = Rails.application.routes.url_helpers.url_for(attachment)
|
||||
|
||||
expect(url).to match %r|^http://test\.host/rails/active_storage/blobs/redirect/[[:alnum:]-]+/logo-black\.png$|
|
||||
end
|
||||
end
|
||||
|
||||
describe "using AWS S3" do
|
||||
@@ -47,9 +59,12 @@ module Spree
|
||||
allow(Spree::Image).to receive(:attachment_definitions).and_return(
|
||||
attachment: attachment_definition.merge(s3_config)
|
||||
)
|
||||
allow(Rails.application.config.active_storage).
|
||||
to receive(:service).and_return(:test_amazon)
|
||||
end
|
||||
|
||||
it "saves a new image when none is present" do
|
||||
# Paperclip requests
|
||||
upload_pattern = %r"^https://ofn.s3.amazonaws.com/[0-9]+/(original|mini|small|product|large)/logo-black.png$"
|
||||
download_pattern = %r"^https://ofn.s3.amazonaws.com/[0-9]+/product/logo-black.png$"
|
||||
public_url_pattern = %r"^https://ofn.s3.us-east-1.amazonaws.com/[0-9]+/product/logo-black.png\?[0-9]+$"
|
||||
@@ -57,15 +72,27 @@ module Spree
|
||||
stub_request(:put, upload_pattern).to_return(status: 200, body: "", headers: {})
|
||||
stub_request(:head, download_pattern).to_return(status: 200, body: "", headers: {})
|
||||
|
||||
# Active Storage requests
|
||||
as_upload_pattern = %r"^https://ofn.s3.amazonaws.com/[[:alnum:]]+$"
|
||||
|
||||
stub_request(:put, as_upload_pattern).to_return(status: 200, body: "", headers: {})
|
||||
|
||||
image = Spree::Image.create!(
|
||||
attachment: file,
|
||||
viewable: product.master,
|
||||
)
|
||||
|
||||
# Paperclip
|
||||
attachment = image.attachment
|
||||
expect(attachment.exists?).to eq true
|
||||
expect(attachment.file?).to eq true
|
||||
expect(attachment.url).to match public_url_pattern
|
||||
|
||||
# Active Storage
|
||||
attachment = image.active_storage_attachment
|
||||
expect(attachment).to be_attached
|
||||
expect(Rails.application.routes.url_helpers.url_for(attachment)).
|
||||
to match %r"^http://test\.host/rails/active_storage/blobs/redirect/[[:alnum:]-]+/logo-black\.png"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user