The Dev Blog

Putting Family Management on Rails!

Ruby Duck Typing Goodness

Posted by Guy Naor Fri, 12 Jan 2007 22:48:00 GMT

Coming over from C++ to Ruby, duck typing was one of the really cool features I learned about. You don't use it every day, but when you need it, it's an amazing tool.

Case in point: writing the admin interface to the Famundo help app (you can currently see the public interface at Famundo's Help. (A new version is coming out shortly, and I intend to Open Source it soon.) I used AjaxScaffold for the interface, but wanted to also manage file uploads. I wanted to keep it the same interface as the rest of the tables, as it gives me a nice UI, sorting, searching, etc...

To make that work, I duck typed a class that uses the filesystem, but acts like an ActiveRecord class. I then pointed AjaxScaffold at it and as far as the user experience goes, it's just like managing a database table. Simple and intuitive.

I didn't duck type everything in ActiveRecord, just the stuff that AjaxScaffold needed. Of course, if the need comes, adding more methods is very easy.

Following is the class I created. The $STORAGE_DIR global points to where the storage dir is. Usually something under public so that it's easy to serve the files. But you can also use x-send-file or some other trick and put it someplace else.

class StoredFile 

  attr_accessor :size, :filename, :modified_time

  # Trick the id...
  alias_method :id, :filename 
  alias_method :id=, :filename=

  # All the methods we need to overwrite from active_record...
  def self.table_name
    'stored_files'
  end

  def self.primary_key
    'filename'
  end

  def filename_before_type_cast
    filename
  end

  def self.count(*args)
    options = args.last.is_a?(Hash) ? args.pop : {} # Taken from the rails source!
    fltr = options[:conditions] || ''
    Dir.entries($STORAGE_DIR).delete_if{|i| i[0..0] == '.' || (fltr.is_a?(Regexp) ? i !~ fltr : !i.downcase.include?(fltr.to_s))}.size
  end

  def self.find(*args)
    options = args.last.is_a?(Hash) ? args.pop : {} # Taken from the rails source!
    # See if it's a single file find, like ID in rails tables
    if args.first.is_a?(String)
      fname = args.first.gsub(/[^\w\.\-]/, '_')
      raise "File not found #{args.first}" if !File.exist?("#{$STORAGE_DIR}#{fname}")
      return StoredFile.init_from_file(fname)
    end

    fltr = options[:conditions] || ''
    flist = Dir.entries($STORAGE_DIR).delete_if{|i| i[0..0] == '.' || (fltr.is_a?(Regexp) ? i !~ fltr : !i.downcase.include?(fltr.to_s))}.collect{|f| StoredFile.init_from_file f }

    # This is now an unsorted list of files. Now we can sort and apply limits/offsets
    if options[:order]
      options[:order] =~ /^(.+) (asc|desc)?$/i
      reverse_ord = !$2.nil? && ($2.downcase == "desc")
      case $1
        when 'filename'     : flist.sort! {|a,b| a.filename <=> b.filename }
        when 'modified_time': flist.sort! {|a,b| a.modified_time <=> b.modified_time }
        when 'size'         : flist.sort! {|a,b| a.size <=> b.size }
      end

      flist.reverse! if reverse_ord
    end

    idx = (options[:offset] ? options[:offset] : 0).to_i
    len = (options[:limit]  ? options[:limit ] : flist.size).to_i

    flist[idx,len]
  end

  def errors
    []
  end

  def self.destroy fname
    FileUtils.rm_f "#{$STORAGE_DIR}#{fname}"
    fname
  end

  def destroy
    StoredFile.destroy self.id
  end

  protected

  def self.init_from_file f
    fstat = File::stat "#{$STORAGE_DIR}#{f}"
    sf = StoredFile.new
    sf.filename, sf.modified_time, sf.size = f, fstat.mtime, fstat.size
    sf
  end

end

Posted in ,  | 2 comments

del.icio.us:Ruby Duck Typing Goodness digg:Ruby Duck Typing Goodness spurl:Ruby Duck Typing Goodness wists:Ruby Duck Typing Goodness simpy:Ruby Duck Typing Goodness newsvine:Ruby Duck Typing Goodness blinklist:Ruby Duck Typing Goodness furl:Ruby Duck Typing Goodness reddit:Ruby Duck Typing Goodness fark:Ruby Duck Typing Goodness blogmarks:Ruby Duck Typing Goodness Y!:Ruby Duck Typing Goodness smarking:Ruby Duck Typing Goodness magnolia:Ruby Duck Typing Goodness segnalo:Ruby Duck Typing Goodness

Comments

  1. Chris said about 15 hours later:

    In the case of code such as:

    def id filename end

    def id=(the_id) filename = the_id end

    You may wish to consider using the alias_method method.

    alias_method :id, :filename alias_method :id=, :filename=

  2. Guy Naor said about 23 hours later:

    Good catch Chris. I was adding the methods one by one based on tests, so I missed this as being the same :-).

    It's double good, as it cuts a few more lines of code from the help app I'm writing. It's already under 600 LOC, but the less the better.

    I updated the post to reflect this change.

Comments are disabled

Subscribe to The Dev Blog