Export CSV and Attach to Model With Paperclip
From time to time, users will want export their data from your app. An easy way to do that is to build a CSV (comma separated values) file and then let them download it. I’ll show you how build that CSV and then attach to a model with Paperclip.
First things first, add an attachment to your model:
rails generate migration add_csv_to_foo
Then in your migration:
def change
add_attachment :foos, :csv
end
Then in your model:
class Foo < ActiveRecord::Base
has_attached_file :csv
validates_attachment :csv, content_type: { content_type: "text/csv" }
end
Next, for you model, create a method for exporting whatever it is you want to export to a CSV. There’s a great RailsCast about it you can check out to get it set up. In my app, I’m allowing users to export their contacts to a CSV. Some users have upwards of 3,000 contacts, so exporting contacts can take quite a long time. Therefore, I put my export method in background job.
def perform(foo, options = {})
CSV.generate(options) do |csv|
csv << make_column_names(foo)
build_row_values(foo).each do |row|
csv << row
end
file = StringIO.new(csv.string)
foo.csv = file
foo.csv.instance_write(:content_type, 'text/csv')
foo.csv.instance_write(:file_name, make_filename(foo))
foo.save!
end
end
The first part of that method generates the CSV, which is really just a big string. The second part of the method is:
file = StringIO.new(csv.string)
foo.csv = file
foo.csv.instance_write(:content_type, 'text/csv')
foo.csv.instance_write(:file_name, make_filename(foo))
foo.save!
That part takes the big string part of the CSV, turns into a pseudo I/O that Paperclip can attach to the model and upload to S3.
Now, you can call foo.csv.url
to get a link to the CSV file stored on S3. If you just link to that however, most browsers will simply render the CSV as text. What you want is to force the file to download. To do that, create a controller action that will send the file as data, instead of just linking to the url. It looks like this:
class FoosController < ApplicationController
def download_csv
foo = Foo.find(params[:id])
data = open(foo.csv.url)
send_data data.read, filename: "#{foo.csv_file_name}", type: "text/csv", disposition: 'attachment'
end
end
That will force the file to be downloaded instead of just being rendered in the browser.
Bonus: Progress bar
In my app, users are exporting sometimes 3,000 or more contacts. I use DelayedJob for the background work in the app. There’s a great gem progress_job that gives you an easy way to display the progress of background jobs.
The demo code in the progress_job repo has a JavaScript function with a 100ms timeout, which hits your server and database a lot. I just upped that 800ms and it still looks great.