In my Rails 7 app to PDF creation I'm using external service. To get PDF I have to send a GET request and in the response I'm receiving encoded string. Now I would like to give the possibility to download this file without saving it on the server.
How to do so? all searched topics are almost 10y old, is it some modern way (or maybe a gem) to do so ?
Everything I found actually boils down to the code below:
# service which I'm using to create generate pdf from string
class PdfGenerator
def initialize(binary_pdf)
#binary_pdf = binary_pdf
end
attr_reader :binary_pdf
def call
File.open('invoice.pdf', 'wb') do |file|
content = Base64.decode64(binary_pdf)
file << content
end
end
end
# payments_controller.rb
def pdf_download
response = client.invoice_pdf(payment_id: params[:id]) # get request to fetch pdf
PdfGenerator.new(response[:invoice_pdf]).call
end
View where I'm hitting pdf_download endpoint:
<%= link_to t('.pdf_download'), pdf_download_payment_path(payment.id), data: { 'turbo-method' => :post } %>
I expected to start downloading the file but nothing like that happened. In rails server I'm receiving below message instead:
No template found for PaymentsController#pdf_download, rendering head :no_content
Completed 204 No Content in 8ms (ActiveRecord: 0.2ms | Allocations: 2737)
My answer is mostly a guess as working with files is not one of my strengths.
But basically because you have no escape in payments_controller#pdf_download such as render, redirect etc .. your application is expecting a view called "pdf_download" in views/payments folder.
In your controller, you have to send the data to the user, and this should fix the problem:
send_data(file, disposition: 'inline', filename: "invoice.pdf", type: 'application/pdf')
or
send_data(PdfGenerator.new(response[:invoice_pdf]).call, disposition: 'inline', filename: "invoice.pdf", type: 'application/pdf')
in your case. (The call action should return the file, if it doesn't be more explicit with a return statement)
Though one thing : you create invoices from the same file "invoice.pdf" at the root of your app. Because of GIL/GVL only one thread is performing Ruby at a single time then your file will be created in one go, and returned to the user the same way, without any issue.
Though in case you change the way your app work, maybe introducing async, then race may happen where multiple threads are modifying the same single file at the same time. You may then switch to using tempfiles with different names or finding a way to make your invoices all distincts.
Related
I've got a Rails app where I'm trying to pass the creation of a large PDF to a background process and allow the user to see it when it's finished. It's a multi-page document combined with the combine_pdf gem and passed to delayed_job.
I have 3 actions: the first creates and saves the file, the second is called repeatedly with short delays via an asynchronous request to check if the file exists yet, and the third shows the PDF in the browser.
I'm having trouble with the second part, as it uses File.exist?('my_file.pdf'), but this is returning true before the file has finished saving. The link that is then shown to view the PDF results in an error (ActionController::MissingFile). The file actually becomes available about 10 seconds later, at which point the link works correctly.
I'm guessing the file is still being written at the point that it's checked? How can I check the file saving has completed and the file is actually available to be read?
This is (very broadly and somewhat roughly) how I do it:
First, I call a post action on the controller that is calling the background create process. This action creates a ServiceRequest (a model in my app) with relevant service_request.details and a status of created. The service_request is then sent to the background process (I use RabbitMQ). And the action returns the service_request.id.
The front end starts pinging (via AJAX) the service request end point (something like service_requests/:id), and the ServiceRequestController's show action sends back the service_request.status (along with other stuff, including service_request.results. This loops while the service_request.status is neither completed nor failed.
Meanwhile, the background process creates the PDF. When it is done, it sets the service_request.status to completed. And, it sets service_request.results to contain the data the front end needs to locate and retrieve the PDF. (I store my PDFs to and AWS bucket since I'm on Heroku.)
When the front end finally receives a service_request.status of completed it uses the service_request.results to fetch and display the PDF.
You could write to a temp file then rename it once it's finished. You haven't included your code so far but something like this could work for you:
def write_and_replace(path, file, file_name, data)
file_path = File.join(folder_path, "#{file_name}.pdf")
temp_file_path = File.join(folder_path, "temp_#{file_name}.pdf")
File.open(temp_file_path, 'w') { |f| f.write(data) }
# Args: existing file, new name
File.rename(path, file_path)
File.rename(temp_file_path, file)
File.delete(file_path)
end
Is there a way to stream compressed data to the browser from rails?
I'm implementing an export function that dumps large amounts of text data from a table and lets the user download it. I'm looking for a way to do the following (and only as quickly as the user's browser can handle it to prevent resource spiking):
Retrieve item from table
Format it into a CSV row format
Gzip the row data
Stream to user
If there are more items, go to 1.
The idea is to keep resource usage down (i.e. don't pull from the database if it's not required, don't keep a whole CSV file/gzip state in memory). If the user aborts the download midway, rails shouldn't keep wasting time fetching the whole data set.
I also considered having rails just write a temporary file to disk and stream it from there but this would probably cause the user's browser to time out if the data is large enough.
Are there any ideas?
Here's an older blog post that shows an example of streaming: http://patshaughnessy.net/2010/10/11/activerecord-with-large-result-sets-part-2-streaming-data
You might also have luck with the new Streaming API and Batches. If I'm reading the documentation correctly, you'd need to do your queries and output formatting in a view template rather than your controller in order to take advantage of the streaming.
As for gzipping, it looks like the most common way to do that in Rails is Rack::Deflator. In older versions of Rails, the Streaming API didn't play well Rack::Deflator. That might be fixed now, but if not that SO question has a monkey patch that might help.
Update
Here's some test code that's working for me with JRuby on Torquebox:
# /app/controllers/test_controller.rb
def index
respond_to do |format|
format.csv do
render stream: true, layout: false
end
end
end
# /app/views/test/index.csv.erb
<% 100.times do -%>
<%= (1..1000).to_a.shuffle.join(",") %>
<% end -%>
# /config/application.rb
module StreamTest
class Application < Rails::Application
config.middleware.use Rack::Deflater
end
end
Using that as an example, you should be able to replace your view code with something like this to render your CSV
Name,Created At
<% Model.scope.find_each do |model| -%>
"<%= model.name %>","<%= model.created_at %>"
<% end -%>
As far as I can tell, Rails will continue to generate the response if the user hits stop half-way through. I think this is a limitation with HTTP, but I could be wrong. This should meet the rest of your requirements, though.
I am generating a xls using 'to_xls' gem. So I have like:
my_xls = User.all.to_xls
Now I want to send it using ActionMailer, I tried like that:
attachments[my_xls.original_filename] = {
:content=>my_xls.read,
:mime_type=>my_xls.content_type
}
But for my surprise, my_xls is not a file but a String. I guess I could solve that by opening a new file and writing the string to it, but I'm using Heroku and it doesn't like writing to file (Permission denied). The best solution would be generate a file-like stream data (like getting a file from a HTML form) and send it.
I need something like rails send_data controller method, that send a stream of data to the view without generating a new file.
So, how do I do that?
Something close to this, I might have gotten the mime type wrong, it's generic in the code below, but the format I use in my rails code for action mailer is as follows:
attachment "application/octet-stream" do |a|
a.body = my_xls.read
a.filename = my_xls.original_filename
end
possible types could be:
"application/excel" or "application/vnd.ms-excel" instead of "application/octet-stream"
I have not tested this...
Also if my_xls is a string instead you might have to convert it to bytes before sending it over the wire:
my_xls.bytes.to_a.pack("C*")
there is a SOF topic here talking about this but this is for send_data, but might still apply:
Difficulty with send_data in Ruby on Rails in conjunction with Spreadsheet plug-in
Just trying to point you in a direction that will hopefully help!
The below chunk of code works like a charm. I have tried this, and is in a working application.
def send_excel_report(file_name, emails, subject, email_content, file_path, bcc_emails = [])
attachments["#{file_name}.xls"] = File.read(file_path)
mail(to: emails, subject: subject, from: "my_email#gmail.com", bcc: bcc_emails) do |format|
format.html { render :partial => "users/mail_body"}
end
end
FYI: I have used spreadsheet gem to create the excel
In my Rails application I have an action which creates a XML document using an XML Builder template (rxml) template and render_to_string. The XML document is forwarded to a backend server.
After creating the XML document I want to send a normal HTML response to the browser, but somehow Rails is remembering the first call to render_to_string.
For example:
Rails cannot find the default view show.html.erb because it looks for a show.rxml.
Simply putting a render 'mycontroller/show.html.erb' at the bottom of my action handler makes Rails find the template, but the browser doesn't work because the response header's content type is text/xml.
Is there any way to use render_to_string without "tainting" the actual browser response?
EDIT: It seems that in Rails 2 erase_render_results would do the trick, but in Rails 3 it is no longer available.
The pragmatic answer is that using a view file and two calls to render is Not The Rails Way: views are generally something that is sent to the client, and ActionPack is engineered to work that way.
That said, there's an easy way to achieve what you're trying to do. Rather than using ActionView, you could use Builder::XmlMarkup directly to generate your XML as a string:
def action_in_controller
buffer = ""
xml = Builder::XmlMarkup.new(buffer)
# build your XML - essentially copy your view.xml.builder file here
xml.element("value")
xml.element("value")
# send the contents of buffer to your 3rd server
# allow your controller to render your view normally
end
Have a look at the Builder documentation to see how it works.
The other feature of Builder that you can take advantage of is the fact that XML content is appended to the buffer using <<, so any IO stream can be used. Depending how you're sending content to the other server, you could wrap it all up quite nicely.
Of course, this could end up very messy and long, which is why you'd want to encapsulate this bit of functionality in another class, or as a method in your model.
Seems as if this may be a bug in rails 3 (at least compared to the behavior of 2.3.x render_to_string). In the source for 2.3.8 they clearly take extra steps to reset content_type and set the response body to nil (among other things).
def render_to_string
...
ensure
response.content_type = nil
erase_render_results
reset_variables_added_to_assigns
end
but in the 3.0.3 source for AbstractController::Rendering
def render_to_string(*args, &block)
options = _normalize_args(*args, &block)
_normalize_options(options)
render_to_body(options)
end
You can see there is no explicit resetting of variables, render_to_body just returns view_context.render. It is possible that content-type, response_body, etc are handled elsewhere and this is a red herring, but my first instinct would be to set
response.headers['Content-Type'] = 'text/html'
after your render_to_string before actually rendering.
In migrating the actionwebservice gem I encountered the same error. In their code they circumvent the double render exception by calling the function erase_render_results.
This function is no longer available in rails3. Luckily the fix is quite easy (but it took me a while to find).
Inside actionwebservice the following function was called inside a controller to allow a second render:
def reset_invocation_response
erase_render_results
response.instance_variable_set :#header, Rack::Utils::HeaderHash.new(::ActionController::Response::DEFAULT_HEADERS.merge("cookie" => []))
end
To make this work in rails3, you just have to write:
def reset_invocation_response
self.instance_variable_set(:#_response_body, nil)
response.instance_variable_set :#header, Rack::Utils::HeaderHash.new("cookie" => [], 'Content-Type' => 'text/html')
end
Hope this helps.
I am writing a Rails app that processes data into a graph (using Scruffy). I am wondering how can I render the graph to a blog/string and then send the blog/string directly to the the browser to be displayed (without saving it to a file)? Or do I need to render it, save it to a file, then display the saved image file in the browser?
I think you will be able to use send_data for this purpose:
send_data data_string, :filename => 'icon.jpg', :type => 'image/jpeg', :disposition => 'inline'
If you put this in a controller action - say show on a picture controller, then all you need do is include the following in your view (assuming RESTful routes):
<%= image_tag picture_path(#picture) %>
I wonder if sending direct to the browser is the best way? If there is the possibility that users will reload the page would this short circuit any cache possibilities? I ask because I really don't know.
"If there is the possibility that users will reload the page would this short circuit any cache possibilities?"
No - whether you're serving from a file system or send_data doesn't matter. The browser is getting the data from your server anyway. Just make sure you've got your HTTP caching directives sorted out.