I have an app that must accept almost every file type except those known to be malitious (ie exe, dll, bat, etc). I am using Paperclip and am wondering if there is a way to do this. Following the commit on github, https://github.com/thoughtbot/paperclip/commit/020625921adae884534608d76c11f65692e4bbec, it looks like it may be possible. But I am unsure.
UPDATE: I could not find a Paperclip way of doing things, however I did add this custom validation:
def extension_not_blacklisted?
#An attempt to make a blacklist command when saving...
forbiden_types = Array.new()
forbiden_types << "jpg" << "exe" <<"dll"
path_array = attachment.to_s.split(".")
extension = path_array.pop
extension_with_extras = extension.to_s.split("?")
extension = extension_with_extras[0]
forbiden_types.each do |f|
if f == extension
errors.add(:attachment,'FORBIDEN FILE EXTENSION: ' + extension)
end
end
Your custom validation method is probably the only way. At least for now, Paperclip can validate only content types, with something like:
validates_attachment_content_type :attachment, :content_type => ['image/png', 'application/pdf'], :message => 'should be a valid type'
and it validates inclusion, not exclusion.
You can use a regular expression that uses negative lookahead:
validates_attachment_content_type :attachment, :content_type => /\/(?!(php|pl|exe|pm|cfm|asp)$)/
Use a before_post_process filter and return false if the extension is in your blacklist - returning false will prevent the rest of the processing chain from executing.
See the bottom of this page for an example on checking for a valid file size.
https://github.com/thoughtbot/paperclip/wiki/Thumbnail-Generation
Create a custom validation.
BANNED_FILE_EXTENSIONS = [
".exe",
".js",
".sh",
".shar"
].freeze
validate :file_extension_is_allowed
def file_extension_is_allowed
errors.add( :attachment, "is not an allowed file extension" ) if BANNED_FILE_EXTENSIONS.include?( File.extname( self.attachment_file_name ) )
end
Related
I'm using Paperclip in my project but some of my users are complaining that it's incorrectly rotating some images.
For some reasons I can't even imagine I figured it out that some files are with wrong exif orientation attributes. I was looking and I saw that paperclip calls ImageMagick by default using -auto-orient. I saw that the Thumbnail processor has an option to turn auto-orient on or off.
But I couldn't find a way to pass this to the Processor.
This is the code I have:
has_attached_file :photo,
styles: { :square => "400x400#" }
Does anyone now how to do that?
Thanks!
In the end I created a new processor which extends from the paperclip default Thumbnail processor to send the correct options.
class WithouAutoOrientProcessor < Paperclip::Thumbnail
def initialize(file, options = {}, attachment = nil)
options[:auto_orient] = false
super
end
end
And in the model I added
has_attached_file :photo,
styles: { :square => "400x400#" },
processors: [:WithouAutoOrientProcessor]
Although it is a valid option to add your own processor, this is how you pass the option to the processor:
In your styles hash replace your dimension strings with another hash
Put your old dimensions in the key geometry into this hash
The other key/value pairs are the options passed to the processor
You can of course pass auto_orient: false, too
Applying this to your model's code:
has_attached_file :photo,
styles: { square: { geometry: "400x400#", auto_orient: false } }
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.
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
How can I prevent users from adding new tags which don't already exist in the tags db?
I want them to be able to add any tags that already exist to another model which they can fully edit, but not be able to create new tags if they don't yet exist?
I'm using declarative_auth so some users with permissions should be create to add whatever tags they want.
user.rb
acts_as_tagger
post.rb
acts_as_taggable_on :features
https://github.com/mbleigh/acts-as-taggable-on
UPDATE:
This seems to do it except I can't get the error message variable to work:
validates :feature_list, :inclusion => {
:in => SomeModel.tag_counts_on(:features).map(&:name),
:message => "does not include {s}" }
I havn't used acts_as_taggable, but can you pass normal rails validations?
# LIKE is used for cross-database case-insensitivity
validates_inclusion_of :name => lambda { find(:all, :conditions => ["name LIKE ?", name]) }
Could probably be more robust and rails validation like but this works:
validate :valid_feature_tag
def valid_feature_tag
invalid_tags = false
feature_list.each do |tag|
list = SomeModel.tag_counts_on(:features).map(&:name)
unless list.include?(tag)
invalid_tags = true
end
end
unless invalid_tags == false
errors.add(:feature_list, 'cannot contain new tags, please suggest new tags to us')
return false
else
return true
end
end
Here's an efficient and clean way to enforce allowed tags:
validate :must_have_valid_tags
def must_have_valid_tags
valid_tags = ActsAsTaggableOn::Tag.select('LOWER(name) name').where(name: tag_list).map(&:name)
invalid_tags = tag_list - valid_tags
if invalid_tags.any?
errors.add(:tag_list, "contains unknown tags: [#{invalid_tags.join(', ')}]")
end
end
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