Shrine::Plugins::Rom
Provides ROM integration for Shrine.
Installation
$ bundle add shrine-romQuick start
Let's asume we have "photos" that have an "image" attachment. We start by
configuring Shrine in our initializer, and loading the rom plugin provided by
shrine-rom:
# Gemfile
gem "shrine", "~> 3.0"
gem "shrine-rom"require "shrine"
require "shrine/storage/file_system"
Shrine.storages = {
cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), # temporary
store: Shrine::Storage::FileSystem.new("public", prefix: "uploads"), # permanent
}
Shrine.plugin :rom # ROM integration, provided by shrine-rom
Shrine.plugin :cached_attachment_data # for retaining the cached file across form redisplays
Shrine.plugin :rack_file # for accepting Rack uploaded file hashes
Shrine.plugin :form_assign # for assigning file from form fields
Shrine.plugin :restore_cached_data # re-extract metadata when attaching a cached file
Shrine.plugin :validation_helpers # for validating uploaded files
Shrine.plugin :determine_mime_type # determine MIME type from file contentNext, we run a migration that adds an image_data text or JSON column to our
photos table:
ROM::SQL.migration do
change do
add_column :photos, :image_data, :text # or :jsonb
end
endNow we can define an ImageUploader class and include an attachment module
into our Photo entity:
class ImageUploader < Shrine
# we add some basic validation
Attacher.validate do
validate_max_size 20*1024*1024
validate_mime_type %w[image/jpeg image/png image/webp]
validate_extension %w[jpg jpeg png webp]
end
endclass PhotoRepo < ROM::Repository[:photos]
commands :create, update: :by_pk, delete: :by_pk
struct_namespace Entities
def find(id)
photos.fetch(id)
end
endmodule Entities
class Photo < ROM::Struct
include ImageUploader::Attachment[:image]
end
endLet's now add fields for our image attachment to our HTML form for creating
photos:
# with Forme gem:
form @photo, action: "/photos", enctype: "multipart/form-data", namespace: "photo" do |f|
f.input :title, type: :text
f.input :image, type: :hidden, value: @attacher&.cached_data
f.input :image, type: :file
f.button "Create"
endNow in our controller we can attach the uploaded file from request params. We'll assume you're using dry-validation for validating user input.
post "/photos" do
@photo = Entities::Photo.new
@attacher = @photo.image_attacher
@attacher.form_assign(params["photo"]) # assigns file and performs validation
contract = CreatePhotoContract.new(image_attacher: @attacher)
result = contract.call(params["photo"])
if result.success?
@attacher.finalize # upload cached file to permanent storage
attributes = result.to_h
attributes.merge!(@attacher.column_values)
photo_repo.create(attributes)
# ...
else
# ... render view with form ...
end
endclass CreatePhotoContract < Dry::Validation::Contract
option :image_attacher
params do
required(:title).filled(:string)
end
# copy any attacher's validation errors into our dry-validation contract
rule(:image) do
key.failure("must be present") unless image_attacher.attached?
image_attacher.errors.each { |message| key.failure(message) }
end
endOnce the image has been successfully attached to our photo, we can retrieve the
image URL by calling #image_url on the entity:
<img src="<%= @photo.image_url %>" />If you want to see a complete example with direct uploads and backgrounding, see the demo app.
Understanding
The rom plugin builds upon Shrine's entity plugin, providing
persistence functionality.
The attachment module included into the entity provides convenience methods for reading the data attribute:
photo.image_data #=> '{"id":"path/to/file","storage":"store","metadata":{...}}'
photo.image #=> #<Shrine::UploadedFile @id="path/to/file" @storage_key=:store ...>
photo.image_url #=> "https://s3.amazonaws.com/..."
photo.image_attacher #=> #<Shrine::Attacher ...>Updating
When updating the attached file for an existing record, it's important to
initialize the attacher from that record's current attachment. That way the old
file will be automatically deleted on Attacher#finalize.
photo = photo_repo.find(photo_id)
photo.image #=> #<Shrine::UploadedFile @id="foo" ...>
attacher = photo.image_attacher # has current attachment
attacher.assign(file)
photo_repo.update(photo_id, attacher.column_values)
attacher.finalize # deletes previous attachmentAttacher state
Unlike the model plugin, the entity plugin doesn't memoize the
Shrine::Attacher instance:
photo.image_attacher #=> #<Shrine::Attacher:0x00007ffe564085d8>
photo.image_attacher #=> #<Shrine::Attacher:0x00007ffe53b2f378> (different instance)So, if you want to update the attacher state, you need to first assign it to a variable:
attacher = photo.image_attacher
attacher.assign(file)
attacher.finalizePersisting
Normally you'd persist attachment changes explicitly, by using
Attacher#column_data or Attacher#column_values:
attacher = photo.image_attacher
attacher.attach(file)
photo_repo.create(image_data: attacher.column_data)
# or
photo_repo.create(attacher.column_values)Backgrounding
If you want to delay promotion into a background job, you need to call
Attacher#finalize after you've persisted the cached file, so that your
background job is able to retrieve the record. We'll assume your repository
objects are registered using dry-container.
Shrine.plugin :backgrounding
Shrine::Attacher.promote_block do
Attachment::PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
end
Shrine::Attacher.destroy_block do
Attachment::DestroyJob.perform_async(self.class.name, data)
endattacher = photo.image_attacher
attacher.assign(file)
photo = photo_repo.create(attacher.column_values)
attacher.finalize # calls the promote blockclass Attachment::PromoteJob
include Sidekiq::Worker
def perform(attacher_class, entity_class, entity_id, name, file_data)
attacher_class = Object.const_get(attacher_class)
# entity_class is your custom ROM::Struct entity class name.
# generate repo_registry_name from entity_class.
repo = Application[repo_registry_name] # retrieve repo from container
entity = repo.find(entity_id)
attacher = attacher_class.retrieve(
entity: entity,
name: name,
file: file_data,
repository: repo, # repository needs to be passed in and it should be the last parameter
)
attacher.atomic_promote
rescue Shrine::AttachmentChanged, # attachment has changed
ROM::TupleCountMismatchError # record has been deleted
end
endclass Attachment::DestroyJob
include Sidekiq::Worker
def perform(attacher_class, data)
attacher = Object.const_get(attacher_class).from_data(data)
attacher.destroy
end
endContributing
Tests are run with:
$ bundle exec rake testCode of Conduct
Everyone interacting in the Shrine::Rom project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.