Paperclip skip style on first save - ruby-on-rails

This is my code:
:styles => lambda { |attachment| attachment.instance.define_styles }
def define_styles
return_styles = Hash.new
case self.imageable_type
when "Admin::ProductDetail"
return_styles[:thumb] = "70x60>"
return_styles[:front] = "450x400>"
else
end
return_styles
end
The problem is that when I first upload the image, it dont use the styles... only after I reupload the image, so it is only creating the default style, not the :thumb or :front in the first upload.

You can use the callback before_post_process
:styles => lambda { |attachment| attachment.instance.define_styles }
before_post_process :skip_on_create
def skip_on_create
!new_record?
end
When this callback returns false, the post processing step will be halted
Documentation here

I added this code and it works now.
after_create :reprocess
def reprocess
self.image.reprocess!
end
But I don't know if it's the right way to do it.

Related

Using different style for paperclip image in Rails while rendering in json

I have a model which has an image field managed by paperclip:
class Meal < ActiveRecord::Base
has_attached_file :image, :default_url => "/images/normal/missing.png",
:styles => { :medium => "612x612", :small => "300x300" },
:path => ":rails_root/public/system/:attachment/:id/:style/:filename",
:url => "/system/:attachment/:id/:style/:filename"
I can access the different sizes like this:
meals.each do |n|
n.image.url(:small) # gives url for small images
puts n.image.url # returns url for original images, I want this to return small for this function
end
I am rendering the meals in JSON using render :json.
My question is, how can I pass the small image URLs into my meals variable (in my controller below)? I want to be able to return small image URLs as I tried doing above, except return it when my response renders (see below).
UPDATE:
In my controller:
def view_patient
response = Response.new
this_doctor = Doctor.find_by_remember_token(Doctor.digest(auth_params["remember_token"]))
if this_doctor
this_patient = this_doctor.users.find_by_id(params[:id])
if this_patient
meals = this_patient.meals
#
# Here should be code on how to set the meals.image.url to small
glucoses = this_patient.glucoses
response.data = { :patient => this_patient, :meals => meals }
response.code = true
else
response.error = "Could not find patient"
response.code = false
end
else
response.error = "Please Login"
response.code = false
end
render :json => response.json
end
TLDR
# inside meal.rb
def as_json(options=nil)
super( (options || {}).merge({
:methods => [:small_url]
}))
end
def small_url
self.image.url(:small)
end
You can then access the URL in your JSON structure
JSON.parse(meal.to_json)['small_url']
Explanation
When a ActiveModel is serialized through to_json the method as_json is first invoked on the object to separate the actual construction of the JSON data structure from the rendering. This data structure (a hash really) is then encoded as a JSON string through ActiveSupport.
So in orde to customize the object we wish to display as JSON we need to override the as_json method of that object - which is well documented. As per the documentation, the methods key for the options hash simply invokes the methods listed in the array passed as the value (in our case just small_url) and creates a key in the hash to be JSON encoded, with the value of the method invocation.
For an even more detailed explanation, please see this excellent answer.

Use paperclip for saving base64 images obtained from an api

I have a Photo model with an image attribute. The image contains a base64 string obtained from an api. I need to run an after_create callback and I was thinking I could use Paperclip for saving the image to the disk in the callback as it would save me some work implementing the folder structure in the public folder and generating thumbnails. Is there an easy way to do that?
To answer my own question, here is what I've come up with:
class Photo < ActiveRecord::Base
before_validation :set_image
has_attached_file :image, styles: { thumb: "x100>" }
validates_attachment :image, presence: true, content_type: { content_type: ["image/jpeg", "image/jpg"] }, size: { in: 0..10.megabytes }
def set_image
StringIO.open(Base64.decode64(image_json)) do |data|
data.class.class_eval { attr_accessor :original_filename, :content_type }
data.original_filename = "file.jpg"
data.content_type = "image/jpeg"
self.image = data
end
end
end
image_json is a text field containing the actual base64 encoded image (just the data part, eg "/9j/4AAQSkZJRg...")
your set_image should look something like this
def set_image
self.update({image_attr: "data:image/jpeg;base64," + image_json[PATH_TO_BASE64_DATA]})
end
At least with Paperclip 5 it works out of the box you need to provide base64 string with format data:image/jpeg;base64,#{base64_encoded_file}
For you model it will be
Photo.new(
image: "data:image/jpeg;base64,#{image_json}",
image_file_name: 'file.jpg' # this way you can provide file_name
)
Additionally in your controller you do not need to change anything:-) (maybe you would like to accept :image_file_name in params)
As of Paperclip 5.2 you need to register the DataUriAdapter for Paperclip to handle base64 images for you.
In config/initializers/paperclip put:
Paperclip::DataUriAdapter.register
Then as #eldi says you can just do:
Photo.new(
image: "data:image/jpeg;base64,#{image_json}",
image_file_name: 'file.jpg' # this way you can provide file_name
)
(See Paperclip release notes here)
require 'RMagick'
data = params[:image_text]# code like this data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABPUAAAI9CAYAAABSTE0XAAAgAElEQVR4Xuy9SXPjytKm6ZwnUbNyHs7Jc7/VV9bW1WXWi9q
image_data = Base64.decode64(data['data:image/png;base64,'.length .. -1])
new_file=File.new("somefilename.png", 'wb')
new_file.write(image_data)
After you kan use image as file
Photo.new(image: image)#save useng paperclip in Photo model

Sending a custom parameter from a form into a custom Paperclip Processor

I have a custom Paperclip processor which extracts a screenshot from an MP4 using FFMPEG. This is working very well but I would like for the user to be able to select the time at which the screenshot is taken in the video in the upload form before it is processed, which has so far eluded me.
upload.rb
class Upload < ActiveRecord::Base
has_attached_file :uploaded_file,
styles: {
screenshot: { :processors => [:screenshot], :format => 'png' }
},
default_url: "/images/:style/missing.png"
default_scope order('created_at DESC')
def paperclip_screenshot_time
# I'm attempting to pull in params[:screenshot_time] (set in the view)
self.screenshot_time.to_s
end
end
The above does not work, however if I change the paperclip_screenshot_time method to:
def paperclip_screenshot_time
'5'
end
It works fine.
Here is an excerpt from the processor, which as I say is also working fine:
screenshot.rb
module Paperclip
class Screenshot < Processor
def initialize(file, options = {}, attachment = nil)
super
#file = file
#options = options
#instance = attachment.instance
#current_format = '.*'
#basename = File.basename(#file.path, #current_format)
#whiny = options[:whiny].nil? ? true : options[:whiny]
end
def target
#attachment.instance
end
def make
# Removed for brevity
begin
system('ffmpeg -i ' + #file.path + ' -ss 00:00:0' + target.paperclip_screenshot_time.to_s + ' -f image2 -vframes 1 ' + tmp_dir + '/' + tmp_filename)
end
# Removed for brevity
end
end
end
...and the relevant field from the view.
upload.jst.erb
<input name="upload[screenshot_time]" type="number" min="0" max="9" value="4" class="js-form-input" />
The field is also permitted in the controller:
uploads_controller.rb
def upload_params
params[:upload].permit(:uploaded_file, :title, :screenshot_time)
end
So, to re-iterate the problem seems to be in setting paperclip_screenshot_time with the value from upload[screenshot_time] in the view. How should I go about doing this?
Any help gratefully received!
It turns out the issue was the order in which the upload params were set in the controller.
So, in order for title and screenshot_time to be accessible in the processor they had to come before uploaded_file. So by changing the upload_params method to this:
def upload_params
params[:upload].permit(:title, :screenshot_time, :uploaded_file)
end
It all works smoothly.

save! method for referenced attributes in mongoid

I use Rails 3.0.6 with mongoID 2.0.2. Recently I encountered an issue with save! method when overriding setter (I am trying to create my own nested attributes).
So here is the model:
class FeedItem
include Mongoid::Document
has_many :audio_refs
def audio_refs=(attributes_array, binding)
attributes_array.each do |attributes|
if attributes[:audio_track][:id]
self.audio_refs.build(:audio_track => AudioTrack.find(attributes[:audio_track][:id]))
elsif attributes[:audio_track][:file]
self.audio_refs.build(:audio_track => AudioTrack.new(:user_id => attributes[:audio_track][:user_id], :file => attributes[:audio_track][:file]))
end
end
if !binding
self.save!
end
end
AudioRef model (which is just buffer between audio_tracks and feed_items) is:
class AudioRef
include Mongoid::Document
belongs_to :feed_item
belongs_to :audio_track
end
And AudioTrack:
class AudioTrack
include Mongoid::Document
has_many :audio_refs
mount_uploader :file, AudioUploader
end
So here is the spec for the FeedItem model which doesn`t work:
it "Should create audio_track and add audio_ref" do
#audio_track = Fabricate(:audio_track, :user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3"))
#feed_item= FeedItem.new(
:user => #author,
:message => {:body => Faker::Lorem.sentence(4)},
:audio_refs => [
{:audio_track => {:id => #audio_track.id}},
{:audio_track => {:user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3")}}
]
)
#feed_item.save!
#feed_item.reload
#feed_item.audio_refs.length.should be(2)
end
As you can see, the reason I am overriding audio_refs= method is that FeedItem can be created from existing AudioTracks (when there is params[:audio_track][:id]) or from uploaded file (params[:audio_track][:file]).
The problem is that #feed_item.audio_refs.length == 0 when I run this spec, i.e. audio_refs are not saved. Could you please help me with that?
Some investigation:
1) binding param is "true" by default (this means we are in building mode)
I found a solution to my problem but I didnt understand why save method doesnt work and didn`t make my code work. So first of all let me describe my investigations about the problem. After audio_refs= is called an array of audio_refs is created BUT in any audio_ref is no feed_item_id. Probably it is because the feed_item is not saved by the moment.
So the solution is quite simple - Virtual Attributes. To understand them watch corresponding railscasts
So my solution is to create audio_refs by means of callback "after_save"
I slightly changed my models:
In FeedItem.rb I added
attr_writer :audio_tracks #feed_item operates with audio_tracks array
after_save :assign_audio #method to be called on callback
def assign_audio
if #audio_tracks
#audio_tracks.each do |attributes|
if attributes[:id]
self.audio_refs << AudioRef.new(:audio_track => AudioTrack.find(attributes[:id]))
elsif attributes[:file]
self.audio_refs << AudioRef.new(:audio_track => AudioTrack.new(:user_id => attributes[:user_id], :file => attributes[:file]))
end
end
end
end
And the spec is now:
it "Should create audio_track and add audio_ref" do
#audio_track = Fabricate(:audio_track, :user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3"))
#feed_item= FeedItem.new(
:user => #author,
:message => {:body => Faker::Lorem.sentence(4)},
:audio_tracks => [
{:id => #audio_track.id},
{:user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3")}
]
)
#feed_item.save!
#feed_item.reload
#feed_item.audio_refs.length.should be(2)
end
And it works fine!!! Good luck with your coding)
Check that audio_refs=() is actually being called, by adding debug output of some kind. My feeling is that your FeedItem.new() call doesn't use the audio_refs=() setter.
Here's the source code of the ActiveRecord::Base#initialize method, taken from APIdock:
# File activerecord/lib/active_record/base.rb, line 1396
def initialize(attributes = nil)
#attributes = attributes_from_column_definition
#attributes_cache = {}
#new_record = true
#readonly = false
#destroyed = false
#marked_for_destruction = false
#previously_changed = {}
#changed_attributes = {}
ensure_proper_type
populate_with_current_scope_attributes
self.attributes = attributes unless attributes.nil?
result = yield self if block_given?
_run_initialize_callbacks
result
end
I don't currently have an environment to test this, but it looks like it's setting the attributes hash directly without going through each attribute's setter. If that's the case, you'll need to call your setter manually.
Actually, I think the fact you're not getting an exception for the number of arguments (binding not set) proves that your setter isn't being called.

Paperclip renaming files after they're saved

How do I rename a file after is has been uploaded and saved?
My problem is that I need to parse information about the files automatically in order to come up with the file name the file should be saved as with my application, but I can't access the information required to generate the file name till the record for the model has been saved.
If, for example, your model has attribute image:
has_attached_file :image, :styles => { ...... }
By default papepclip files are stored in /system/:attachment/:id/:style/:filename.
So, You can accomplish it by renaming every style and then changing image_file_name column in database.
(record.image.styles.keys+[:original]).each do |style|
path = record.image.path(style)
FileUtils.move(path, File.join(File.dirname(path), new_file_name))
end
record.image_file_name = new_file_name
record.save
Have you checked out paperclip interpolations?
If it is something that you can figure out in the controller (before it gets saved), you can use a combination of the controller, model, and interpolation to solve your problem.
I have this example where I want to name a file based on it's MD5 hash.
In my controller I have:
params[:upload][:md5] = Digest::MD5.file(file.path).hexdigest
I then have a config/initializers/paperclip.rb with:
Paperclip.interpolates :md5 do|attachment,style|
attachment.instance.md5
end
Finally, in my model I have:
validates_attachment_presence :upload
has_attached_file :upload,
:path => ':rails_root/public/files/:md5.:extension',
:url => '/files/:md5.:extension'
To add to #Voyta's answer, if you're using S3 with paperclip:
(record.image.styles.keys+[:original]).each do |style|
AWS::S3::S3Object.move_to record.image.path(style), new_file_path, record.image.bucket_name
end
record.update_attribute(:image_file_name, new_file_name)
My avatar images are named with the user slug, if they change their names I have to rename images too.
That's how I rename my avatar images using S3 and paperclip.
class User < ActiveRecord::Base
after_update :rename_attached_files_if_needed
has_attached_file :avatar_image,
:storage => :s3,
:s3_credentials => "#{Rails.root}/config/s3.yml",
:path => "/users/:id/:style/:slug.:extension",
:default_url => "/images/users_default.gif",
:styles => { mini: "50x50>", normal: "100x100>", bigger: "150x150>" }
def slug
return name.parameterize if name
"unknown"
end
def rename_attached_files_if_needed
return if !name_changed? || avatar_image_updated_at_changed?
(avatar_image.styles.keys+[:original]).each do |style|
extension = Paperclip::Interpolations.extension(self.avatar_image, style)
old_path = "users/#{id}/#{style}/#{name_was.parameterize}#{extension}"
new_path = "users/#{id}/#{style}/#{name.parameterize}#{extension}"
avatar_image.s3_bucket.objects[old_path].move_to new_path, acl: :public_read
end
end
end
And to add yet another answer, here is the full method I'm using for S3 renaming :
def rename(key, new_name)
file_name = (key.to_s+"_file_name").to_sym
old_name = self.send(file_name)
(self.send(key).styles.keys+[:original]).each do |style|
path = self.send(key).path(style)
self[file_name] = new_name
new_path = self.send(key).path(style)
new_path[0] = ""
self[file_name] = old_name
old_obj = self.send(key).s3_object(style.to_sym)
new_obj = old_obj.move_to(new_path)
end
self.update_attribute(file_name, new_name)
end
To use : Model.find(#).rename(:avatar, "test.jpg")
I'd like to donate my "safe move" solution that doesn't rely on any private API and protects against data loss due to network failure:
First, we get the old and new paths for every style:
styles = file.styles.keys+[:original]
old_style2key = Hash[ styles.collect{|s| [s,file.path(s).sub(%r{\A/},'')]} ]
self.file_file_name = new_filename
new_style2key = Hash[ styles.collect{|s| [s,file.path(s).sub(%r{\A/},'')]} ]
Then, we copy every file to it's new path. Since the default path includes both object ID and filename, this can never collide with the path for a different file. But this will fail if we try to rename without changing the name:
styles.each do |style|
raise "same key" if old_style2key[style] == new_style2key[style]
file.s3_bucket.objects[old_style2key[style]].copy_to(new_style2key[style])
end
Now we apply the updated model to the DB:
save!
It is important to do this after we create the new S3 objects but before we delete the old S3 objects. Most of the other solutions in this thread can lead to a loss of data if the database update fails (e.g. network split with bad timing), because then the file would be at a new S3 location but the DB still points to the old location. That's why my solution doesn't delete the old S3 objects until after the DB update succeeded:
styles.each do |style|
file.s3_bucket.objects[old_style2key[style]].delete
end
Just like with the copy, there's no chance that we accidentally delete another database object's data, because the object ID is included in the path. So unless you rename the same database object A->B and B->A at the same time (e.g. 2 threads), this delete will always be safe.
To add to #Fotios's answer:
its the best way I think to make custom file name, but in case you want file name based on md5 you can use fingerprint which is already available in Paperclip.
All you have to do is to put this to config/initializers/paperclip_defaults.rb
Paperclip::Attachment.default_options.update({
# :url=>"/system/:class/:attachment/:id_partition/:style/:filename"
:url=>"/system/:class/:attachment/:style/:fingerprint.:extension"
})
There's no need to set :path here as by default it's made that way:
:path=>":rails_root/public:url"
I didn't check if it's necessary but in case it doesn't work for you make sure your model is able to save fingerprints in the database -> here
One more tip which I find handy is to use rails console to check how it works:
$ rails c --sandbox
> Paperclip::Attachment.default_options
..
> s = User.create(:avatar => File.open('/foo/bar.jpg', 'rb'))
..
> s.avatar.path
=> "/home/groovy_user/rails_projectes/funky_app/public/system/users/avatars/original/49332b697a83d53d3f3b5bebce7548ea.jpg"
> s.avatar.url
=> "/system/users/avatars/original/49332b697a83d53d3f3b5bebce7548ea.jpg?1387099146"
The following migration solved the problem to me.
Renaming avatar to photo:
class RenamePhotoColumnFromUsers < ActiveRecord::Migration
def up
add_attachment :users, :photo
# Add `avatar` method (from Paperclip) temporarily, because it has been deleted from the model
User.has_attached_file :avatar, styles: { medium: '300x300#', thumb: '100x100#' }
User.validates_attachment_content_type :avatar, content_type: %r{\Aimage\/.*\Z}
# Copy `avatar` attachment to `photo` in S3, then delete `avatar`
User.where.not(avatar_file_name: nil).each do |user|
say "Updating #{user.email}..."
user.update photo: user.avatar
user.update avatar: nil
end
remove_attachment :users, :avatar
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
Hope it helps :)
Another option is set to default, work for all upload.
This example change name file to 'name default' for web, example: test áé.jpg to test_ae.jpg
helper/application_helper.rb
def sanitize_filename(filename)
fn = filename.split /(?<=.)\.(?=[^.])(?!.*\.[^.])/m
fn[0] = fn[0].parameterize
return fn.join '.'
end
Create config/initializers/paperclip_defaults.rb
include ApplicationHelper
Paperclip::Attachment.default_options.update({
:path => ":rails_root/public/system/:class/:attachment/:id/:style/:parameterize_file_name",
:url => "/system/:class/:attachment/:id/:style/:parameterize_file_name",
})
Paperclip.interpolates :parameterize_file_name do |attachment, style|
sanitize_filename(attachment.original_filename)
end
Need restart, after put this code

Resources