<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>The Napkin ~ A Blog By Highgroove Studios comments on Mini File Uploads</title>
    <link>http://cleanair.highgroove.com/</link>
    <language>en-us</language>
    <ttl>40</ttl>
    <description>The Napkin ~ A Blog By Highgroove Studios comments</description>
    <item>
      <title>"Mini File Uploads" by james</title>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Before we start, I should mention that it&amp;#8217;s not Rails&amp;#8217;s fault this is tricky.  The &lt;span class="caps"&gt;CGI&lt;/span&gt; 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&amp;#8217;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.&lt;/p&gt;


	&lt;p&gt;First, I had to change the code we used to check if we had received a file upload.  The old code was:&lt;/p&gt;


&lt;pre&gt;&lt;code&gt;  def file_provided?
    not [String, StringIO].include? @file.class
  end
&lt;/pre&gt;&lt;/code&gt;

	&lt;p&gt;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).&lt;/p&gt;


	&lt;p&gt;The trick to fixing this is to know what Rails can pass you.  There are five options:&lt;/p&gt;


	&lt;ul&gt;
	&lt;li&gt;A String file name (useless).  This happens when you make my famous dumb mistake and forget to set the form as multipart.&lt;/li&gt;
		&lt;li&gt;An empty String.  Same mistake, no file selected by the user.&lt;/li&gt;
		&lt;li&gt;A StringIO containing all the file data.  User uploaded a file smaller than 20 KB.&lt;/li&gt;
		&lt;li&gt;An empty StringIO.  User forgot to upload file.&lt;/li&gt;
		&lt;li&gt;A Tempfile containing all the file data.  User uploaded a file at least 20 KB in size.&lt;/li&gt;
	&lt;/ul&gt;


	&lt;p&gt;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 &lt;strong&gt;some&lt;/strong&gt; content.  Both StringIO and Tempfile support #size and &lt;em&gt;if it walks like a duck and talks like a duck, it must be a duck&lt;/em&gt;:&lt;/p&gt;


&lt;pre&gt;&lt;code&gt;  def file_provided?
    [StringIO, Tempfile].include?(@file.class) and @file.size.nonzero?
  end
&lt;/pre&gt;&lt;/code&gt;

	&lt;p&gt;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:&lt;/p&gt;


&lt;pre&gt;&lt;code&gt;  def save_file
    FileUtils.cp(@file.path, path) if @file
  end
&lt;/pre&gt;&lt;/code&gt;

	&lt;p&gt;This just copies the Tempfile to a permanent location specified by our #path method (not shown).  The problem is that I&amp;#8217;m now allowing StringIO objects through and they won&amp;#8217;t be on the disk to be copied.&lt;/p&gt;


	&lt;p&gt;The easy fix is to use the fact that both StringIO and Tempfile support a #read method:&lt;/p&gt;


&lt;pre&gt;&lt;code&gt;  def save_file
    File.open(path, "wb") { |disk_file| disk_file &amp;lt;&amp;lt; @file.read } if @file
  end
&lt;/pre&gt;&lt;/code&gt;

	&lt;p&gt;&lt;strong&gt;Don&amp;#8217;t do that though!&lt;/strong&gt;&lt;/p&gt;


	&lt;p&gt;If someone uploads a 90 &lt;span class="caps"&gt;MB &lt;/span&gt;PowerPoint presentation it must be loaded into a Ruby String (where it will be much bigger) and dumped back.  Yuck.&lt;/p&gt;


	&lt;p&gt;Because of this, we want to avoid Duck Typing this time and resort to type checking:&lt;/p&gt;


&lt;pre&gt;&lt;code&gt;  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 &amp;lt;&amp;lt; @file.read }
    end
  end
&lt;/pre&gt;&lt;/code&gt;

	&lt;p&gt;This uses our old system for Tempfile objects so we don&amp;#8217;t need to slurp massive data amounts.  When we get a StringIO though, we just write the data out ourselves.  It&amp;#8217;s already in memory and we know it&amp;#8217;s small anyway so there&amp;#8217;s nothing to worry about in this case.&lt;/p&gt;


	&lt;p&gt;That&amp;#8217;s it folks.  Uploads for all sizes.&lt;/p&gt;</description>
      <pubDate>Tue,  3 Oct 2006 16:37:00 EST</pubDate>
      <guid>&lt;a href="/articles/2006/10/03/mini-file-uploads"&gt;Mini File Uploads&lt;/a&gt;</guid>
      <link>&lt;a href="/articles/2006/10/03/mini-file-uploads"&gt;Mini File Uploads&lt;/a&gt;</link>
    </item>
  </channel>
</rss>
