diff --git a/alchemy_cms.gemspec b/alchemy_cms.gemspec index 89d7631f35..9ebb2ad2bb 100644 --- a/alchemy_cms.gemspec +++ b/alchemy_cms.gemspec @@ -39,6 +39,8 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency "cancancan", [">= 2.1", "< 4.0"] gem.add_runtime_dependency "coffee-rails", [">= 4.0", "< 6.0"] gem.add_runtime_dependency "csv", ["~> 3.3"] + gem.add_runtime_dependency "dragonfly", ["~> 1.4"] + gem.add_runtime_dependency "dragonfly_svg", ["~> 0.0.4"] gem.add_runtime_dependency "gutentag", ["~> 2.2", ">= 2.2.1"] gem.add_runtime_dependency "handlebars_assets", ["~> 0.23"] gem.add_runtime_dependency "image_processing", [">= 1.2"] diff --git a/app/models/alchemy/attachment.rb b/app/models/alchemy/attachment.rb index 5623cbf32c..cd0d08b496 100644 --- a/app/models/alchemy/attachment.rb +++ b/app/models/alchemy/attachment.rb @@ -24,6 +24,19 @@ class Attachment < BaseRecord include Alchemy::Taggable include Alchemy::TouchElements + # Legacy Dragonfly file attachments + extend Dragonfly::Model + dragonfly_accessor :legacy_file, app: :alchemy_attachments + DEPRECATED_COLUMNS = %i[ + legacy_file + legacy_file_name + legacy_file_size + legacy_file_uid + ].each do |column| + deprecate column, deprecator: Alchemy::Deprecation + deprecate :"#{column}=", deprecator: Alchemy::Deprecation + end + # Use ActiveStorage file attachments has_one_attached :file, service: :alchemy_cms diff --git a/app/models/alchemy/picture.rb b/app/models/alchemy/picture.rb index a059aad99d..350e44ebe7 100644 --- a/app/models/alchemy/picture.rb +++ b/app/models/alchemy/picture.rb @@ -80,6 +80,22 @@ def self.preprocessor_class=(klass) @_preprocessor_class = klass end + # Legacy Dragonfly image attachments + extend Dragonfly::Model + dragonfly_accessor :legacy_image_file, app: :alchemy_pictures + DEPRECATED_COLUMNS = %i[ + legacy_image_file + legacy_image_file_format + legacy_image_file_height + legacy_image_file_name + legacy_image_file_size + legacy_image_file_uid + legacy_image_file_width + ].each do |column| + deprecate column, deprecator: Alchemy::Deprecation + deprecate :"#{column}=", deprecator: Alchemy::Deprecation + end + # Use ActiveStorage image processing has_one_attached :image_file, service: :alchemy_cms diff --git a/db/migrate/20240611080918_rename_alchemy_attachment_file.rb b/db/migrate/20240611080918_rename_alchemy_attachment_file.rb new file mode 100644 index 0000000000..caf952940c --- /dev/null +++ b/db/migrate/20240611080918_rename_alchemy_attachment_file.rb @@ -0,0 +1,13 @@ +class RenameAlchemyAttachmentFile < ActiveRecord::Migration[7.0] + COLUMNS = %i[ + file_name + file_size + file_uid + ] + + def change + COLUMNS.each do |column| + rename_column :alchemy_attachments, column, :"legacy_#{column}" + end + end +end diff --git a/db/migrate/20240611080918_rename_alchemy_picture_image_file.rb b/db/migrate/20240611080918_rename_alchemy_picture_image_file.rb new file mode 100644 index 0000000000..00b21693ee --- /dev/null +++ b/db/migrate/20240611080918_rename_alchemy_picture_image_file.rb @@ -0,0 +1,16 @@ +class RenameAlchemyPictureImageFile < ActiveRecord::Migration[7.0] + COLUMNS = %i[ + image_file_format + image_file_height + image_file_name + image_file_size + image_file_uid + image_file_width + ] + + def change + COLUMNS.each do |column| + rename_column :alchemy_pictures, column, :"legacy_#{column}" + end + end +end diff --git a/lib/alchemy/test_support/factories/attachment_factory.rb b/lib/alchemy/test_support/factories/attachment_factory.rb index 94f5949195..d988e7b9fb 100644 --- a/lib/alchemy/test_support/factories/attachment_factory.rb +++ b/lib/alchemy/test_support/factories/attachment_factory.rb @@ -20,6 +20,5 @@ end name { "image" } - file_name { "image.png" } end end diff --git a/lib/alchemy/upgrader/eight_zero.rb b/lib/alchemy/upgrader/eight_zero.rb new file mode 100644 index 0000000000..fe1dbfc5cd --- /dev/null +++ b/lib/alchemy/upgrader/eight_zero.rb @@ -0,0 +1,126 @@ +require "alchemy/shell" +require "benchmark" +require "active_storage/service/disk_service" + +module Alchemy + class Upgrader::EightZero < Upgrader + extend Alchemy::Shell + DEFAULT_CONTENT_TYPE = "application/octet-stream" + DISK_SERVICE = ActiveStorage::Service::DiskService + SERVICE_NAME = :alchemy_cms + + # Prevents (down)loading the original file + METADATA = { + identified: true, # Skip identifying file type + analyzed: true, # Skip analyze job + composed: true # Skip checksum check + } + + class << self + def migrate_pictures_to_active_storage + pictures_without_as_attachment = Alchemy::Picture.where.missing(:image_file_attachment) + count = pictures_without_as_attachment.count + if count > 0 + log "Migrating #{count} Dragonfly image file(s) to ActiveStorage." + realtime = Benchmark.realtime do + pictures_without_as_attachment.find_each do |picture| + Alchemy::Deprecation.silence do + uid = picture.legacy_image_file_uid + key = key_for_uid(uid) + content_type = Mime::Type.lookup_by_extension(picture.legacy_image_file_format) || DEFAULT_CONTENT_TYPE + Alchemy::Picture.transaction do + blob = ActiveStorage::Blob.create!( + key: key, + filename: picture.legacy_image_file_name, + byte_size: picture.legacy_image_file_size, + content_type: content_type, + metadata: METADATA.merge( + width: picture.legacy_image_file_width, + height: picture.legacy_image_file_height + ), + service_name: SERVICE_NAME + ) + picture.create_image_file_attachment!( + name: :image_file, + record: picture, + blob: blob + ) + end + move_file(Rails.root.join("uploads/pictures", uid), key) + end + print "." + end + end + puts "\nDone in #{realtime.round(2)}s!" + else + log "No Dragonfly image files for migration found.", :skip + end + end + + def migrate_attachments_to_active_storage + attachments_without_as_attachment = Alchemy::Attachment.where.missing(:file_attachment) + count = attachments_without_as_attachment.count + if count > 0 + log "Migrating #{count} Dragonfly attachment file(s) to ActiveStorage." + realtime = Benchmark.realtime do + attachments_without_as_attachment.find_each do |attachment| + Alchemy::Deprecation.silence do + uid = attachment.legacy_file_uid + key = key_for_uid(uid) + Alchemy::Attachment.transaction do + blob = ActiveStorage::Blob.create!( + key: key, + filename: attachment.legacy_file_name, + byte_size: attachment.legacy_file_size, + content_type: attachment.file_mime_type.presence || DEFAULT_CONTENT_TYPE, + metadata: METADATA, + service_name: SERVICE_NAME + ) + attachment.create_file_attachment!( + record: attachment, + name: :file, + blob: blob + ) + end + move_file(Rails.root.join("uploads/attachments", uid), key) + end + print "." + end + end + puts "\nDone in #{realtime.round(2)}s!" + else + log "No Dragonfly attachment files for migration found.", :skip + end + end + + private + + # ActiveStorage::Service::DiskService stores files in a folder structure + # based on the first two characters of the file uid. + def key_for_uid(uid) + case service + when DISK_SERVICE + uid.split("/").last + else + uid + end + end + + # ActiveStorage::Service::DiskService stores files in a folder structure + # based on the first two characters of the file uid. + def move_file(uid, key) + case service + when DISK_SERVICE + if File.exist?(uid) + service.send(:make_path_for, key) + FileUtils.mv uid, service.send(:path_for, key) + end + end + end + + def service + ActiveStorage::Blob.services.fetch(SERVICE_NAME) + end + end + end +end diff --git a/lib/alchemy_cms.rb b/lib/alchemy_cms.rb index b588aa7e6d..298702ddcb 100644 --- a/lib/alchemy_cms.rb +++ b/lib/alchemy_cms.rb @@ -9,6 +9,8 @@ require "active_model_serializers" require "awesome_nested_set" require "cancan" +require "dragonfly" +require "dragonfly_svg" require "gutentag" require "handlebars_assets" require "importmap-rails" diff --git a/lib/generators/alchemy/install/install_generator.rb b/lib/generators/alchemy/install/install_generator.rb index adc2693181..594f53f42f 100644 --- a/lib/generators/alchemy/install/install_generator.rb +++ b/lib/generators/alchemy/install/install_generator.rb @@ -69,6 +69,15 @@ def install_assets append_to_file Rails.root.join("app/assets/config/manifest.js"), "//= link alchemy/admin/custom.css\n" end + def set_active_storage_service + insert_into_file app_config_path.join("storage.yml"), <<-YAML.strip_heredoc + + alchemy_cms: + service: Disk + root: <%= Rails.root.join("storage") %> + YAML + end + def copy_demo_views return if options[:skip_demo_files] diff --git a/lib/tasks/alchemy/upgrade.rake b/lib/tasks/alchemy/upgrade.rake index 90a86df5a7..4c0ee9d733 100644 --- a/lib/tasks/alchemy/upgrade.rake +++ b/lib/tasks/alchemy/upgrade.rake @@ -9,6 +9,7 @@ namespace :alchemy do "alchemy:upgrade:prepare", "alchemy:upgrade:7.0:run", "alchemy:upgrade:7.3:run" + "alchemy:upgrade:8.0:run" ] do Alchemy::Upgrader.display_todos end @@ -47,6 +48,14 @@ namespace :alchemy do Alchemy::Upgrader.display_todos end + desc "Upgrade Alchemy to v8.0" + task "8.0" => [ + "alchemy:upgrade:prepare", + "alchemy:upgrade:8.0:run" + ] do + Alchemy::Upgrader.display_todos + end + namespace "7.0" do task "run" => [ "alchemy:upgrade:7.0:remove_admin_entrypoint" @@ -77,5 +86,22 @@ namespace :alchemy do Alchemy::Upgrader::SevenPointThree.generate_custom_css_entrypoint end end + + namespace "8.0" do + task "run" => [ + "alchemy:upgrade:8.0:migrate_pictures_to_active_storage", + "alchemy:upgrade:8.0:migrate_attachments_to_active_storage" + ] + + desc "Migrate pictures to active_storage" + task migrate_pictures_to_active_storage: [:environment] do + Alchemy::Upgrader::EightZero.migrate_pictures_to_active_storage + end + + desc "Migrate attachments to active_storage" + task migrate_attachments_to_active_storage: [:environment] do + Alchemy::Upgrader::EightZero.migrate_attachments_to_active_storage + end + end end end diff --git a/spec/dummy/config/storage.yml b/spec/dummy/config/storage.yml index d32f76e8fb..f1a392497c 100644 --- a/spec/dummy/config/storage.yml +++ b/spec/dummy/config/storage.yml @@ -32,3 +32,7 @@ local: # service: Mirror # primary: local # mirrors: [ amazon, google, microsoft ] + +alchemy_cms: + service: Disk + root: <%= Rails.root.join("storage") %> diff --git a/spec/dummy/db/migrate/20240611152553_rename_alchemy_attachment_file.alchemy.rb b/spec/dummy/db/migrate/20240611152553_rename_alchemy_attachment_file.alchemy.rb new file mode 100644 index 0000000000..3c56d08687 --- /dev/null +++ b/spec/dummy/db/migrate/20240611152553_rename_alchemy_attachment_file.alchemy.rb @@ -0,0 +1,14 @@ +# This migration comes from alchemy (originally 20240611080918) +class RenameAlchemyAttachmentFile < ActiveRecord::Migration[7.0] + COLUMNS = %i[ + file_name + file_size + file_uid + ] + + def change + COLUMNS.each do |column| + rename_column :alchemy_attachments, column, :"legacy_#{column}" + end + end +end diff --git a/spec/dummy/db/migrate/20240611152554_rename_alchemy_picture_image_file.alchemy.rb b/spec/dummy/db/migrate/20240611152554_rename_alchemy_picture_image_file.alchemy.rb new file mode 100644 index 0000000000..3a623642ae --- /dev/null +++ b/spec/dummy/db/migrate/20240611152554_rename_alchemy_picture_image_file.alchemy.rb @@ -0,0 +1,17 @@ +# This migration comes from alchemy (originally 20240611080918) +class RenameAlchemyPictureImageFile < ActiveRecord::Migration[7.0] + COLUMNS = %i[ + image_file_format + image_file_height + image_file_name + image_file_size + image_file_uid + image_file_width + ] + + def change + COLUMNS.each do |column| + rename_column :alchemy_pictures, column, :"legacy_#{column}" + end + end +end diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb index 2728ed3600..dc0f2a2af8 100644 --- a/spec/dummy/db/schema.rb +++ b/spec/dummy/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_04_11_155901) do +ActiveRecord::Schema[7.2].define(version: 2024_06_11_152554) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -41,16 +41,16 @@ create_table "alchemy_attachments", force: :cascade do |t| t.string "name" - t.string "file_name" + t.string "legacy_file_name" t.string "file_mime_type" - t.integer "file_size" + t.integer "legacy_file_size" t.integer "creator_id" t.integer "updater_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.string "file_uid" + t.string "legacy_file_uid" t.index ["creator_id"], name: "index_alchemy_attachments_on_creator_id" - t.index ["file_uid"], name: "index_alchemy_attachments_on_file_uid" + t.index ["legacy_file_uid"], name: "index_alchemy_attachments_on_legacy_file_uid" t.index ["updater_id"], name: "index_alchemy_attachments_on_updater_id" end @@ -234,19 +234,19 @@ create_table "alchemy_pictures", force: :cascade do |t| t.string "name" - t.string "image_file_name" - t.integer "image_file_width" - t.integer "image_file_height" + t.string "legacy_image_file_name" + t.integer "legacy_image_file_width" + t.integer "legacy_image_file_height" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "creator_id" t.integer "updater_id" t.string "upload_hash" - t.string "image_file_uid" - t.integer "image_file_size" - t.string "image_file_format" + t.string "legacy_image_file_uid" + t.integer "legacy_image_file_size" + t.string "legacy_image_file_format" t.index ["creator_id"], name: "index_alchemy_pictures_on_creator_id" - t.index ["image_file_name"], name: "index_alchemy_pictures_on_image_file_name" + t.index ["legacy_image_file_name"], name: "index_alchemy_pictures_on_legacy_image_file_name" t.index ["name"], name: "index_alchemy_pictures_on_name" t.index ["updater_id"], name: "index_alchemy_pictures_on_updater_id" end diff --git a/spec/models/alchemy/picture_spec.rb b/spec/models/alchemy/picture_spec.rb index ff1fd98b31..c3d68dd6a6 100644 --- a/spec/models/alchemy/picture_spec.rb +++ b/spec/models/alchemy/picture_spec.rb @@ -88,7 +88,7 @@ module Alchemy describe ".alchemy_resource_filters" do context "with image file formats" do - let!(:picture) { create(:alchemy_picture, image_file_format: "png") } + let!(:picture) { create(:alchemy_picture, image_file: image_file) } it "returns a list of filters with image file formats" do expect(Alchemy::Picture.alchemy_resource_filters).to eq([ @@ -403,7 +403,7 @@ module Alchemy describe "#convertible?" do let(:picture) do - Picture.new(image_file_format: "image/png") + build(:alchemy_picture, image_file: image_file) end subject { picture.convertible? } diff --git a/spec/views/admin/pictures/show_spec.rb b/spec/views/admin/pictures/show_spec.rb index 9b5af7b2d0..1ff3dd103b 100644 --- a/spec/views/admin/pictures/show_spec.rb +++ b/spec/views/admin/pictures/show_spec.rb @@ -11,11 +11,7 @@ end let(:picture) do - create(:alchemy_picture, { - image_file: image, - name: "animated", - image_file_name: "animated.gif" - }) + create(:alchemy_picture, image_file: image) end let(:language) { create(:alchemy_language) } diff --git a/spec/views/alchemy/admin/ingredients/edit_spec.rb b/spec/views/alchemy/admin/ingredients/edit_spec.rb index 5bbf761c44..f7c1035eb1 100644 --- a/spec/views/alchemy/admin/ingredients/edit_spec.rb +++ b/spec/views/alchemy/admin/ingredients/edit_spec.rb @@ -23,11 +23,7 @@ end let(:picture) do - create(:alchemy_picture, { - image_file: image, - name: "img", - image_file_name: "img.png" - }) + create(:alchemy_picture, image_file: image) end let(:ingredient) { Alchemy::Ingredients::Picture.new(id: 1, picture: picture) } diff --git a/spec/views/alchemy/ingredients/audio_view_spec.rb b/spec/views/alchemy/ingredients/audio_view_spec.rb index 7cf1c51448..5d37c29269 100644 --- a/spec/views/alchemy/ingredients/audio_view_spec.rb +++ b/spec/views/alchemy/ingredients/audio_view_spec.rb @@ -8,7 +8,7 @@ end let(:attachment) do - build_stubbed(:alchemy_attachment, file: file, name: "a podcast", file_name: "image with spaces.png") + build_stubbed(:alchemy_attachment, file: file) end let(:ingredient) do diff --git a/spec/views/alchemy/ingredients/video_view_spec.rb b/spec/views/alchemy/ingredients/video_view_spec.rb index 7441ce8b5e..66dcb2e338 100644 --- a/spec/views/alchemy/ingredients/video_view_spec.rb +++ b/spec/views/alchemy/ingredients/video_view_spec.rb @@ -8,7 +8,7 @@ end let(:attachment) do - build_stubbed(:alchemy_attachment, file: file, name: "a movie", file_name: "image with spaces.png") + build_stubbed(:alchemy_attachment, file: file) end let(:ingredient) do