25
votes

The scenario is a normal model that contains a paperclip attachment along with some other columns that have various validations. When a form to to create an object cannot be saved due to a validation error unrelated to the attachment, columns like strings are preserved and remain prefilled for the user, but a file selected for uploading is completely lost and must be reselected by the user.

Is there a standard approach to preserving the attachment in the case of a model validation error? This seems like a very common use case.

It seems inelegant to hack up a solution where the file is saved without an owner and then reconnected to the object after it's successfully saved so I'm hoping to avoid this.

8
When save is unsuccessful, you could remove all params except the file and call 'update_attribute' which works without validation.apneadiving
That would work though I'd like to avoid saving unvalidated records into the database since I then need to maintain state about validation. Intuitively it feels like paperclip probably has some means of more elegantly handling this since it abstracts away so many other parts of file handling.peanutbutter
did you find a solution?glebm

8 Answers

12
votes

Switch to using CarrierWave. I know this was in a comment, but I just spent all day making the transition so my answer may be helpful still.

First you can follow a great railscast about setting up carrier wave: http://railscasts.com/episodes/253-carrierwave-file-uploads

To get it to preserve the image between posts, you need to add a hidden field with the suffix 'cache':

<%= form_for @user, :html => {:multipart => true} do |f| %>
  <p>
    <label>My Avatar</label>
    <%= f.file_field :avatar %>
    <%= f.hidden_field :avatar_cache %>
  </p>
<% end %>

For Heroku

And if you're deploying to Heroku like I am, you need to make some changes to get it to work, since the caching works by temporarily saving uploads in a directory called public/uploads. Since the filesystem is readonly in Heroku, you need to have it use the tmp folder instead, and have rack serve static files from there.

Tell carrierwave to use the tmp folder for caching.

In your config/initializers/carrierwave.rb (feel free to create if not there), add:

CarrierWave.configure do |config|
  config.root = Rails.root.join('tmp')
  config.cache_dir = 'carrierwave'
end

Configure rack to serve static files in from the tmp/carrierwave folder

In your config.ru file, add:

use Rack::Static, :urls => ['/carrierwave'], :root => 'tmp'

For an example of a fully functional barebones rails/carrierwave/s3/heroku app, check out:

https://github.com/trevorturk/carrierwave-heroku (no affiliation, just was useful).

Hope this helps!

3
votes

I had to fix this on a recent project using PaperClip. 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.

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: ''}
3
votes

Following the idea of @galatians , i got this solution (and worked beautfully )

Created a repo to that example: * https://github.com/mariohmol/paperclip-keeponvalidation

  1. The first thing to do is put some methods in your base active record, so every model that uses attach you can make it work

In config/initializers/active_record.rb

module ActiveRecord
    class Base

    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

    #ex: @avatar_cache = cache_files(avatar,@avatar_cache)
    def cache_files(avatar,avatar_cache)
      if avatar.queued_for_write[:original]
        FileUtils.cp(avatar.queued_for_write[:original].path, avatar.path(:original))
        avatar_cache = encrypt(avatar.path(:original))
      elsif avatar_cache.present?
        File.open(decrypt(avatar_cache)) {|f| assign_attributes(avatar: f)}
      end
      return avatar_cache
    end

    end
end
  1. After that , include in your model and attached field, the code above

In exemple, i included that into /models/users.rb

  has_attached_file :avatar, PaperclipUtils.config
  attr_accessor :avatar_cache
  def cache_images
    @avatar_cache=cache_files(avatar,@avatar_cache)
  end
  1. In your controller, add this to get from cache the image (just before the point where you save the model)

    @user.avatar_cache = params[:user][:avatar_cache]

    @user.cache_images

    @user.save

  2. And finally include this in your view, to record the location of the current temp image

f.hidden_field :avatar_cache

  1. If you want to show in view the actual file, include it:
<% if @user.avatar.exists?  %>
<label class="field">Actual Image </label>
  <div class="field file-field">  
      <%= image_tag @user.avatar.url %>
    </div>
<% end %>
1
votes

As of Sept 2013, paperclip has no intention of "fixing" the losing of attached files after validation. "The problem is (IMHO) more easily and more correctly avoided than solved"

https://github.com/thoughtbot/paperclip/issues/72#issuecomment-24072728

I'm considering the CarrierWave solution proposed in John Gibb's earlier solution

1
votes

Also check out refile (newer option)

Features:

  • Configurable backends, file system, S3, etc...
  • Convenient integration with ORMs
  • On the fly manipulation of images and other files
  • Streaming IO for fast and memory friendly uploads
  • Works across form redisplays, i.e. when validations fail, even on S3
  • Effortless direct uploads, even to S3
  • Support for multiple file uploads

https://gorails.com/episodes/file-uploads-with-refile

0
votes

If the image isn't required why not split the form into two stages, the first one creates the object, the second page lets you add optional information (like a photo).

Alternatively you could validate the form as the user enters the information so that you don't have to submit the form to find out your data is invalid.

0
votes

save your picture first than try the rest

lets say you have a user with a paperclip avatar:

def update
  @user = current_user
  unless params[:user][:avatar].nil?
    @user.update_attributes(avatar: params[:user][:avatar])
    params[:user].delete :avatar
  end
  if @user.update_attributes(params[:user])
    redirect_to edit_profile_path, notice: 'User was successfully updated.' 
  else
    render action: "edit" 
  end
end
0
votes

In view file just put if condition that should accept only the record which had valid id. In my scenario this is the code snippet

            <p>Uploaded files:</p>
            <ul>
                <% @user.org.crew.w9_files.each do |file| %>
                  <% if file.id.present? %>
                    <li> <%= rails code to display value %> </li>
                  <% end %>
                <% end %>
            </ul>