Monday, November 19, 2007

Rails' attachment_fu, :thumbnail_class and you

I was having a bit of trouble with attachment_fu that took a while to figure out, so I thought I'd post my solution for the next person.

I have a Photo model that I'm using to store pictures. Since attachment_fu will automatically resize, create thumbnails, and store pictures on the file system, it was an easy choice to use it. The things I found I didn't like about it:

  1. It stores thumbnails as separate records inside the photos table. This means you have to check if thumbnail.nil? each time you display the pictures, and you'll have to check parent_id.nil? to count your photos.

  2. You can't use your own model validations. For example, I wanted to use

validates_presence_of :name
validates_presence_of :description

But I'd always get validation errors because attachment_fu tries to save the thumbnail attributes when it creates or resizes a thumbnail. As you can imagine, this is a major problem.

:thumbnail_class to the rescue

Mike Clark's attachment_fu blog post mentions that you can use the :thumbnail_class argument to separate your model validations and attachment_fu's validations. Here's how to do it:

class Photo < ActiveRecord::Base
has_many :thumbnails, :foreign_key => 'parent_id'

has_attachment :storage => :file_system,
:content_type => :image,
:max_size => 10.megabytes,
:resize_to => '640x480',
:thumbnails => { :thumb => '100x100' },
:thumbnail_class => Thumbnail

# Validations
validates_presence_of :name
validates_presence_of :description

class Thumbnail < ActiveRecord::Base
belongs_to :photo, :foreign_key => 'parent_id'

has_attachment :storage => :file_system,
:content_type => :image

The killer for me initially was that I wasn't specifying has_attachment in the Thumbnail model. I always got this error:

undefined method `temp_path=' for #thumbnail:0xb69a9514

So save yourself by putting has_attachment in both models. Make sure to define any attachment_fu arguments in your Photo model, and leave the Thumbnail model bare. You'll also want to make sure you have the normal attachment_fu schema in both models:

t.column :parent_id, :integer
t.column :content_type, :string
t.column :filename, :string
t.column :thumbnail, :string
t.column :size, :integer
t.column :width, :integer
t.column :height, :integer

I've found that everything seems to be working as normal going this route. My model validations work and the thumbnails are not polluting the photos table. If you want to find the Photo for a particular Thumbnail, keep in mind that parent_id now refers to the id in the Photo model:

t = Thumbnail.find .... # find your thumbnail
p = Photo.find(t.parent_id)

And for the ultimate ease-of-use relationship, just use:

t = Thumbnail.find .... # find your thumbnail
p = # get a photo
p.thumbnails # get all thumbnails

This works since we defined the has_many relationship in the model.

Let me know if you have any problems with this method or if it helped you!

Sunday, November 4, 2007

Serve home directories to internal IPs only with lighttpd

Recently, I decided I wanted to share home directories via HTTP to everyone on my home network. This is an easy way to share files with Windows machines where you don't have any type of sshfs-like support. The problem was that this file/web server also faces the Internet. Obviously I don't want to share our personal files to anybody who cares to look. After a bit of playing, I came up with this configuration:

$HTTP["remoteip"] != "" {
$HTTP["url"] =~ "^/~" {
url.access-deny = ( "" )
dir-listing.activate = "disable"

This allows any host with an IP in the range view any URL that begins with /~username. It denies everyone else with a 403 - Forbidden message. Note that for some reason, listing the conditions in the opposite order (url first, remoteip second) did not produce the correct results.

Finally, make sure that you have both the appropriate modules enabled:

server.modules += ( "mod_access", "mod_userdir" )