40

I have a form with multiple file uploads, The issue is when i am submitting the form and an validation error occurs, the file input field gets reset.

I basically wanted to persist those files inside the file input field for the complete process.

I have also gone through few links

How can I "keep" the uploaded image on a form validation error?

Please let me know what are the various options in such cases that one can follow.

cseelus
  • 1,447
  • 1
  • 20
  • 31
RailsEnthusiast
  • 1,251
  • 2
  • 12
  • 18
  • 4
    Since Rails 5.2 supports file uploads with Active Storage, is there a way to do retain uploads after form redisplays "natively" now? – cseelus May 16 '18 at 10:45
  • 1
    Does ActiveStorage in Rails 6.0 finally handle this issue? – TomDogg Oct 04 '19 at 17:47
  • 1
    @cseelus yes, i have just found this article here which outlines exactly how to solve this: https://medium.com/earthvectors/validations-and-file-caching-using-activestorage-e16418060f8f see the Solution 2 section – Tony Beninate Jul 17 '21 at 12:18
  • Yes, Rails 6.0 upwards handles this by default, no problem at all. – cseelus Jul 18 '21 at 14:14
  • @TonyBeninate I tried solution 2 given in the article but it still fails when submitting form: I get the following error `ActiveSupport::MessageVerifier::InvalidSignature - ActiveSupport::MessageVerifier::InvalidSignature`. The params sent is a string retrieved from `f.object.photo.signed_id` and not anymore an `ActionDispatch` object. Any ideas how I could resolve that? Thanks! – raphael-allard Mar 28 '22 at 08:27

8 Answers8

7

Carrierwave is a great tool for handling file uploads and can handle this for you

https://github.com/jnicklas/carrierwave#making-uploads-work-across-form-redisplays

msaspence
  • 1,424
  • 2
  • 14
  • 25
  • 1
    I am using gem 'paperclip' in my project, Is there any such option for paperclip – RailsEnthusiast Mar 28 '13 at 12:24
  • I haven't used paper clip myself, certainly not obviously one of the reason's I use CarrierWave – msaspence Mar 28 '13 at 12:29
  • You could migrate to CarrierWave if this is something you really need. https://github.com/jnicklas/carrierwave/wiki/How-to%3A-migrate-from-paperclip-to-carrierwave-%28example-thor-task%29 – msaspence Mar 28 '13 at 12:36
  • Persisting password in such cases is also not possible ? right ? – RailsEnthusiast Apr 05 '13 at 10:20
  • 4
    This answer comes within an ace of being acceptable, but it should really explain more specifically why this particular tool is useful for this task. Not just "it works for this". – Nathan Tuggy Apr 30 '15 at 01:32
3

I took a completely different approach to the other solutions on offer here, as I didn't fancy switching to CarrierWave or using yet another gem to implement a hack to get around this.

Basically, I define placeholders for validation error messages and then make an AJAX call to the relevant controller. should it fail validation I simply populate the error message placeholders - this leaves everything in place client side including the file input ready for resubmission.

Example follows, demonstrating an organisation with nested address model and a nested logo model (that has a file attachment) - this has been cut for brevity :

organisations/_form.html.erb

<%= form_for @organisation, html: {class: 'form-horizontal', role: 'form', multipart: true}, remote: true do |f| %>
  <%= f.label :name %>
  <%= f.text_field :name %>
  <p class='name error_explanation'></p>

  <%= f.fields_for :operational_address do |fa| %>
    <%= fa.label :postcode %>
    <%= fa.text_field :postcode %>
    <p class='operational_address postcode error_explanation'></p>
  <% end %>

  <%= f.fields_for :logo do |fl| %>
    <%= fl.file_field :image %>
    <p class='logo image error_explanation'></p>
  <% end %>
<% end %>

organisations_controller.rb

def create   
  if @organisation.save
    render :js => "window.location = '#{organisations_path}'"
  else
    render :validation_errors
  end
end

organisations/validation_errors.js.erb

$('.error_explanation').html('');
<% @organisation.errors.messages.each do |attribute, messages| %>
  $('.<%= attribute %>.error_explanation').html("<%= messages.map{|message| "'#{message}'"}.join(', ') %>");
<% end %>
David
  • 3,510
  • 3
  • 21
  • 39
2

Created a repo with a example of using Paperclip on rails and mainting your files when validation error occurs

https://github.com/mariohmol/paperclip-keeponvalidation

mariomol
  • 667
  • 1
  • 7
  • 15
1

I had to fix this on a recent project using the Paperclip Gem. It's a bit hacky but it works. I've tried calling cache_images() using after_validation and before_save in the model but it fails on create for some reason that I can't determine so I just call it from the controller instead. Hopefully this saves someone else some time!

model:

class Shop < ActiveRecord::Base    
  attr_accessor :logo_cache

  has_attached_file :logo

  def cache_images
    if logo.staged?
      if invalid?
        FileUtils.cp(logo.queued_for_write[:original].path, logo.path(:original))
        @logo_cache = encrypt(logo.path(:original))
      end
    else
      if @logo_cache.present?
        File.open(decrypt(@logo_cache)) {|f| assign_attributes(logo: f)}
      end
    end
  end

  private

  def decrypt(data)
    return '' unless data.present?
    cipher = build_cipher(:decrypt, 'mypassword')
    cipher.update(Base64.urlsafe_decode64(data).unpack('m')[0]) + cipher.final
  end

  def encrypt(data)
    return '' unless data.present?
    cipher = build_cipher(:encrypt, 'mypassword')
    Base64.urlsafe_encode64([cipher.update(data) + cipher.final].pack('m'))
  end

  def build_cipher(type, password)
    cipher = OpenSSL::Cipher::Cipher.new('DES-EDE3-CBC').send(type)
    cipher.pkcs5_keyivgen(password)
    cipher
  end

end

controller:

def create
  @shop = Shop.new(shop_params)
  @shop.user = current_user
  @shop.cache_images

  if @shop.save
    redirect_to account_path, notice: 'Shop created!'
  else
    render :new
  end
end

def update
  @shop = current_user.shop
  @shop.assign_attributes(shop_params)
  @shop.cache_images

  if @shop.save
    redirect_to account_path, notice: 'Shop updated.'
  else
    render :edit
  end
end

view:

= f.file_field :logo
= f.hidden_field :logo_cache

- if @shop.logo.file?
  %img{src: @shop.logo.url, alt: ''}
galatians
  • 738
  • 7
  • 12
1

Well - I thought of taking a different approach to this; Instead of temporarily storing the file on the server, why not serve it back to the client to be resubmitted when the user fixes the validation issues.

This might still need a bit of refinement but it's the general concept:

# in the controller - save the file and its attributes to params

def create
  # ...
  if params[:doc] # a regular file uploaded through the file form element
    # when the form re-renders, it will have those additional params available to it
    params[:uploaded_file] = params[:doc].read # File contents
    params[:uploaded_file_original_filename] = params[:doc].original_filename
    params[:uploaded_file_headers] = params[:doc].headers
    params[:uploaded_file_content_type] = params[:doc].content_type
  elsif params[:uploaded_file] # a file coming through the form-resubmit
    # generate an ActionDispatch::Http::UploadedFile
    tempfile = Tempfile.new("#{params[:uploaded_file_original_filename]}-#{Time.now}")
    tempfile.binmode
    tempfile.write CGI.unescape(params[:uploaded_file]) #content of the file / unescaped
    tempfile.close

    # merge into the params
    params.merge!(doc: 
       ActionDispatch::Http::UploadedFile.new(
                                :tempfile => tempfile,
                                :filename => params[:uploaded_file_original_filename],
                                :head => params[:uploaded_file_headers],
                                :type => params[:uploaded_file_content_type]
                           )
                 )

  end
  #...
  # params (including the UploadedFile) can be used to generate and save the model object
end


# in the form (haml)
- if !params[:uploaded_file].blank?
  # file contents in hidden textarea element
  = text_area_tag(:uploaded_file, CGI.escape(params[:uploaded_file]), style: 'display: none;') #escape the file content
  = hidden_field_tag :uploaded_file_headers, params[:uploaded_file_headers]
  = hidden_field_tag :uploaded_file_content_type, params[:uploaded_file_content_type]
  = hidden_field_tag :uploaded_file_original_filename, params[:uploaded_file_original_filename]
tamersalama
  • 4,093
  • 1
  • 32
  • 35
0

A workaround for this rather than an outright solution is to use client side validation so that the file isn't lost because the whole form persists.

The few users that don't have JavaScript enabled will lose the files between requests, but perhaps this % is so low for you as to make it an acceptable compromise. If this is the route you decide to go down I'd recommend this gem

https://github.com/bcardarella/client_side_validations

Which makes the whole process really simple and means you don't have to rewrite your validation in JavaScript

msaspence
  • 1,424
  • 2
  • 14
  • 25
0

Browsers block against setting the value attribute on input of file type for security reasons so that you can't upload a file without the user's selected any file itself.

Pre-Populate HTML form file input

You can use carrierwave: https://github.com/carrierwaveuploader/carrierwave

Or validate the model via js request.

artamonovdev
  • 2,260
  • 1
  • 29
  • 33
0

I found a way to keep files without using gems, it can probably be improved but I am still a young dev :)

I draw the whole thing from solution 2 contained in this article: https://medium.com/earthvectors/validations-and-file-caching-using-activestorage-e16418060f8f

First of all, you need to add an hidden_field within your form that contains the signed_id of the attachment:

<%= f.hidden_field :file_signed_id, value: @model.file.signed_id if @model.file.attached? %>
<%= f.input :file %>

The problem when validations fail (in my case), it keeps the file in memory but do not send anymore the ActionDispatch object as parameters. To override it, I did the following in my controller:

if file = params.dig(:model, :file)
  blob = ActiveStorage::Blob.create_and_upload!(
    io: File.open(file.tempfile),
    filename: file.original_filename
  )
  @model.file.attach(blob)
elsif file_signed_id = params.dig(:model, file_signed_id)
  blob = ActiveStorage::Blob.find_signed(file_signed_id)
  @model.file.attach(blob)
end

You then can display your file when rendering your view again:

<%= link_to @model.file.filename, url_for(@model.file) if @model.file.attached? %>

The only problem I see with this workaround it is that it will create a blob object even if validations fail.

I hope it will help someone!

raphael-allard
  • 205
  • 2
  • 9