Rails Paperclip copy set of images from model A to model B - ruby-on-rails

Currently I have a model A (ItemRequest) that has many images.
Once this item request has been approved, it creates a model B (Item) that also has many images.
I'm using paperclip to handle image upload to S3, and what I would like is that after the approve process has been completed, copy the images from model A to model B, duplicating them.
Following some similar questions (but none using models with many images) I've written the following in my ItemRequest model approve method.
if images.length > 0
item.images = images.map { |img| img.dup }
end
And this apparently creates a new set of Image models, but when trying to access the url, it does not open any image and S3 returns 403.
I've checked and no image is created in S3. I can access other images so the S3 is properly configured.
Any ideas on how to solve this?
def resolve! user_id
if status != RESOLVED
ActiveRecord::Base.transaction do
self.resolved_by_id = user_id
self.resolved_date = DateTime.now
self.status = RESOLVED
save!
catalog_item = Item.create(
name: item_name,
oem_name: oem_name,
description: description,
category_id: category_id,
make: make,
part_number: part_number,
uom: uom,
cost_in_cents: cost_in_cents,
weight_in_grams: weight_in_grams
)
if images.length > 0
catalog_item.images = images.map { |img| img.dup }
catalog_item.save
end
end
end
true
end
Something is worth mentioning that doesn't simplify things, is that my Image model is polymorphic.
I've managed to write a solution but not happy with it yet.
Add this to the Image model
def file_remote_url=(url_value)
self.file = URI.parse url_value
#file_remote_url = url_value
end
And then
catalog_item.images = images.map { |img|
new_image = Image.new()
new_image.file_remote_url = img.url
new_image
}
catalog_item.save
Also, this solution does not work locally if uploading to the dev machine.

After a few days of trying different approaches, here is what solved the issue for me:
catalog_item.images = images.map do |img|
new_image = Image.new
new_image.imageable_type = 'CatalogItem'
new_image.imageable_id = catalog_item.id
new_image.file = img.file
new_image
end
This will create a copy of the image instead of changing the reference only. As far as I understood, if you're using S3 and paperclip, it won't simply copy the image using the S3 api but rather make a copy and upload it again, which might be a bit inefficient, but that is a whole other unrelated issue.

Related

Reading text from a PDF works in Rails console but not in Rails application

I have a simple one-page searchable PDF that is uploaded to a Rails 6 application model (Car) using Active Storage. I can extract the text from the PDF using the 'tempfile' and 'pdf-reader' gems in the Rails console:
> #car.creport.attached?
=> true
> f = Tempfile.new(['file', '.pdf'])
> f.binmode
> f.write(#car.creport.blob.download)
> r = PDF::Reader.new(f.path.to_s)
> r.pages[1].text
=> "Welcome to the ABC Car Report for January 16, 20...
But, if I try the same thing in the create method of my cars_controller.rb, it doesn't work:
# cars_controller.rb
...
def create
#car = Car.new(car_params)
#car.filetext = ""
f = Tempfile.new(['file', '.pdf'])
f.binmode
f.write(#car.creport.blob.download)
r = PDF::Reader.new(f.path.to_s)
#car.filetext = r.pages[1].text
...
end
When I run the Rails application I can create a new Car and select a PDF file to attach. But when I click 'Submit' I get a FileNotFoundError in cars_controller.rb at the f.write() line.
My gut instinct is that the controller is trying to read the blob in order to write it to the temp file too soon (i.e., before the blob has even been written). I tried inserting a sleep(2) to give it time, but I get the same FileNotFoundError.
Any ideas?
Thank you!
I don't get why you're jumping through so many hoops. And using .download without a block loads the entire file into memory (yikes). If #car.creport is an ActiveStorage attachment you can just use the open method instead:
#car.creport.blob.open do |file|
file.binmode
r = PDF::Reader.new(file) # just pass the IO object
#car.filetext = r.pages[1].text
end if #car.creport
This steams the file to disk instead (as a tempfile).
If you're just taking file input via a plain old file input you will get a ActionDispatch::Http::UploadedFile in the parameters that also is extemely easy to open:
params[:file].open do |file|
file.binmode
r = PDF::Reader.new(file) # just pass the IO object
#car.filetext = r.pages[1].text
end if params[:file].respond_to?(:open)
The difference looks like it's with your #car variable.
In the console you have a blob attached (#car.creport.attached? => true). In your controller, you're initializing a new instance of the Car class, so unless you have some initialization going on that attaches something in the background, that will be nil.
Why that would return a 'file not found' error I'm not sure, but from what I can see that's the only difference between code samples. You're trying to write #car.creport.blob.download, which is present on #car in console, but nil in your controller.

Uploading Multiple Images to Imgur API with Ruby is Slow

So I have a form that can submit multiple images at once. I also wrote a wrapper for the Imgur API in Ruby. My problem is that since this happens completely synchronously, it takes forever and times out for even 10 images. I'm wondering if there is a better way that would be able to handle more images.
All I can think of is asynchronously submitting forms with one image each, but I'm not sure if that's a good idea or if it will just hold up other requests.
class MissionsController < ApplicationController
def add_images
image_ids = params[:images].collect do |image|
Image.with_imgur(
title: "#{#mission.trip.name} - #{current_user.username}",
image: image,
album_id: #mission.album.imgur_id,
user_id: current_user.id,
trip_id: #mission.trip_id
).imgur_id
end
end
end
class Image < ActiveRecord::Base
def self.with_imgur(options)
trip_id = options.delete(:trip_id)
user_id = options.delete(:user_id)
image = Imgur::Image.create(options)
create(
imgur_id: image["id"],
link: image["link"],
trip_id: trip_id,
user_id: user_id
)
end
end
https://github.com/tomprats/toms-missions/blob/master/app/models/imgur.rb#L116
class Imgur::Image < Base
def self.create(options)
url = "https://api.imgur.com/3/image"
params = {
image: options[:image],
album: options[:album_id],
type: options[:type] || "file", # "file" || "base64" || "URL"
title: options[:title],
description: options[:description]
}
api_post(url, params).body["data"]
end
end
I'd take a look at Typhoeus https://github.com/typhoeus/typhoeus - I've used this in the past to handle uploads etc to Amazon Glacier. I found that it could be much faster - Typhoeus gives you a future object and will handle the upload of you in the background without blocking the application.
I hope this helps!

How to save images to respective id using paperclip?

I am showing image preview after uploading images.My view contains table of persons with name ,email id,file upload and image preview.And on submit button it will save all images to respective one.I am stuck how to save hash of each image to respective one through paperclip.
I successfully get hash with each person id as key.
I have two controllers Person and Image and this preview and save functionality work on new and create of image.
Code for Image controller:
def new
#people = Person.where("avatar_file_name is ?",nil)
end
def create
#people = Person.where("avatar_file_name is ?",nil)
p=#people.coun
u=Array.new
#people.each do |e|
u.push(e.id)
end
h=Hash.new
h=params
u.each do |x|
#newimage=h["#{x}"]
######## Here in #newimage we get hash of image
end`
end
end
What I have to write after getting hash of each images?
Anyone needs some more description they can ask..........
Thanks in advance
got solution......very easy one
u.each do |x|
#person = Person.find(x)
#person.update_attribute(:avatar,h["#{x}"])
end
It's working fine

Duplicate a rails object with associations and paperclip attachments

I have an object with several associations. Some of these associated objects have paperclip-attachments stored at S3. If I duplicate the object and the associations it works fine but the attachments are not duplicated.
This here works without getting the images:
copy_salon = #salon.dup
copy_salon.about_us_versions = #salon.about_us_versions.collect{|about_us| about_us.dup}
I tried to get the image link like this:
copy_salon = #salon.dup
copy_salon.about_us_versions = #salon.about_us_versions.collect{|about_us|
about_us_dup = about_us.dup
if about_us.about_us_image then about_us_dup.about_us_image = about_us.about_us_image end
if about_us.team_image then about_us_dup.team_image = about_us.team_image end
about_us_dup
}
But then I am getting the error 'can't convert nil into String', probably because not all images are set.
Got it, not elegant but working. I had hoped dup would duplicate my object with ALL associations and attachments. Isn't there any gem for that?
copy_salon = #salon.dup
copy_salon.about_us_versions = #salon.about_us_versions.collect{|about_us|
about_us_dup = about_us.dup
unless about_us.about_us_image.url == "/about_us_images/original/missing.png" then about_us_dup.about_us_image = about_us.about_us_image end
unless about_us.team_image.url == "/team_images/original/missing.png" then about_us_dup.team_image = about_us.team_image end
about_us_dup
}
I got it simpler by overriding dup, at least for Paperclip attachments:
def dup
duplicate = super
# attachment_definitions is defined if model has paperclip attachments
return duplicate unless self.class.respond_to?(:attachment_definitions)
duplicate.tap do |d|
self.class.attachment_definitions.keys.each do |name|
d.send("#{name}=", send(name)) if send(name).exists?
end
end
end
It can be defined like this in ApplicationRecord so every model benefits from it.

best strategy to moderate uploaded images?

I'm trying to finalize my image moderation code,
I have just a simple assets model with a column moderated.
Images will not be shown unless the moderated flag is set to true ( boolean )
In addition to this I have the idea to store a is_moderated (integer) inside User model and store a value there like
0 = not moderated
1 = moderated and inappropriate image
2 = moderated and correct image
Then in application controller I do something like, in before filter:
def is_moderated
if user_signed_in?
#moderate = Moderate.find(current_user) rescue nil
if #user.is_moderated == "2"
render :template => "shared/moderated_bad_images"
end
end
end
end
How about you are only allowed to upload 1 image initially.
Then after that image is deemed appropriate you can add more.

Resources