This post originated from an RSS feed registered with Ruby Buzz
by Guy Naor.
Original Post: Ruby Duck Typing Goodness
Feed Title: Famundo - The Dev Blog
Feed URL: http://devblog.famundo.com/xml/rss/feed.xml
Feed Description: A blog describing the development and related technologies involved in creating famundo.com - a family management sytem written using Ruby On Rails and postgres
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 StoredFileattr_accessor:size,:filename,:modified_time# Trick the id...def idfilenameenddef id=(the_id)filename=the_idend# All the methods we need to overwrite from active_record...def self.table_name'stored_files'enddef self.primary_key'filename'enddef filename_before_type_castfilenameenddef 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))}.sizeenddef 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 tablesifargs.first.is_a?(String)fname=args.first.gsub(/[^\w\.\-]/,'_')raise"File not found #{args.first}"if!File.exist?("#{$STORAGE_DIR}#{fname}")returnStoredFile.init_from_file(fname)endfltr=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_filef}# This is now an unsorted list of files. Now we can sort and apply limits/offsetsifoptions[:order]options[:order]=~/^(.+) (asc|desc)?$/ireverse_ord=!$2.nil?&&($2.downcase=="desc")case$1when'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}endflist.reverse!ifreverse_ordendidx=(options[:offset]?options[:offset]:0).to_ilen=(options[:limit]?options[:limit]:flist.size).to_iflist[idx,len]enddef errors[]enddef self.destroyfnameFileUtils.rm_f"#{$STORAGE_DIR}#{fname}"fnameenddef destroyStoredFile.destroyself.idendprotecteddef self.init_from_fileffstat=File::stat"#{$STORAGE_DIR}#{f}"sf=StoredFile.newsf.filename,sf.modified_time,sf.size=f,fstat.mtime,fstat.sizesfendend