Several times in the last few years I’ve built Rails sites that needed to store files via Paperclip that were not uploaded by a user, but generated programmatically on the server side, for instance PDF or Excel reports built from something in the database. There’s not much documentation on this, and my first effort led to this StackOverflow question. The first solution is not perfect, but it sometimes works:
user.photo = photo_bytes user.photo.instance_write(:file_name, photo_file_name) user.save!
But sometimes that won’t do, because Paperclip uses the file name to determine what kind of file you’ve got. For instance, suppose you’re generating a spreadsheet with the WriteExcel gem:
io = StringIO.new xls = WriteExcel.new(io) # ... xls.close report.spreadsheet = io.string
This will raise a
NoHandlerError in the last line. The solution is to make sure you give Paperclip something with a filename, not just the raw binary string.
The easy way out would be to write your spreadsheet to a temp file, then point Paperclip at it. But writing and reading a temp file adds risk of failure, increases disk IO, and slows things down. I’m stubborn, so I really wanted to keep everything in memory.
My solution is based on the second answer to that StackOverflow question above. If we give Paperclip an
IO object with a method called
original_filename, it will do the right thing. So let’s define this class:
class NamedStringIO < StringIO def initialize(data, filename, content_type=nil) super(data) @filename = filename @content_type = content_type end def original_filename @filename end def content_type @content_type end end
Now we can change our Paperclip code to this:
io = StringIO.new xls = WriteExcel.new(io) # ... xls.close report.spreadsheet = NamedStringIO.new( io.string, xls_filename, 'application/vnd.ms-excel')
That code makes Paperclip happy, and it lets us generate a named “file” that never hits the disk.
Note I’m also adding a
content_type method. Paperclip will accept an instance without that, but if you are storing the files on S3, it will save the file with a generic content type like
application/octet-stream, and that can cause problems when serving the file to web browsers. If you provide a content type here, it will get stored on S3 correctly.
It would be even better to create our
NamedStringIO at the top, so we don’t have that extra
StringIO we give to WriteExcel. For some reason I couldn’t get WriteExcel to accept my
NamedStringIO, so I had to copy things around a bit. Oh well, good enough for me!