Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Not losing paperclip attachment when model cannot be saved due to validation error

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.

like image 708
peanutbutter Avatar asked Mar 04 '11 19:03

peanutbutter


5 Answers

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!

like image 100
John Gibb Avatar answered Nov 16 '22 18:11

John Gibb


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: ''}
like image 25
galatians Avatar answered Nov 16 '22 19:11

galatians


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 %>
like image 32
mariomol Avatar answered Nov 16 '22 18:11

mariomol


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

like image 1
sdoxsee Avatar answered Nov 16 '22 18:11

sdoxsee


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

like image 1
Abram Avatar answered Nov 16 '22 19:11

Abram