Renaming uploaded files with Carrierwave - ruby-on-rails

I'm using Carrierwave to upload files, and I have it working.
My issue is attempting to change the name of the uploaded file.
In the generated uploader.rb there is a method I think I should be using
def filename
"something.jpg" if original_filename
basename = "what"+orginal_filename if original_filename, works
basename = (0...8).map{65.+(rand(25)).chr}.join if original_filename # will create a random name for each version, e.g. the orginal, the thumb, and the filename in the db, useless
end
I can't seem to access items like 'extension' or 'content_type' in sanitized_file.rb, so this is a bit beyond my current skill level right now.
Any suggestions or exercises for doing this, i.e. generate filename for an uploaded file that works as well as the carrierwave default (do nothing, but does carry on to each version)? Seems like it should be simple enough but I've stumbled over this.

Well, another problem with your random filename generator is that it's possible to have collisions isn't it? You could possibly generate a filename that was already generated.
One way to go about it would be to somehow generate a hash based on unique properties of the image, like file path. An example, from the carrierwave group:
def filename
if original_filename
#name ||= Digest::MD5.hexdigest(File.dirname(current_path))
"#{#name}.#{file.extension}"
end
end
This will create an MD5 hash based on the current path and then append the original file's extension to it.
Edit: The carrierwave wiki added an entry with a few methods on how to create random and unique filenames for all versioned files.

To have a real unique filename (not almost unique) I recommend to use the uuid gem.
in Gemfile add:
gem 'uuid'
in file_uploader.rb:
def filename
if original_filename
if model && model.read_attribute(mounted_as).present?
model.read_attribute(mounted_as)
else
#name ||= "#{mounted_as}-#{uuid}.#{file.extension}"
end
end
end
protected
def uuid
UUID.state_file = false
uuid = UUID.new
uuid.generate
end

From the Google Group:
def filename
#name ||= "#{secure_token}.#{file.extension}" if original_filename
end
private
def secure_token
ivar = "##{mounted_as}_secure_token"
token = model.instance_variable_get(ivar)
token ||= model.instance_variable_set(ivar, ActiveSupport::SecureRandom.hex(4))
end

To just make the record.id prefix the filename you can do the following:
class MyUploader < CarrierWave::Uploader::Base
storage :file
def store_dir
model.class.to_s.underscore.pluralize
end
def filename
model.id ? "#{model.id}-#{original_filename}" : original_filename
end
def url
"/#{store_dir}/#{model.id}-#{model.file_before_type_cast}"
end
end

The other solution looks good, but how I did it then was to have a hook that created a random string for a new name on instance creation, then:
def filename
"#{model.randomstring}.#{model.image.file.extension}"
end
in the uploader.
That worked, putting the random name generation as part of the model, then having carrierwave use that.
I am curious which is faster, more effective, reasonable, sound, etc.

Here is the solution, how to change the name of the file, if store_dir already contains the file with the exact name:
if File.exists?(Rails.root.join("documents/" + "#{file.filename}")) && !path.to_s.eql?(Rails.root.join("documents/" + original_filename).to_s)
#name ||= File.basename(original_filename, '.*') + Digest::MD5.hexdigest(File.dirname(current_path)).from(25)
"#{#name}.#{file.extension}"
else
"#{original_filename}"
end
Note: Rails.root.join("documents/") is defined as my store_dir.
Hope it helps someone.

Related

Rails: carrierwave file name updating on every updates with model

I'm trying to write new file name on uploaded file with carrierwave and I have used two methods on app/uploaders/media_uploader.rb
def filename
"#{secure_token}.#{file.extension}"
end
protected
def secure_token
var = :"##{mounted_as}_secure_token"
model.instance_variable_get(var) or model.instance_variable_set(var, SecureRandom.uuid)
end
#model/user.rb
mount_uploader :avatar, MediaUploader
This working perfectly when uploading new or update file, but the problem is when I work with other attributes like updating my name, email, or bio using a different form then file name updating continuously then file are missing after update other attributes.
What can I do now?
You can resave it by calling file.cache_stored_file!, I found this here. I had my uploader path depending on updated_at so I did the following in my model:
My Uploader
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}/#{model.updated_at.to_i}"
end
My Model
before_update :update_file_path
...
def update_file_path
my_uploader.cache_stored_file! if changed?
end

Carrierwave unique filename not being set

I looked at the carrierwave wiki on github, and used the method they describe to generate unique file names:
def filename
#filename ||= "#{secure_token}.#{file.extension}" if original_filename.present?
end
private
def secure_token
var = :"##{mounted_as}_secure_token"
random_token = Digest::SHA2.hexadigest("#{Time.now.utc}--#{model.id.to_s}").first(20)
model.instance_variable_get(var) or model.instance_variable_set(var, random_token)
end
(Using hashes though)
The problem I am having is that the filename is still being set to the original file's name. It's as if the filename mehod is being ignored. Not really sure what is going on. I reset the server and everything, still getting the original filename on the uploaded file and thumbnail version.
I'm not sure why the docs used ||= operator in filename method, but this way the unique file name will not be set unless #filename is nil, which doesn't seem to be the usual case. Using = instead of ||= (or not using assignment at all) seems to solve the issue.
def filename
#filename = "#{secure_token}.#{file.extension}" if original_filename.present?
# The following line works too
#"#{secure_token}.#{file.extension}" if original_filename.present?
end

Carrierwave precalculated md5 checksum of a file as a filename

Using carrierwave uploader for images, trying to provide uniqueness of uploaded images using md5 checksum as filename
looks like I'm doing something wrong
model is defined like:
class Image < ActiveRecord::Base
attr_accessible :description, :img
mount_uploader :img, ImageUploader
My uploader code is as following:
class ImageUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
storage :file
def store_dir
"images/#{filename[0,2]}"
end
def md5
#md5 ||= ::Digest::MD5.file(current_path).hexdigest
end
def filename
#name ||= "#{md5}#{::File.extname(current_path)}" if super
end
first of all, I suspect this approach inflicts calculation of checksum each time image entry is queried to display
secondly, after image entry is saved, every other of img.original_filename img.filename img.path img.current_path seem to be undefined with following error:
You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.[]
app/uploaders/image_uploader.rb:17:in `store_dir'
carrierwave (0.5.7) lib/carrierwave/uploader/store.rb:43:in `store_path'
carrierwave (0.5.7) lib/carrierwave/storage/file.rb:41:in `retrieve!'
carrierwave (0.5.7) lib/carrierwave/uploader/store.rb:95:in `block in retrieve_from_store!'
carrierwave (0.5.7) lib/carrierwave/uploader/callbacks.rb:17:in `with_callbacks'
carrierwave (0.5.7) lib/carrierwave/uploader/store.rb:94:in `retrieve_from_store!'
carrierwave (0.5.7) lib/carrierwave/mount.rb:311:in `uploader'
any kind of help or tip is appreciated
UPD:
changed uploader this way:
def store_dir
"images/#{model.img_identifier[0,2]}"
end
def filename
#name ||= "#{md5}#{::File.extname(current_path)}"
end
protected
def md5
var = :"##{mounted_as}_md5"
model.instance_variable_get(var) or model.instance_variable_set(var, ::Digest::MD5.file(current_path).hexdigest)
end
current_path seems to refer to full path of form-submitted tempfile, thus being valid for extension extraction and digest calculation
img_identifier stands for persisting resulting filename and thus goes valid for prefix extraction for our store_dir
still not sure if any caveat is induced with this approach
also still not convinced about the way file uniqueness validation should be performed
UPD:
I've added this before_validation callback in my model class:
validates_uniqueness_of :checksum
before_validation :assign_checksum
def assign_checksum
self.checksum = img.md5 if img.present? and img_changed?
end
where checksum is a separate string field in my model's db table
it is quite redundant as it duplicates the img field in general, but I still can't figure out the way to validate uniqueness of img itself.
UPD:
Moved away from db redundancy this way. In my model:
validate :img_uniqueness
def img_uniqueness
errors.add :img, "Image already exists in database" if Image.where(:img => self.img.filename).first
end
now there's no need in checksum field
This could help:
How to: Use file`s MD5 as filename
https://github.com/carrierwaveuploader/carrierwave/wiki/How-to:-Use-file%60s-MD5-as-filename
And maybe
How-to: Use file's digest (e.g. MD5, SHA-1) as file path
https://github.com/carrierwaveuploader/carrierwave/wiki/How-to:-Use-file's-digest-(e.g.-MD5,-SHA-1)-as-file-path
Add a md5hash field to your model, then add the following code to your Model:
before_validation :compute_hash
validates_uniqueness_of :md5hash, :on => :create
def compute_hash
self.md5hash = Digest::MD5.hexdigest(self.file.read)
end
That should do the trick.
1. When you define store_dir .. filename seems to be NIL!!
That seems to be the immediate error -- try a puts statement to print out what filename is set to..
If the filename is NIL, you'll see the error you're seeing:
You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.[]
app/uploaders/image_uploader.rb:17:in `store_dir'
NOTE:
you're overriding both filename and store_dir ... and using filename inside the definition of store_dir...
There could be a "chicken and the egg" type of problem here.. better check on that
store_dir should just be a directory, e.g. /somewhere/on/your/disk/images
filename should just be a filename without path, e.g. 24371592d9ea16625854ed68ac4b5846 , or 24371592d9ea16625854ed68ac4b5846.jpg
e.g. check how those two are used in the code in store.rb (at end below)
Question:
using filename[0,2] - you're using directories with 2-letter prefix of the MD5-sum to store the images?
2. Side-note: What is current_path?? Seems a misnomer. Should be a path+filename, not just a path
3. Check the second code snippet (below) from store.rb
Seems like you set store_dir to a relative directory -- that's very fragile and error prone.. it might be a better idea to set it to an absolute path (starting with '/')
4. Try setting filename and store_dir to constants for debugging
Just as a sanity check, does it work when you do this:
def store_dir
'/tmp/' # from the source code, it looks like this needs to start with '/' !!
end
def filename
'my_uploaded_file'
end
that should work before setting filename to MD5 sum..
From the source code: (0.5.7)
lib/carrierwave/storage/file.rb
lib/carrierwave/uploader/store.rb
You can override CarrierWave::Uploader::Store#filename to point to a filename of your choice (see source code):
Overriding it in CarrierWave::Uploader::Base should work, as 'Store' is included in 'Base' ; this overrides the default filename.
Fromstore.rb
# Override this in your Uploader to change the filename.
#
# Be careful using record ids as filenames. If the filename is stored in the database
# the record id will be nil when the filename is set. Don't use record ids unless you
# understand this limitation.
#
# Do not use the version_name in the filename, as it will prevent versions from being
# loaded correctly.
#
# === Returns
#
# [String] a filename
#
def filename
#filename
end
You could also check for the cached filename, compute the MD5-sum (or better SHA1 sum) on it, and then use the result to name the file.
Fromstore.rb
# Calculates the path where the file should be stored. If +for_file+ is given, it will be
# used as the filename, otherwise +CarrierWave::Uploader#filename+ is assumed.
#
# === Parameters
#
# [for_file (String)] name of the file <optional>
#
# === Returns
#
# [String] the store path
#
def store_path(for_file=filename) # DEFAULT argument is filename - you don't need to override both
File.join([store_dir, full_filename(for_file)].compact)
end
File Equality:
files_equal?( filename1, filename2 )
return true if File.size(filename1) == File.size(filename2)
# return MD5(filename1) == MD5(filename2)
# as we assume that the filename == MD5 + some suffix :
return File.basename(filename1) == File.basename(filename2) # if names == MD5 are the same
end
I would consider the wiki way for file name uniquess,
https://github.com/jnicklas/carrierwave/wiki/How-to%3A-Create-random-and-unique-filenames-for-all-versioned-files

Displaying a Carrierwave filename in the view

I am trying to display the filename of a Carrierwave attachment in a Rails erb template. The following does not work:
<%= #page.form.filename %>
This seems in line with the documentation. Is some additional step needed?
My page model looks like this:
class Page < ActiveRecord::Base
mount_uploader :form, FormUploader
end
The form uploader looks like this:
class FormUploader < CarrierWave::Uploader::Base
storage :file
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
def extension_white_list
%w(pdf)
end
end
I have been able to get the filename via the file internal parameter:
<%= #page.form.file.filename %>
The documentation you're looking at is the sanitized file, it's what it uses for actually storing a file. The part you're looking for is FormUploader, which is an Uploader, and part of http://rubydoc.info/gems/carrierwave/0.5.2/CarrierWave/Uploader
If you want to get the file name, you could either read it from the database column directly, or use File.basename(#page.form.path) to extract it easily.
The Carrierwave docs might be a bit off, but recommended way seems to be:
#page.form.file.identifier
#adamonduty's solution is great. Another solution I used before, just create a method on the model:
def name
file.path.split("/").last
end
You're right #epylinkn. Documentation points towards using:
#page.form.file.identifier
But when I use that, I always get nil (just as #Cheng commented).
I then inspected my objects methods (#page.form.file.methods.inspect), and found the following to work:
#page.form.file_identifier
In your model's associated uploader class, define a filename method.
def filename
File.basename(path)
end
You can then call
model_instance.file.filename
Works as of CarrierWave 1.1.0. This is a succinct restatement/amalgamation of kikito and Chris Alley's responses above.
If you're using ActiveRecord, you can directly access the field named form in two ways:
def my_method
self[:form]
end
or
def my_method
form_before_type_cast
end
The second method is read-only.
CarrierWave::SanitizedFile has a private original_filename method containing the filename of the uploaded file. (docs: http://rdoc.info/github/jnicklas/carrierwave/master/CarrierWave/SanitizedFile:original_filename)
After reading through this thread from the CarrierWave mailing list, none seemed to fit my needs. With something like
class Upload < ActiveRecord::Base
mount_uploader :file, FileUploader
# ...
I heavily modify the :file column value from the original filename. Due to this I decided to track the original filename in a separate column from the one bound to CarrierWave. In my FileUploader I simply added a reader that wraps the private original_filename method:
def original_file
original_filename
end
I then added a before_create event to the Upload class (my Upload records are never modified, so a before_create is acceptable for my needs)
before_create do
self.original_file = self.file.original_file
end
I'm assuming you've got models like this?
class Page
mount_uploader :form, FormUploader
end
If so you should be able to call:
#page.form.url
#page.form.filename
Are you sure you've uploaded/attached the file correctly? What do you see when you inspect #page.form? Remember, the attachment will not be saved until you've fully processed the upload.
This is my solution:
before_save :update_file_attributes
def update_file_attributes
if file.present? && file_changed?
self.content_type = file.file.content_type
self.file_size = file.file.size
self.file_name = read_attribute(:file)
end
end

Paperclip save attachment

Is there a better way to save some string as an attachment via Paperlip as making a tmp file, putting the string into it, opening it again and saving it as an attachment ?
Like this :
def save_string data
tmp_file = "/some/path"
File.open(tmp_file,'w') do |f|
f.write(data)
end
File.open(tmp_file,'r') do |f|
ceneo_xml = f
save!
end
end
There is actually a better way - you can wrap it to StringIO which Paperclip enhances and you will get a pseudo uploaded file in no time. You can customize it by defining instance methods or directly create a subclass of StringIO like this
class InvoiceAttachment < StringIO
def initialize(invoice, content)
#invoice = invoice
super(content)
end
def original_filename
from = #invoice.from
to = #invoice.to
date = #invoice.created_at.strftime('%B-%Y').downcase
"invoice_#{date}_from_#{from}_to_#{to}.pdf"
end
def content_type
'application/pdf'
end
end
Enjoy!
Paperclip stores files alongside your models -- this is what it has been written to do, so I think the short answer is "no".
If you look in attachment.rb in the Paperclip source you'll see a method called def assign uploaded_file. If you look at the implementation of this method you can see that it expects the uploaded file object to have a certain methods defined on it.
You could create your own class which followed the same interface as Paperclip expects, but to be honest your solution of saving a file and assigning that to Paperclip is probably the easiest approach.

Resources