How do I handle an external API callback with Ruby on Rails? - ruby-on-rails

I'm in need of some guidance. I'm using the 6px.io API to resize images. The conversion can take a couple of seconds and 6px sends me a post callback when the image has been sent/stored in our S3 bucket. After the callback, when the image is resized and saved we'd like to download the file to the user's browser from our S3 bucket.
If the resized image is already in our S3 bucket and does not need to be processed by 6px we use send_data to download the file to the user. Unfortunately I can't use send_data on the callback controller action to initiate download of the file to the user. How is this done in Rails?
User story:
Click medium resize of image menu item of dropdown.
Rails controller action builds JSON with post callback url and sends to 6px.
6px returns JSON with input and output information plus the status (complete/fail).
File starts to download to user's computer/browser ***This is what I need help with.
Example code (this is for work, can't post the real code):
class FakeExampleController < ApplicationController
def convert_image
# code ommitted
#fake_example = FakeExample.find_by(:id)
image_conversion(params)
end
def callback
# width, height, type variable code ommitted
if params['status'] == 'complete'
converted_file = FakeConvertedImage.create!(filename: #new_file_name, attachment_id: #fake_example.id, format: type, width: width, height: height)
send_converted_file( :url => "OUR_S3_BUCKET", :filename => "#{converted_file.filename}", :type => "#{type}", :disposition => 'attachment' ) #This does not work to download file to user's browser.
else
raise
"Image Conversion Failed"
end
end
def image_conversion(params)
callback_url = fake_example_url.gsub(/localhost:3000/, '********.ngrok.com')
image_converter = ImageConverter.new(params) #This is a wrapper I made for the 6px-ruby gem
image_converter
.convert_type(#fakeexample.mime_type_for_6px(params[:mimetype])
.resize_images
.save("OUR_S3_BUCKET_URL", callback_url)
end
def send_converted_file(opts={})
other_opts = opts.select{ |opt,v| /(filename|type|disposition)/ === opt }
response = { url: opts.fetch(:url), :opts => other_opts }
send_data( open(URI.encode(response[:url].to_s)).read, response[:opts] )
end
end
I don't get/have any errors. I just need advice on how to download the file saved in our S3 to the user.

Related

Post image from Rails

I've got a Base64 encoded image coming in to my application. I want to re-post that image somewhere else, but it's setting the content-type to multipart/form-data at the destination. How do I upload this image?
file_name = permitted_params[:file_name]
file_contents = permitted_params[:file_contents]
file = Tempfile.new( file_name )
file.binmode
file.write( Base64.decode64( file_contents ) )
file.rewind()
raw_response = RestClient.put(
url,
{ 'upload' => file, :content_type => 'image/jpeg' },
:headers => {:content_type => 'image/jpeg'}
)
UPDATE (SOLVED)
I needed to use RestClient because I needed to pass it through to another server (hence the 'url' in the PUT).
My problem was in decoding the image I wasn't stripping out the
data:image/jpeg;base64,
then with this code:
raw_response = RestClient.put(url,
file_binary,
{:content_type => imageContentType})
I was able to get it to put the image and set the correct content-type. The answer below did help though, because I tried it to make sure the image was being decoded properly and it wasn't.
It is quite simple to do. First, you need to decode base64 encoded file. You will get binary file representation. Next use send_data from ActionController to send binary data. Also I have set a filename so it will be delivered to the user.
require 'base64'
class SomeController < ApplicationController
def some_action
file_name = permitted_params[:file_name]
file_base64_contents = permitted_params[:file_contents]
file_binary_contents = Base64.decode64(file_base64_contents)
# https://apidock.com/rails/ActionController/Streaming/send_data
send_data file_binary_contents, filename: file_name
end
end
I'll suggest you to update this implementation with error handling to improve security of your app. One more thing, don't use RestClient. Why do you need it here? Rails gives you all needed things for HTTP communication from controller.
If you have any questions about this please ask. Good luck.

How to handle a file_as_string (generated by Prawn) so that it is accepted by Carrierwave?

I'm using Prawn to generate a PDF from the controller of a Rails app,
...
respond_to do |format|
format.pdf do
pdf = GenerateReportPdf.new(#object, view_context)
send_data pdf.render, filename: "Report", type: "application/pdf", disposition: "inline"
end
end
This works fine, but I now want to move GenerateReportPdf into a background task, and pass the resulting object to Carrierwave to upload directly to S3.
The worker looks like this
def perform
pdf = GenerateReportPdf.new(#object)
fileString = ???????
document = Document.new(
object_id: #object.id,
file: fileString )
# file is field used by Carrierwave
end
How do I handle the object returned by Prawn (?????) to ensure it is a format that can be read by Carrierwave.
fileString = pdf.render_file 'filename' writes the object to the root directory of the app. As I'm on Heroku this is not possible.
file = pdf.render returns ArgumentError: string contains null byte
fileString = StringIO.new( pdf.render_file 'filename' ) returns TypeError: no implicit conversion of nil into String
fileString = StringIO.new( pdf.render ) returns ActiveRecord::RecordInvalid: Validation failed: File You are not allowed to upload nil files, allowed types: jpg, jpeg, gif, png, pdf, doc, docx, xls, xlsx
fileString = File.open( pdf.render ) returns ArgumentError: string contains null byte
....and so on.
What am I missing? StringIO.new( pdf.render ) seems like it should work, but I'm unclear why its generating this error.
It turns out StringIO.new( pdf.render ) should indeed work.
The problem I was having was that the filename was being set incorrectly and, despite following the advise below on Carrierwave's wiki, a bug elsewhere in the code meant that the filename was returning as an empty string. I'd overlooked this an assumed that something else was needed
https://github.com/carrierwaveuploader/carrierwave/wiki/How-to:-Upload-from-a-string-in-Rails-3
my code ended up looking like this
def perform
s = StringIO.new(pdf.render)
def s.original_filename; "my file name"; end
document = Document.new(
object_id: #object.id
)
document.file = s
document.save!
end
You want to create a tempfile (which is fine on Heroku as long as you don't expect it to persist across requests).
def perform
# Create instance of your Carrierwave Uploader
uploader = MyUploader.new
# Generate your PDF
pdf = GenerateReportPdf.new(#object)
# Create a tempfile
tmpfile = Tempfile.new("my_filename")
# set to binary mode to avoid UTF-8 conversion errors
tmpfile.binmode
# Use render to write the file contents
tmpfile.write pdf.render
# Upload the tempfile with your Carrierwave uploader
uploader.store! tmpfile
# Close the tempfile and delete it
tmpfile.close
tmpfile.unlink
end
Here's a way you can use StringIO like Andy Harvey mentioned, but without adding a method to the StringIO intstance's eigenclass.
class VirtualFile < StringIO
attr_accessor :original_filename
def initialize(string, original_filename)
#original_filename = original_filename
super(string)
end
end
def perform
pdf_string = GenerateReportPdf.new(#object)
file = VirtualFile.new(pdf_string, 'filename.pdf')
document = Document.new(object_id: #object.id, file: file)
end
This one took me couple of days, the key is to call render_file controlling the filepath so you can keep track of the file, something like this:
in one of my Models e.g.: Policy i have a list of documents and this is just the method for updating the model connected with the carrierwave e.g.:PolicyDocument < ApplicationRecord mount_uploader :pdf_file, PdfDocumentUploader
def upload_pdf_document_file_to_s3_bucket(document_type, filepath)
policy_document = self.policy_documents.where(policy_document_type: document_type)
.where(status: 'processing')
.where(pdf_file: nil).last
policy_document.pdf_file = File.open(file_path, "r")
policy_document.status = 's3_uploaded'
policy_document.save(validate:false)
policy_document
rescue => e
policy_document.status = 's3_uploaded_failed'
policy_document.save(validate:false)
Rails.logger.error "Error uploading policy documents: #{e.inspect}"
end
end
in one of my Prawn PDF File Generators e.g.: PolicyPdfDocumentX in here please note how im rendering the file and returning the filepath so i can grab from the worker object itself
def generate_prawn_pdf_document
Prawn::Document.new do |pdf|
pdf.draw_text "Hello World PDF File", size: 8, at: [370, 462]
pdf.start_new_page
pdf.image Rails.root.join('app', 'assets', 'images', 'hello-world.png'), width: 550
end
end
def generate_tmp_file(filename)
file_path = File.join(Rails.root, "tmp/pdfs", filename)
self.generate_prawn_pdf_document.render_file(file_path)
return filepath
end
in the "global" Worker for creating files and uploading them in the s3 bucket e.g.: PolicyDocumentGeneratorWorker
def perform(filename, document_type, policy)
#here we create the instance of the prawn pdf generator class
pdf_generator_class = document_type.constantize.new
#here we are creating the file, but also `returning the filepath`
file_path = pdf_generator_class.generate_tmp_file(filename)
#here we are simply updating the model with the new file created
policy.upload_pdf_document_file_to_s3_bucket(document_type, file_path)
end
finally how to test, run rails c and:
the_policy = Policies.where....
PolicyDocumentGeneratorWorker.new.perform('report_x.pdf', 'PolicyPdfDocumentX',the_policy)
NOTE: im using meta-programming in case we have multiple and different file generators, constantize.new is just creating new prawn pdf doc generator instance so is similar to PolicyPdfDocument.new that way we can only have one pdf doc generator worker class that can handle all of your prawn pdf documents so for instance if you need a new document you can simply PolicyDocumentGeneratorWorker.new.perform('report_y.pdf', 'PolicyPdfDocumentY',the_policy)
:D
hope this helps someone to save some time

Rails - Upload picture to Facebook using URL

I'm using Koala for Facebook API and Paperclip for Amazon S3.
I already finished the code for S3 Upload, but having problem for Facebook's upload.
Here's my simplified code:
#user = User.find(params[:id])
#graph = Koala::Facebook::API.new(session[:access_token])
file = open(#user.photo.url(:origin)).read
# ALBUM_ID is constant, its value is the Facebook's album ID
#graph.put_picture(file, { }, ALBUM_ID)
I keep getting this error on last line:
ArgumentError
string contains null byte
I think the way I set file is wrong but I can't find other way to do it.
Thanks before.
While uploading picture on facebook using URL, you just need to send picture url directly (you don't need to send binary data).
# REMOVE THIS LINE
file = open(#user.photo.url(:origin)).read
Update API call as:
# ALBUM_ID is constant, its value is the Facebook's album ID
#graph.put_picture(#user.photo.url(:origin), {}, ALBUM_ID)
Some other ways to use put_picture method:
# put_picture(file, content_type, {:message => "Message"}, 01234560)
# put_picture(params[:file], {:message => "Message"})
# # with URLs, there's no optional content type field
# put_picture(picture_url, {:message => "Message"}, my_page_id)
Source: Koala gem.

Get large images from Page Feed? using Koala Gem & Rails 3.2

Does anyone know how to pull different size images from the Page Feed?
I was trying to use the Type hash that works great for friends and profile pictures.
#page-feed = #graph.get_connections("somepage", "feed", {"type" => "large"})
but for some reason I'm always getting the same picture size for all posts.
Thanks !
Reading the code here: https://github.com/arsduo/koala/blob/81e66f459df840d9d5e122c0d498e2fb9d146655/lib/koala/api/graph_api.rb (line 178, def get_picture) you can see that the method accepts options hash:
Gem source:
# Fetches a photo.
# (Facebook returns the src of the photo as a response header; this method parses that properly,
# unlike using get_connections("photo").)
#
# #param options options for Facebook (see #get_object).
# To get a different size photo, pass :type => size (small, normal, large, square).
# #param block (see Koala::Facebook::API#api)
#
# #note to delete photos or videos, use delete_object(id)
#
# #return the URL to the image
def get_picture(object, args = {}, options = {}, &block)
# Gets a picture object, returning the URL (which Facebook sends as a header)
resolved_result = graph_call("#{object}/picture", args, "get", options.merge(:http_component => :headers)) do |result|
result ? result["Location"] : nil
end
block ? block.call(resolved_result) : resolved_result
end
So you can call it like .get_picture(id, type: :large). Like this:
graph = Koala::Facebook::API.new(token)
profile = graph.get_object('me')
graph.get_picture(profile['id'], type: :large)
Just in case anyone else comes across this, this is what I had to do in order to retrieve the large images. Note that I'm only grabbing the first post in the feed.
In my controller:
#fb_post = #facebook.get_connections(page_id, 'posts').first
#photo = #facebook.get_connections(#fb_post['id'], 'attachments').first
Then, to grab the proper link in the view inside of an img tag, I used:
#photo["media"]["image"]["src"]
For anyone still struggling with this, I was able to use the 'full_picture' field in my Koala object to retrieve a full-resolution URLs of the images:
fields = ['id','picture','full_picture']
graphObj = Koala::Facebook::API.new(accessToken)
hashes = graphObj.get_connection(groupId, 'feed', { limit: 10, fields: fields })
hashes.each do |hash|
mash = Hashie::Mash.new(hash)
fullPicture = mash.full_picture
end

What is the best way to upload a file to another Rails application?

I 've researched and noticed that ActiveResource lack this functionality. So, what is the current state of the art when doing a file upload?
One problem with Guillermo's approach is that the request has to be nested, like this:
body = { :file => {:uploaded_data => File.open("#{RAILS_ROOT}/public/tmp/" + original_filename), :owner_id => current_user.owner_id }, :api_key => '123123123123123123'}
Of course it is not possible to do a request like this with HttpClient. I tried other gems I found in github (sevenwire-http-client and technoweenie-rest-client) but they have problems with the file being nested. Is it possible to upload a file with a nested request?
The Httpclient gem allows you to do multipart posts like this:
clnt = HTTPClient.new
File.open('/tmp/post_data') do |file|
body = { 'upload' => file, 'user' => 'nahi' }
res = clnt.post(uri, body)
end
You could use this to simply post a file on the local file system to a controller in the other application. If you want to upload data just upload with a form into your app without storing it first, you could probably use the uploaded data from your params immediately in the post body.
You can try something like the following:
#I used the HTTPClient gem as suggested (thanks!)
clnt = HTTPClient.new
# The file to be uploaded is originally on /tmp/ with a filename 'RackMultipart0123456789'.
# I had to rename this file, or the resulting uploaded file will keep that filename.
# Thus, I copied the file to public/tmp and renamed it to its original_filename.(it will be deleted later on)
original_filename = params[:message][:file].original_filename
directory = "#{RAILS_ROOT}/public/temporary"
path = File.join(directory, original_filename)
File.open(path, "w+") { |f| f.write(params[:job_application][:resume].read) }
# I upload the file that is currently on public/tmp and then do the post.
body = { :uploaded_data => File.open("#{RAILS_ROOT}/public/tmp/" + original_filename), :owner_id => current_user.owner_id}
res = clnt.post('http://localhost:3000/files.xml', body)

Resources