Paperclip renaming files after they're saved
Asked Answered
G

9

19

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.

Grainger answered 25/4, 2010 at 12:2 Comment(1)
What information do you need to generate the file name? It might be available immediately before saving, as in @Voyta's example below, or it might not, and that affects the solution.Infatuate
Z
24

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
Zombie answered 25/4, 2010 at 18:5 Comment(0)
P
23

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'
Pectase answered 14/5, 2010 at 16:17 Comment(0)
J
18

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)
Joeyjoffre answered 13/6, 2011 at 19:32 Comment(6)
Line 2 should read AWS::S3::S3Object.rename(record.image.path(style), new_file_path, record.image.bucket_name). If you don't include style it will default to :original; bucket_name can be determined from the attachment instance.Gaelic
it's now called 'rename_to' or 'move_to'Zambia
Shouldn't it be record.image.s3_object.move_to instead of AWS::S3::S3Object.move_to ? move_to is an instance method of AWS::S3::S3Object, not a class method.Gingery
After renaming the file in S3 using the above approach and updating the file name is DB, I was not able to get the file using the new url(given by Model.image.url or url got from firefox S3Organiser), I could get the image only after reprocessing it ? So is it necessary to reprocess the image ? or am I missing anything?Pivotal
Confirmed suggestion from @evanrmurphy: had to use record.image.s3_object(style).move_to new_file_path to get it to work.Whangee
@Pivotal when you move an s3 object to a new location you need to copy over the permissions as well: record.image.s3_object(style).move_to new_file_path, acl: record.image.s3_permissions, content_type: record.image.content_typeWhangee
N
6

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
Nephritis answered 13/2, 2013 at 16:51 Comment(4)
How did you get name and name_was?Gingery
The name is an attribute (column in the database) in my User (ActiveRecord::Base). The _was and _changed? are from ActiveModel::Dirty api.rubyonrails.org/classes/ActiveModel/Dirty.htmlNephritis
I changed: extension = File.extname avatar_image_file_name to extension = File.extname(avatar_image.path(style)).downcase because my style extensions are not necessarily the same as the original.Chateau
Thank you! Only change I'd make here is using extension = Paperclip::Interpolations.extension(self.avatar_image, style) ...which comes in handy if you need to calculate other standard Paperclip things like 'basename' or 'attachment' - rubydoc.info/github/thoughtbot/paperclip/Paperclip/…Valenti
D
5

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")

Deservedly answered 1/2, 2012 at 11:32 Comment(2)
great solution - also something to take care of here is the access policy for the s3 object. When you call move_to the access policy defaults to private. There is an :acl option you can pass to the move_to method tho.Zambia
worked for me, very clean example to create a method on the model.Mulholland
M
3

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.

Mickeymicki answered 2/8, 2016 at 9:15 Comment(0)
M
0

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" 
Mama answered 15/12, 2013 at 9:28 Comment(0)
U
0

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 :)

Upthrust answered 20/8, 2016 at 22:11 Comment(0)
S
0

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

Sheepfold answered 8/3, 2017 at 2:40 Comment(1)
will this break existing records?Unlawful

© 2022 - 2024 — McMap. All rights reserved.