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

8

25

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.

Trencherman answered 4/3, 2011 at 19:58 Comment(2)
When save is unsuccessful, you could remove all params except the file and call 'update_attribute' which works without validation.Splashboard
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.Trencherman
O
12

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!

Opsonin answered 15/4, 2011 at 0:56 Comment(0)
F
3

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: ''}
Forficate answered 19/5, 2014 at 14:44 Comment(1)
Caching file on front-end is only good for small files/few files, as with larger files the HTML gets bloated and therefore a huge bottleneck is created when reading back this data.Ruthenious
C
3

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 %>
Curvy answered 21/1, 2015 at 16:48 Comment(1)
The "f.hidden_field :avatar_cache" has the file content.Ruthenious
L
1

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

Lindeman answered 24/2, 2014 at 14:51 Comment(0)
G
1

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

Grethel answered 15/6, 2015 at 17:2 Comment(0)
A
0

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.

Aeolotropic answered 4/3, 2011 at 20:11 Comment(5)
Yep, both approaches would technically work. I'm hoping to find something more elegant since this seems so fundamental to attachment handling in rails.Trencherman
It's not really a rails problem, the server doesn't send back the the image.Aeolotropic
The advantage of paperclip is that it allows a Rails app to transparently treat an attachment as just another column. This is a case where the default is seemingly broken with standard rails model validation. Therefore I'm looking for the most elegant, least hacked up approach.Trencherman
The best way is to find another UI paradigm that works for you. What are the photos going to be used for?Aeolotropic
One thing you should look at is carrier wave (github.com/jnicklas/carrierwave) as tehy've got support for what you want to do out of the box.Aeolotropic
H
0

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
Harbinger answered 17/10, 2012 at 9:4 Comment(1)
Not only this does not work on create method but also puts the model in an inconsistent state. The idea is to not lose the attachment but not by modifying the model partially.Rudie
D
0

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>
Dru answered 14/7, 2017 at 7:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.