Passing remote web data to attachment_fu

This week I have been mostly discovering what a wonderful Rails plugin attachment_fu is for handling image uploads. You just hand it the contents of an upload field on a form, and it takes care of everything else – checking that it’s a valid image, working out its file type, choosing a sensible filename for it, resizing it appropriately, and sticking the resulting image file into either the filesystem, the database, or Amazon S3.

That last feature has been particularly handy on Demozoo, which is currently hosted in borrowed space on a server that doesn’t really have 100-odd megabytes of free space for demo screenshots. However, attachment_fu comes a bit unstuck when you’re dealing with files that don’t come from a form upload – for instance, in my case I’m planning to have a scheduled task that leeches new productions and their screenshots from Pouët, and I’d quite like to take advantage of the Imagemagick-and-S3 goodness that attachment_fu brings. A bit of digging around was in order.

As the HowtoUploadFiles page on the Rails wiki helpfully explains, when you post a form containing a file upload field, it emerges on the Rails side as a sort-of IO object with a bunch of additional attributes: original_filename, content_type and length (in addition to the usual read method you expect from an IO object), and this is what the uploaded_data method (as implemented by attachment_fu) expects to be passed. According to Ruby’s principle of duck typing (i.e. it doesn’t have to actually be a duck, as long as it quacks like one and waddles like one), we therefore have to come up with our own sort-of IO object that behaves the same way, but with the data sourced from an HTTP fetch instead.

By a happy coincidence, the open-uri module from Ruby’s standard library returns a sort-of IO object that’s mutated in almost (but not quite) just the right way. It gives us content_type and length, but we’re still missing original_filename. Following the guidance of a rails recipe, I came up with the following code to tack that method on:

require 'open-uri'

class Screenshot < ActiveRecord::Base
  has_attachment(
    :content_type => :image,
    :resize_to => '400x300>',
    :storage => :s3
  )

  def source_uri=(uri)
    io = open(URI.parse(uri))
    (class << io; self; end;).class_eval do
      define_method(:original_filename) { base_uri.path.split('/').last }
    end

    self.uploaded_data = io
  end
  
end

The source_uri method is the bit we’re interested in; after getting back our IO object from open, we do some class-level fiddling (which, um, I don’t completely understand myself – I think it’s creating a subclass descending from the IO object or something) to define the missing method original_filename. (If your URLs are coming from somewhere unpredictable, like a web form, you might want to do something more robust than base_uri.path.split(‘/’).last – you might also want to deal with nil being passed as the URI.) We then pass our tweaked IO object off to the uploaded_data method, from where attachment_fu will start doing its stuff unaware that it isn’t really a form upload. Let’s see it in action then…

>> s = Screenshot.new(:source_uri => "http://www.pouet.net/screenshots/18890.jpg")
=> #<Screenshot id: nil, width: nil, height: nil, format: nil, size: nil,
content_type: "image/jpeg", filename: "18890.jpg">
>> s.save!
=> true
>> s.public_filename
=> "http://i-has-a-bucket.s3.amazonaws.com/1/18890.jpg"

There we go… one screenshot successfully uploaded to Amazon S3, without a form upload in sight! (And as a bonus, in theory open-uri ought to work just the same with FTP URLs as HTTP ones, if that floats your boat.)

12 Responses to “Passing remote web data to attachment_fu”

  1. Denis says:

    Thanks, it was useful for me.

  2. Stefan says:

    useful for me as well….thanks!

  3. matt says:

    Since people are finding this useful (which is great), perhaps I should fill in the missing detail on the (class << io; self; end;) bit which I now understand a bit better. It turns out that’s a standard Ruby idiom for returning the singleton class of the io object, which is a special subclass of the IO class that’s spontaneously created to just refer to this particular object (io). So, the class_eval block defines original_filename as an instance method within the class of this particular io object.

    Not really sure why you need the class_eval rather than simply doing class << io; (define method here); end – I suspect that would work too, and the original programmer I borrowed it off just used that idiom out of familiarity. If so, it just goes to show that there’s a fine line between idioms and cargo cult programming

  4. Hi there. Your tip saved me tons of time and lots of code! Thanks ;)

  5. Alex says:

    Very useful, just what I was looking for. Thanks!

  6. Paul says:

    Thanks a million. You saved us lots of time :)

  7. [...] did a bit of digging around before I came across this handy trick that makes it pretty painless to add uploading via a URL to your app. I’ve wrapped up the code a [...]

  8. tachekent says:

    I adapted this to use a local image file grabbed from a video:

    def source_file=(filename)
    io = open(filename)
    (class << io; self; end;).class_eval do
    define_method(:original_filename) { filename.split('/').last }
    define_method(:content_type) { 'image/jpeg' }
    define_method(:size) { File.size(filename)}
    end
    self.uploaded_data = io
    end

  9. Mark says:

    This is great but how would you go about having attachment_fu resize the image like if it was uploaded?

  10. matt says:

    Mark: It should just respect the :resize_to => ’400×300>’ directive exactly as it stands. As far as attachment_fu is concerned, the IO object it receives is no different from the one it would get from an upload (or that’s the plan, anyway), so all the resizing / storage functionality will work as normal.

  11. matteo says:

    Thanks a lot. Easy and effective.

  12. Duende13 says:

    Thank you!!!!

Leave a Reply