Search code examples
image-uploadingruby-on-rails-5shrine

Shrine with Rails multiple polymorphic image uploads


I've been struggling for about 5 hours trying to understand why Shrine is blocking my uploads. I either get errors like "Shrine: Invalid file", or "Expected Array but got string" in strong params. If there aren't errors, the images aren't actually saved.

require "image_processing/mini_magick"

class ImageUploader < Shrine
  include ImageProcessing::MiniMagick

  plugin :activerecord
  plugin :backgrounding
  plugin :cached_attachment_data
  plugin :determine_mime_type
  plugin :delete_raw
  plugin :direct_upload
  plugin :logging, logger: Rails.logger
  plugin :processing
  plugin :remove_attachment
  plugin :store_dimensions
  plugin :validation_helpers
  plugin :versions

  Attacher.validate do
    validate_max_size 2.megabytes, message: 'is too large (max is 2 MB)'
    validate_mime_type_inclusion ['image/jpg', 'image/jpeg', 'image/png', 'image/gif']
  end

  def process(io, context)
    case context[:phase]
    when :store
      thumb = resize_to_limit!(io.download, 200, 200)
      { original: io, thumb: thumb }
    end
  end
end

class Image < ActiveRecord::Base
  include ImageUploader[:image]
  belongs_to :imageable, polymorphic: true
end

class Product < ApplicationRecord

  has_many :images, as: :imageable, dependent: :destroy    
  accepts_nested_attributes_for :images, allow_destroy: true
...

# Strong Params: 

def product_params
  params.require(:product).permit(
    :name, :brand_id, :category_id, :price, :compare_price, :description,
    images_attributes: { image: [] },
    product_properties_attributes: [:id, :property_id, :value]
  )
...

And my view:

  <%= f.fields_for :images do |image_form| %>
    <%= image_form.file_field :image, multiple: true %>
  <% end %>

According to everything I've read on the docs or from gorails, this should work. Do I need to restructure the images_attributes hash? I also tried using direct_uploads, but struggled to get the presigned_url to work with S3.

Refile makes this really easy, so I'll probably run crying back to that.

Is there something I'm obviously doing wrong?


Solution

  • According to the fields_for documentation, the provided block will be called for each image in the project.images collection. So if your product currently doesn't have any images, the block won't be called (according to the docs).

    For nested attributes to work, you need to forward the following parameters when creating the Product:

    product[images_attributes][0][image] = <file object or data hash>
    product[images_attributes][1][image] = <file object or data hash>
    product[images_attributes][2][image] = <file object or data hash>
    ...
    

    If you look at the "Multiple Files" Shrine guide, it's recommended that you just have a single file field which accepts multiple files:

    <input type="file" name="file" multiple>
    

    And then setup direct uploads for this field using Uppy, dynamically generating the image field for each uploaded file populated with the uploaded file data hash:

    <input type="hidden" name="product[images_attributes][0][image]" value='{"id":"...","storage":"cache","metadata":{...}}'>
    <input type="hidden" name="product[images_attributes][1][image]" value='{"id":"...","storage":"cache","metadata":{...}}'>
    ....
    

    Alternatively you can just let users attach multiple files, which are all submitted to the app, and then destructure them in the controller:

    class ProductsController < ApplicationController
      def create
        images_attributes = params["files"].map { |file| {image: file} }
        Product.create(product_params.merge(images_attributes: images_attributes))
      end
    end
    

    In that case you have to make sure your HTML form has the enctype="multipart/form-data" attribute set (otherwise only the files' filenames will get submitted, not files themselves).