Provides ROM integration for Shrine.
$ bundle add shrine-romLet'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.
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 ...>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 attachmentUnlike 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.finalizeNormally 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)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 endTests are run with:
$ bundle exec rake testEveryone interacting in the Shrine::Rom project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.