Skip to content

Commit

Permalink
Add PictureThumb.storage_class
Browse files Browse the repository at this point in the history
Instead of configuring a `PictureThumb.generator_class` that needs
to implement the creation of the `Alchemy::PictureThumb` records
and make sure do that in a multi-concurrency safe way, we keep
the implementation in core and abstract the storage class instead.
  • Loading branch information
tvdeyen committed Feb 27, 2023
1 parent 11b0505 commit 11faeef
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 29 deletions.
2 changes: 1 addition & 1 deletion app/models/alchemy/picture/url.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def uid
else
uid = PictureThumb::Uid.call(signature, variant)
ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
PictureThumb.generator_class.call(variant, signature, uid)
PictureThumb::Create.call(variant, signature, uid)
end
uid
end
Expand Down
20 changes: 10 additions & 10 deletions app/models/alchemy/picture_thumb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module Alchemy
# different thumbnail store (ie. a remote file storage).
#
# config/initializers/alchemy.rb
# Alchemy::PictureThumb.generator_class = My::ThumbnailGenerator
# Alchemy::PictureThumb.storage_class = My::ThumbnailStore
#
class PictureThumb < BaseRecord
belongs_to :picture, class_name: "Alchemy::Picture"
Expand All @@ -16,18 +16,18 @@ class PictureThumb < BaseRecord
validates :uid, presence: true

class << self
# Thumbnail generator class
# Thumbnail storage class
#
# @see Alchemy::PictureThumb::Create
def generator_class
@_generator_class ||= Alchemy::PictureThumb::Create
# @see Alchemy::PictureThumb::FileStore
def storage_class
@_storage_class ||= Alchemy::PictureThumb::FileStore
end

# Set a thumbnail generator class
# Set a thumbnail storage class
#
# @see Alchemy::PictureThumb::Create
def generator_class=(klass)
@_generator_class = klass
# @see Alchemy::PictureThumb::FileStore
def storage_class=(klass)
@_storage_class = klass
end

# Upfront generation of picture thumbnails
Expand All @@ -49,7 +49,7 @@ def generate_thumbs!(picture)
next if thumb

uid = Alchemy::PictureThumb::Uid.call(signature, variant)
generator_class.call(variant, signature, uid)
Alchemy::PictureThumb::Create.call(variant, signature, uid)
end
end
end
Expand Down
25 changes: 7 additions & 18 deletions app/models/alchemy/picture_thumb/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

module Alchemy
class PictureThumb < BaseRecord
# Stores the render result of a Alchemy::PictureVariant
# in the configured Dragonfly datastore
# (Default: Dragonfly::FileDataStore)
# Creates a Alchemy::PictureThumb
#
# Stores the processes result of a Alchemy::PictureVariant
# in the configured +Alchemy::PictureThumb.storage_class+
# (Default: {Alchemy::PictureThumb::FileStore})
#
class Create
class << self
Expand All @@ -24,26 +26,13 @@ def call(variant, signature, uid)
thumb.uid = uid
end
begin
# process the image
image = variant.image
# store the processed image
image.to_file(server_path(uid)).close
rescue RuntimeError => e
Alchemy::PictureThumb.storage_class.call(variant, uid)
rescue StandardError => e
ErrorTracking.notification_handler.call(e)
# destroy the thumb if processing or storing fails
@thumb&.destroy
end
end

private

# Alchemys dragonfly datastore config seperates the storage path from the public server
# path for security reasons. The Dragonfly FileDataStorage does not support that,
# so we need to build the path on our own.
def server_path(uid)
dragonfly_app = ::Dragonfly.app(:alchemy_pictures)
"#{dragonfly_app.datastore.server_root}/#{uid}"
end
end
end
end
Expand Down
33 changes: 33 additions & 0 deletions app/models/alchemy/picture_thumb/file_store.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

module Alchemy
class PictureThumb < BaseRecord
# Stores the render result of a Alchemy::PictureVariant
# in the configured Dragonfly datastore
# (Default: Dragonfly::FileDataStore)
#
class FileStore
class << self
# @param [Alchemy::PictureVariant] variant the to be rendered image
# @param [String] uid The Unique Image Identifier the image is stored at
#
def call(variant, uid)
# process the image
image = variant.image
# store the processed image
image.to_file(server_path(uid)).close
end

private

# Alchemys dragonfly datastore config seperates the storage path from the public server
# path for security reasons. The Dragonfly FileDataStorage does not support that,
# so we need to build the path on our own.
def server_path(uid)
dragonfly_app = ::Dragonfly.app(:alchemy_pictures)
"#{dragonfly_app.datastore.server_root}/#{uid}"
end
end
end
end
end
27 changes: 27 additions & 0 deletions spec/models/alchemy/picture_thumb/file_store_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Alchemy::PictureThumb::FileStore do
let(:image) { File.new(File.expand_path("../../../fixtures/image.png", __dir__)) }
let(:picture) { FactoryBot.create(:alchemy_picture, image_file: image) }
let!(:variant) { Alchemy::PictureVariant.new(picture, { size: "1x1" }) }
let(:uid_path) { "pictures/#{picture.id}/1234" }

let(:root_path) do
datastore = Dragonfly.app(:alchemy_pictures).datastore
datastore.server_root
end

subject(:store) do
Alchemy::PictureThumb::FileStore.call(variant, "/#{uid_path}/image.png")
end

before do
FileUtils.rm_rf("#{root_path}/#{uid_path}")
end

it "stores thumb on the disk" do
expect { store }.to change { Dir.glob("#{root_path}/#{uid_path}").length }.by(1)
end
end

0 comments on commit 11faeef

Please sign in to comment.