Carrierwave unique filename not being set - ruby-on-rails

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

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

Upload a Tempfile to Carrierwave in Rails

I am trying to create a Tempfile using some user-inputted information in one of my models:
after_create :form_to_csv
def form_to_csv
temp_file = Tempfile.new('test_temp.csv', encoding: 'utf-8')
begin
self.input_data.split('\n').each do |line|
temp_file.write line
end
temp_file.rewind
self.build_input_data_file(file: Rails.root.join("temp_file.path}").open)
ensure
temp_file.close
temp_file.unlink
end
end
The form data is stored in the input_data field as text. I get to write my data to the Tempfile successfully but attaching it to the model is cumbersome - I keep trying to attach nil to my model. My main problem is that the file I generate has a path like:
/var/folders/jw/7pjlw_212qd3s4ddfj1zvnpr0000gn/T/test_temp.csv20170502-78762-1eh23ml
and it won't work with Rails.root.join and with the attachment method propose in CarrierWave docs here (Upload from a local file).
My mistake was that I tried to assign a .csv extension within the filename parameter of the Tempfile object. Instead I needed to pass an array like this:
def form_to_csv
temp_file = Tempfile.new(['test_temp', '.csv'], encoding: 'utf-8')
begin
self.input_data.split('\n').each do |line|
temp_file.write line
end
temp_file.rewind
self.build_input_data_file(file: Rails.root.join(temp_file.path).open)
ensure
temp_file.close
temp_file.unlink
end
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

Renaming uploaded files with Carrierwave

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.

How do you create a configurable Ruby on Rails plugin?

i am trying to create my first rails plugin, and i want it to be configurable, that is to say, i want to be able to set a variable in the environment.rb file or something.
UPDATE: i'm trying to do something like what's done here: http://soakedandsoaped.com/articles/read/exception-notifier-ruby-on-rails-plugin. i have tried mimicking their code, but i can't get it working.
i have the plugin working with the value hard-coded, but everything i have tried so far for making it configurable hasn't worked.
Here's some of the code:
#vendor/plugin/markup/lib/markup_helper.rb
module MarkupHelper
def stylesheet_cache_link_tag(*sources)
cache = assests_cache_dir ? assests_cache_dir : ""
options = sources.extract_options!.stringify_keys
cached_name = options.delete("cached_name")
stylesheet_link_tag(sources, :cache=> File.join(cache, cached_name))
end
def javascript_cache_include_tag(*sources)
cache = assests_cache_dir ? assests_cache_dir : ""
options = sources.extract_options!.stringify_keys
cached_name = options.delete("cached_name")
javascript_include_tag(sources, :cache=> File.join(cache, cached_name))
end
end
#something like the following in config/environment.rb or probably config/environments/production.rb
MarkupConfig.assests_cache_dir = "cache"
i want assests_cache_dir to default to "cache" but be able to set in an environment config file. i have googled a long time on this, and can't find anything discussing this. How can i accomplish this?
module MarkupHelper
mattr_accessor :assets_cache_dir
self.assets_cache_dir = "cache"
def assets_cache_dir
MarkupHelper.assets_cache_dir
end
end
Then in environment.rb (or development.rb/test.rb/production.rb if you want different values for each environment):
MarkupHelper.assets_cache_dir = "my-value"
Although the approach used by tomafro is quite easy to use, another approach is to use a database.yml-style configuration file that can be split according to environments:
module MyPlugin
class Configuration
# == Constants ==========================================================
CONFIG_FILES = [
"#{RAILS_ROOT}/config/myplugin.yml",
"#{RAILS_ROOT}/config/myplugin.yaml"
].freeze
DEFAULT_CONFIGURATION = {
:url => DEFAULT_HOSTNAME
}.freeze
# == Class Methods ======================================================
# :nodoc:
def self.config_file_found
CONFIG_FILES.find do |path|
File.exist?(path)
end
end
# Returns the default path to the configuration file
def self.default_path
config_file_found or CONFIG_FILES.first
end
# == Instance Methods ===================================================
# Creates a new MyPlugin::Configuration instance by reading from the
# configuration file.
# +env+ The Rails environment to load
def initialize(env)
config_file = self.class.config_file_found
#env_config = DEFAULT_CONFIGURATION
if (#config = (config_file and YAML.load(File.open(config_file))))
[ #config['defaults'], #config[env] ].each do |options|
if (options)
#env_config = #env_config.merge(options.symbolize_keys)
end
end
end
end
# Will return +true+ if a configuration file was found and loaded, or
# +false+ otherwise.
def exists?
#env_config != DEFAULT_CONFIGURATION
end
# Returns a particular configuration option.
def [](key)
#env_config[key.to_sym]
end
end
def self.config
#config ||= Configuration.new(Rails.env)
end
end
You would use this as:
settting = MyPlugin.config[:param_name]
You can also write utility methods to fetch particular values, or use OpenStruct instead of a configuration Hash. This is posted merely as an example of another design pattern.

Resources