Using Rails Paperclip gem to preprocess file before saving - ruby-on-rails

I'm using the Paperclip gem on a Rails 3.2 app where users can upload a specialized file type that contains an image and other information (let's call it a ".elf" file).
I've written all the code needed to extract an image from an elf file via a class called ElfObject, and this works well when tested in the app's console. What I want to do is extract the image and other data from the .elf file before Paperclip saves it to AWS S3, store the other data in the model, and then save just the image object as the Paperclip attachment on S3. Here's the relevant code in the model:
class Photo < ActiveRecord::Base
validates_attachment :attachment,
:presence => true
has_attached_file :attachment,
:storage => :s3,
[[S3 credentials removed]]
before_attachment_post_process :import_photo
attr_accessible :name, :attachment, :properties
def import_photo
if attachment_file_name =~ %r{^*\.elf$}
origfile = attachment.queued_for_write
elf = ElfObject.read(origfile)
properties = elf.get_properties
attachment = elf.image.write "image_for_#{attachment_file_name}.png"
save!
end
end
When I try to upload this way on the app, it raises the error ArgumentError (Invalid argument 'file'. Expected String, got Hash.) from the line elf = ElfObject.read(origfile). If I try something like elf = ElfObject.read(origfile.path), I get NoMethodError (undefined method `path' for #).
Clearly, I'm not fully understanding how to access the file from Paperclip prior to it being posted--any ideas on where I'm going wrong and how to fix it?

It seems like the problem is exactly what the error is saying... that origfile is a Hash, rather than a String.
If that is the case then attachment.queued_for_write returns a Hash, which means you need to find the key that holds the file path string.
origfile = attachment.queued_for_write
p origfile.inspect #print it out so you can figure out what key holds the path
Edited answer try this:
def import_photo
if attachment_file_name =~ %r{^*\.elf$}
origfile = attachment.queued_for_write[:original]
elf = ElfObject.read(origfile.path)
properties = elf.get_properties
attachment = elf.image.write "image_for_#{attachment_file_name}.png"
save!
end
end

Related

Active Storage with Amazon S3 not saving with filename specified but using file key instead

I am having an issue with Active Storage. When I upload to Amazon S3, instead of saving the file inside the bucket with the original name like myfile.zip it is saving it as the key which is associated with that file. So in Cyberduck I am seeing something like this: 5YE1aJQuFYyWNr6BSHxhQ48t. Without any file extension.
I am not sure if there is some setting in Rails 5 or whether it is within Amazon S3 but I have spent hours Googling around to figure out why this is happening.
Any pointers would be really appreciated!
Best regards,
Andrew
This is by design, from ActiveStorage. The file is stored by it's key and without extension on S3, but when the URL is generated by ActiveStorage, the disposition and filename are set.
def url(key, expires_in:, filename:, disposition:, content_type:)
instrument :url, key: key do |payload|
generated_url = object_for(key).presigned_url :get, expires_in: expires_in.to_i,
response_content_disposition: content_disposition_with(type: disposition, filename: filename),
response_content_type: content_type
payload[:url] = generated_url
generated_url
end
end
This is probably done to avoid filename escaping issues that you'd run into otherwise.
You can read more about the Content-Disposition headers here.
You can still reference the name with filename accessor.
class User < ApplicationRecord
has_one_attached :photo
...
end
filename = User.first.photo.filename
In order to have a custom filename on S3, you should update both blob.key and the name on S3.
Active storage uploads images on S3 using blob.key as remote image path and name.
For my usage, I only changed the name for 'images variants' with a Monkey Patch that allows to generate a key terminating by the filename :
config/initializers/active_storate_variant.rb :
ActiveStorage::Variant.class_eval do
def key
"variants/#{blob.key}/#{Digest::SHA256.hexdigest(variation.key)}/#{filename}"
end
end
So when I need the public url for an image variant, I just call image.url('400x400')
This is how my Image model is customized :
class Image < ApplicationRecord
belongs_to :imageable, polymorphic: true
has_one_attached :picture
SIZES = { '400x400' => '400x400' }
def url(size)
return "https://placehold.it/#{size}" unless picture.attached?
'https://my_s3_subdomain.amazonaws.com/' +
picture.variant(resize: SIZES[size]).processed.key
end
...
end
If someone has a better way to do that, I would be happy to see it :)

CarrierWave: detect if an image has already been uploaded

A model is seeded with a remote url for an image, meaning the db entry it is not created in Rails. Then the first time it is fetched from the DB in Rails I want to detect that the image has not been uploaded, assign the remote_seed_url for the image url and save! to trigger the CarrierWave upload. I want to do this only once, obviously, but the code below sometimes uploads the same image more than once.
class Item
include Mongoid::Document
include Sunspot::Mongoid2
field :image, type: String
field :remote_seed_url, type: String #URL for image
mount_uploader :image, ImageUploader
end
Then in a controller
def show
# item = Item.find(params[:id])
# if CarrierWave has already uploded then do nothing
if !#item.image?
#item.image = #item.remote_seed_url # next save will trigger the upload?
#item.save!
end
respond_to do ...
end
The value of #item.image is usually "new" or "old" and #item.image? sometimes returns false when I can see that it has already uploaded the image. This causes the above code to upload multiple times.
Is the controller code correct to get the image uploaded only once?
Is there some way I might have messed things up and caused #item.image? to return false when it should be true? Maybe image aging?
After uploader is mounted on a field, when you call that field, you get an uploader object. If you want to check if there is a file, you should call "file" on that object:
[1] pry(main)> item.image.class
=> ImageUploader
[2] pry(main)> item.image.file.class
=> CarrierWave::SanitizedFile
[3] pry(main)> item.image.file.nil?
=> false

bson_dump error when try to attach a file to mongoid paperclip field of a rails model

I have spent a lot of time trying to attach a file into a mongoid paperclip field. The examples that I have found, always do the same:
my_model_instance = MyModel.new
file = File.open(file_path)
my_model_instance.attachment = file
file.close
my_model_instance.save!
as you can see in How to set a file upload programmatically using Paperclip.
The model that I have done is:
class Logotype
include Mongoid::Document
include Mongoid::Paperclip
has_mongoid_attached_file :logo,
styles: {large: ['640x160'], small: ['300x300>']},
size: { in: 0..3.megabytes },
content_type: [ "image/jpg", "image/png", "image/bmp" ],
storage: :filesystem,
path: ':rails_root/public/resource/resources/:id/:style.:extension',
url: '/logos/resources/:id/:style.:extension'
validates_presence_of :logo
validates_uniqueness_of :logo
end
However I have always the same big error:
NoMethodError: undefined method `__bson_dump__' for /logos/resources/51b5bfaa69fd8a7941000005/original.jpg?1370865578:Paperclip::Attachment
...
Could someone help me? Thanks in advance!
PD: Sorry for my English level.
This is an old question but I just thought I'd share my solution in case someone else finds themselves here.
For me, this was happening in the Moped::BSON::Extensions::Hash module while trying to log the request for a save action. Your stack may be different, but basically the problem occurs when trying to dump data that Mongoid doesn't know how to handle. In my case it was a file upload, ActionDispatch::Http::UploadedFile, but for the OP it's a Paperclip::Attachment.
I solved it by just removing those key/value pairs from the request parameters, e.g.
# recursively remove UploadedFile from a Hash of request parameters
def remove_files(params)
p = proc do |_, v|
v.delete_if(&p) if v.respond_to? :delete_if
v.is_a?(ActionDispatch::Http::UploadedFile)
end
params.delete_if(&p)
end
There might be a better solution that logs the filename or something instead, but I found it easier just to get rid of it.

Parsing an Email Attachment w Paperclip - Possible w/o a tempfile?

My rails 3 app on heroku receives incoming emails. I want to be able to accept attachments but can't get rails to process the attachments without erroring.
The ideal would be to pass the attachment provided by ActionMailer.
message_all = Mail.new(params[:message])
message_all.attachments.each do |a|
attachments.each do |a|
.attachments.build(
:attachment => a
)
end
end
It errors with: NoMethodError (undefined methodrewind' for #)`
Where attachments is a model, with attachment is paperclip
Ideas y? Is there a different way to pass the attachment = a , to paperclip?
I tried another approach, creating a tempfile:
tempfile = File.new("#{Rails.root.to_s}/tmp/#{a.filename}", "w+")
tempfile << a.body
tempfile.puts
attachments.build(
:attachment => File.open(tempfile.path) )
The problem with the tempfile is files without extentions "blah" instead of "blah.png" are breaking paperclip which is why I want to avoid the tempfile. and creating Identity errors, imagemagick doesn't know what they are w/o the ext.
hugely appreciate any advice on this.
The problem with the methods that you are using is that they don't contain all of the necessary information for paperclip like the content type and the original filename. I wrote a blog post about this a while back and how you could fake the format and use an email attachment as a paperclip attachment.
The bottom line was to do this:
file = StringIO.new(attachment)
file.class.class_eval { attr_accessor :original_filename, :content_type }
file.original_filename = attachment.filename
file.content_type = attachment.mime_type

How can I specify a file name for my attachment with Paperclip?

I have byte streams on the server that I'd like to attach to a model class with Paperclip, and I'd like to be able to specify the name that they're saved as on the filesystem. Because I have a lot of these incoming files, I'd prefer to be able to create them as Tempfiles so that I don't have to worry about name collisions and deleting them manually and such. This is what I'm doing:
desired_file_name = 'foo.txt'
Tempfile.open([File.basename(desired_file_name), File.extname(desired_file_name)]) do |tf|
tf.write(content_stream)
tf.rewind
model_obj.paperclip_attachment = tf
end
That pretty much works. The only problem is, my Paperclip attachment ends up with a tempfile name like foo.txt.201029392u-gyh-foh96y.txt. So how can I tell Paperclip what to save my file as? Calling model_obj.paperclip_attachment_file_name = desired_file_name doesn't work. The DB field gets saved as that name, but on the filesystem I still have that tempfile name.
I think you can define your own interpolation interpolation to do that. You can then attach the file normally. For example:
# config/initializers/paperclip.rb
Paperclip.interpolates :custom_filename do |attachment, style|
# Generate your desired file name here.
# The values returned should be able to be regenerated in the future because
# this will also be called to get the attachment path.
# For example, you can use a digest of the file name and updated_at field.
# File name and updated_at will remain the same as long as the file is not
# changed, so this is a safe choice.
SHA1.sha1("#{attachment.original_filename}-#{attachment.updated_at}")
end
# app/models/post.rb
class Post < ActiveRecord::Base
has_attached_file :attachment,
:path => ':rails_root/public/system/:class/:attachment/:id/:style/:custom_filename',
:url => '/system/:class/:attachment/:id/:style/:custom_filename'
end
Note that this only changes the file name in the file system. model.attachment_file_name or model.attachment.original_filename will still keep the original file name.

Resources