Four Key Reasons to Learn Markdown
Back-End Leveling UpWriting documentation is fun—really, really fun. I know some engineers may disagree with me, but as a technical writer, creating quality documentation that will...
I just finshed fixing file uploads in a HighGroove application to work with any size file. I uploaded a 14 byte file to make sure I had things right. This has a few gotchas in Rails, so I thought I would share the recipe for success.
Before we start, I should mention that it’s not Rails’s fault this is tricky. The CGI library Rails relies on has quirky behavior: it returns different kinds of objects for some file uploads. The main issue is that files under 20 KB in size are passed to your code in a StringIO, while the bigger stuff comes in a Tempfile. The StringIO object is modified to support methods like #original_filename that you are likely to need, but it’s still easy to make a mistake so your application fails to work with the little files. This post is about how to avoid those mistakes.
First, I had to change the code we used to check if we had received a file upload. The old code was:
def file_provided? not [String, StringIO].include? @file.class end
This code had probably just been copied from some early work we did, before we understood the rules. We saw that sometimes we would get a String or StringIO, but we just supported files bigger than 20 KB back then (Tempfile).
The trick to fixing this is to know what Rails can pass you. There are five options:
A String file name (useless). This happens when you make my famous dumb mistake and forget to set the form as multipart.
An empty String. Same mistake, no file selected by the user.
A StringIO containing all the file data. User uploaded a file smaller than 20 KB.
An empty StringIO. User forgot to upload file.
A Tempfile containing all the file data. User uploaded a file at least 20 KB in size.
Once you know those, the solution is easy. We want a StringIO or Tempfile (the Strings are errors) and we need to make sure there is at least some content. Both StringIO and Tempfile support #size and if it walks like a duck and talks like a duck, it must be a duck:
def file_provided? [StringIO, Tempfile].include?(@file.class) and @file.size.nonzero? end
The other issue in our old code was when we actually saved the file data. This is why we only supported Tempfile originally. The old code was:
def save_file FileUtils.cp(@file.path, path) if @file end
This just copies the Tempfile to a permanent location specified by our #path method (not shown). The problem is that I’m now allowing StringIO objects through and they won’t be on the disk to be copied.
The easy fix is to use the fact that both StringIO and Tempfile support a #read method:
def save_file File.open(path, "wb") { |disk_file| disk_file << @file.read } if @file end
Don’t do that though!
If someone uploads a 90 MB PowerPoint presentation it must be loaded into a Ruby String (where it will be much bigger) and dumped back. Yuck.
Because of this, we want to avoid Duck Typing this time and resort to type checking:
def save_file if @file.is_a? Tempfile FileUtils.cp(@file.path, path) elsif @file.is_a? StringIO File.open(path, "wb") { |disk_file| disk_file << @file.read } end end
This uses our old system for Tempfile objects so we don’t need to slurp massive data amounts. When we get a StringIO though, we just write the data out ourselves. It’s already in memory and we know it’s small anyway so there’s nothing to worry about in this case.
That’s it folks. Uploads for all sizes.
Writing documentation is fun—really, really fun. I know some engineers may disagree with me, but as a technical writer, creating quality documentation that will...
Humanity has come a long way in its technological journey. We have reached the cusp of an age in which the concepts we have...
Go 1.18 has finally landed, and with it comes its own flavor of generics. In a previous post, we went over the accepted proposal and dove...