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

安稳与你 提交于 2019-11-29 20:19:42

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!

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

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 %>

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

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

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.

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

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>
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!