s

Changing Paperclip File Storage Location

Jan 11, 2009  -  Comments

For a while at Plexus, we've been using FileColumn for all our image/file upload attachment needs. It's worked out really well, but when we saw Paperclip we thought it might be a better choice.

Paperclip is super easy to setup and use, but we found ourselves wanting to slightly change the default way it stored attachments. We were used to the way that FileColumn created its folder structure. It would make a folder named for the model (singular) that the attachment(s) were part of and a folder for each attachment (singular). Say we had a BlogPost model with an image attachment and a file attachment. FileColumn would make the following two folders: public/blog_post/image and public/blog_post/file.

Paperclip operates a little differently. It creates a folder (plural) in public for each attachment. If you had the same setup as before, Paperclip would create the following two folders: public/images and public/files. Can you see the immediate problem with this? We already have a public/images folder in our default Rails file structure, so this might get a little confusing. Another problem arises if we have several models with an image attachment. Paperclip would store them all in the same folder. This would be ok 99% of the time. See, Paperclip gets images by their id and name, so even if two models have image attachments and the same id, as long as the name is different, we're ok. But, if you somehow add a different image with the same name, to a model with the same id, then it would get overwritten. Not very likely, but still possible.

The great thing about Paperclip is that you can change the default way it stores its attachments. You can just past a few extra options to the has_attached_file call in your model. As of the current version of Paperclip, they have fixed the file structure problem by tweaking the default storage path. They added a system subfolder in the public folder. This has a two-fold benefit. First, we don't have the problem before of having an attachment named image. Second, this works great for Capistrano because the system folder is already symlinked from the public folder, so you don't have to worry about adding symlinks to your deploy file.

Anyway, back to my point of changing the default file storage path. You can just add the url (which tells where to retrieve the files) and path (which tells where to save the file) options to your has_attached_file call in the model. We add the :class option to include the model_name as a folder.

has_attached_file :image,
  :styles => {:thumb => '120x120>', :large => '640x480>' },
  :default_style => :thumb,
  :url => "/system/class/attachment/id/style/basename.extension",
  :path => ":rails_root/public/system/:class/:attachment/:id/:style/:basename.:extension"

NOTE: Thanks to commenter Steve Bartz for pointing out that current versions of Paperclip have the default path set to ":rails_root/public:url". Because of this, you can leave out the :path option.

So, now our folder structure for the original example would be public/system/blog_posts/images and public/system/blog_posts/files. Much better!

Just make sure you put the whole path in the :path option using the :rails_root variable.

Tagged: rubyrailspapercliptutorial

Deploying a Merb application with the RailsMachine gem

Feb 07, 2009  -  Comments

I recently launched a monthly bill/task tracking application I've been working on in my spare time. I used Merb so I could get some experience with the framework.

Plexus was kind enough to donate some server space on a RailsMachine server. Luckily, they recently added Passenger support to their awesome RailsMachine gem, so all I needed was to add a Rack config file to run my app on Passenger.

require 'rubygems'
require 'merb-core'
 
Merb::Config.setup(:merb_root => File.expand_path(File.dirname(__FILE__)),
                   :environment => ENV['RACK_ENV'])
Merb.environment = Merb::Config[:environment]
Merb.root = Merb::Config[:merb_root]
Merb::BootLoader.run
 
run Merb::Rack::Application.new

After that, I only needed to update the Capistrano deploy file to work with Merb and Passenger.

require 'railsmachine/recipes'
 
# The name of your application. Used for directory and file names associated with
# the application.
set :application, "listode"
 
# Target directory for the application on the web and app servers.
set :deploy_to, "/var/www/apps/#{application}"
 
# Primary domain name of your application. Used as a default for all server roles.
set :domain, "listode.com"
 
# Login user for ssh.
set :user, "deploy"
set :runner, user
set :admin_runner, user
 
# Rails environment. Used by application setup tasks and migrate tasks.
set :rails_env, "production"
 
# Automatically symlink these directories from curent/public to shared/public.
set :app_symlinks, %w{graphs}
 
set :deploy_via, :remote_cache
 
# =============================================================================
# ROLES
# =============================================================================
# Modify these values to execute tasks on a different server.
role :web, domain
role :app, domain
role :db, domain, :primary => true
role :scm, domain
 
 
# =============================================================================
# APPLICATION SERVER OPTIONS
# =============================================================================
set :app_server, :passenger # :mongrel or :passenger
 
# =============================================================================
# SCM OPTIONS
# =============================================================================
set :scm, :git # :subversion or :git
set :repository, "git@github.com:travisr/#{application}.git"
 
# =============================================================================
# CUSTOM CONFIGURATION
# =============================================================================
# action to symlink database file
namespace :deploy do
  desc "Symlink database config file."
  task :symlink_db do
    run "ln -nfs #{shared_path}/system/database.yml #{release_path}/config/database.yml"
  end
end
 
# Overwrite the default deploy.migrate as it calls:
# rake RAILS_ENV=production db:migrate
desc "Use datamapper to call autoupgrade instead of db:migrate."
deploy.task :migrate do
 run "cd #{release_path}; rake db:autoupgrade MERB_ENV=production"
end

after 'deploy:update_code', 'deploy:symlink_db'

The custom section at the bottom sets up a symlink to my databases.yml file since I don't keep that in my git repo. I also have to override the migration action to use DataMapper's db:autoupgrade.

Tagged: rails machinecapistranopassengermerbtutorial

Create your own custom rails generator

Feb 22, 2009  -  Comments

It's really easy to add a custom generator to your Rails application. Say you have a component you want to include in multiple projects, but you don't want to manually copy ALL of the files from project to project. At Plexus, we have an empty Rails project with basic styling and structure that we use for all new applications. We have several components that we wanted to simplify adding to new projects. So, we created a few custom generators that we can use to create the components with very little effort.

The first thing you need to do is add a generators folder inside the lib folder. In there you can add the files and folders for each custom generator. In this example, I'll use a Blog as the component I'm building a generator for.

Inside the generators folder, I created a blog folder (hint: whatever you name the folder will be how you call your custom generator). All of my files for the blog functionality will be in this folder. The two most important things in this folder are the actual generator file that will do all of the work and the templates folder which contains all the files to be copied. My blog generator file, blog_generator.rb looks like this:

class BlogGenerator < Rails::Generator::Base
  def manifest
    record do |m|

      # Controllers
      m.file "controllers/blog_controller.rb", "app/controllers/blog_controller.rb"

      # Models
      m.file "models/blog_post.rb", "app/models/blog_post.rb"

      # Helpers
      m.file "helpers/blog_helper.rb", "app/helpers/blog_helper.rb"

      # Views
      m.directory "app/views/blog"
      m.file "views/index.html.erb", "app/views/blog/index.html.erb"
      m.file "views/details.html.erb", "app/views/blog/details.html.erb"
      m.file "views/feed.rss.builder", "app/views/blog/feed.rss.builder"

      # Migration
      m.migration_template "migrate/create_blog.rb", "db/migrate"

      # Tests
      m.file "test/fixtures/blog_posts.yml", "test/fixtures/blog_posts.yml"
      m.file "test/functional/blog_controller_test.rb", "test/functional/blog_controller_test.rb"
      m.file "test/unit/blog_post_test.rb", "test/unit/blog_post_test.rb"

      # CSS and images
      m.file "/2009/assets/blog_styles.css", "/2009/public/stylesheets/px_blogger.css"
      m.file "/2009/assets/comment_add.gif", "/2009/public/images/comment_add.gif"
      m.file "/2009/assets/comment.gif", "/2009/public/images/comment.gif"

      m.readme "INSTALL"
    end
  end

  def file_name
    "create_blog"
  end

end

Here is a breakdown of what is going on:

  • The directory method will create the specified directory if it doesn't exist already.
  • The file method will copy the specified file to the given directory.
  • The migration_template file will copy the given migration file into the db/migrations folder using the file_name method defined at the bottom of the generator to name the file.
  • The readme function prints out the contents of the INSTALL file after the generator script is called. You can use this file to put any extra instructions for the generator.

This is what the file structure looks like for the generator:

lib
\- generators
   \- blog
      \- blog_generator.rb
         templates
         \- assets
            \- blog_styles.css
               comment_add.gif
               comment.gif
            controllers
            \- blog_controller.rb
            helpers
            \- blog_helper.rb
            INSTALL
            migrate
            \- create_blog.rb
            models
            \- blog_post.rb
            test
            \- fixtures
               \- blog_posts.yml
               functional
               \- blog_controller_test.rb
               unit
               \- blog_post_test.rb
            views
            \- index.html.erb
               details.html.erb
               feed.rss.builder
         USAGE

All we need to do to run this generator is call script/generate blog.

Tagged: railsgeneratorstutorial

Ruby Net::FTP Tutorial

Mar 29, 2009  -  Comments

Recently, at Plexus, a client needed the ability to import photos to their site from a remote FTP server. Perfect opportunity for me to learn about Net::FTP. Turns out it was surprisingly simple.

Let's say we want to login to the server 'ftp.sample.com' with the username 'test' and the password 'pass', then switch to the directory 'source/files' and get the file 'photos.zip'. There are a couple ways to do this. First, we have to create and FTP connection with:

require 'net/ftp'

# Login to the FTP server
ftp = Net::FTP.new('ftp.sample.com', 'test', 'pass')
# OR
ftp = Net::FTP.new('ftp.sample.com')
ftp.login('test', 'pass')

# Switch to the desired directory
ftp.chdir('source/files')

# Get the file we need and save it to our 'ftp_photos' directory
ftp.getbinaryfile('photos_2009-03-29.zip', 'ftp_photos/photos.zip')

# We're done, so we need to close the connection
ftp.close

You can also accomplish the same thing by passing a block to the open method, like so:

require 'net/ftp'

Net::FTP.open('ftp.sample.com') do |ftp|
  ftp.login('test', 'pass')
  ftp.chdir('source/files')
  ftp.getbinaryfile('photos_2009-03-29.zip', 'ftp_photos/photos.zip')
end

Pretty straightforward and simple.

Tagged: net ftptutorialftpruby

Better Text Replacement with CSS

Apr 16, 2009  -  Comments

There are several methods for rendering non-web fonts (eg. Cufón and sIFR). Both are tedious to set up and implement. The easiest alternative is the good old image-instead-of-text trick, but that's not good for SEO, even with alt tags.

There is a way to have SEO text AND use an image as the text. With a little CSS trickery, you can easily achieve the desired effect. Worth noting, however, is that this method is really only good for non-dynamic, fixed width text (such as headings). It's not practical for blocks of text.

The first thing you need is an image for the text you want to replace (and yes, I'm using Comic Sans):

Now for the html code that we'll be working with:

<h1 class="replace heading">My Heading</h1>

The CSS does all the work.

.replace {
  display:block;
  height:0;
  overflow:hidden;
  font-size:0;
  letter-spacing:-1em;
  text-indent:-1000em;
}
.heading {
  width:135px;
  padding:26px 0 0 0;
  background:url('/images/custom_header.png') no-repeat 0 0;
}

First, we make the h1 virtually invisible via CSS by assigning a height and font-size of 0. Then we make sure it's invisible in all browsers by setting the letter-spacing and text-indent. The heading class has a top-padding which creates just enough space to show the image, which we set as the background. Super easy and effective!

You'll notice that I made two classes. I use the replace class as a global for the styles that are common to all replaced text, then the heading class has the unique styles.

Tagged: csstext replacementtutorial

Twitter Gem Examples

Apr 22, 2009  -  Comments

I recently set up a Twitter account for a monthly bill and task tracking application that I built a few months back. My intent was to try and drive traffic to my site (which had been sitting unused by the general public). To do this, I decided to mass-follow around 350 accounts in hopes of having them follow me back and checking out the site. It worked pretty well, and I even had quite a few users cold follow the account. At first, I would follow the users that followed me when I got the notification from Twitter. After a few days, I got a little behind and the followers started to build up. I figured this would be a good time to check out the Twitter gem to see if I could automate some of my tasks. The gem had exactly what I needed: a way to talk to Twitter via Ruby. I've included below two of the tasks that I created to work with my Twitter account.

First things first, I needed to set up my authentication. To do this, I just created a YAML file in my home directory called .twitter that contains my user email and password. The . means that it's a hidden file (I'm on a Mac). The YAML file is extremely simple, and looks like this:

email: my_twitter_email
password: my_twitter_password

Now, I could use this YAML file for any of the scripts that I wrote.

Task #1: Follow Users Who Follow Me

I wanted to get a list of all my followers and check to see if I'm already following them. If I'm not, I want to create a friendship with them.

#!/usr/bin/env ruby
require 'rubygems'
require 'twitter'

config = YAML::load(open("#{ENV['HOME']}/.twitter"))

httpauth = Twitter::HTTPAuth.new(config['email'], config['password'])
base = Twitter::Base.new(httpauth)

base.followers.each do |follower|
  if !follower.following
    # make sure to rescue in case there is anything wrong with the account
    base.friendship_create(follower.id, true) rescue next
    puts "Created friendship with #{follower.screen_name}"
  end
end

Task #2: Stop Following Users Who Aren't Following Me

I followed about 350 accounts initially, and after about a week, I figured that if they weren't following me by then, they'd probably never follow me. So, since I'm all about reciprocation, I decided to stop following them.

#!/usr/bin/env ruby
require 'rubygems'
require 'twitter'

config = YAML::load(open("#{ENV['HOME']}/.twitter"))

httpauth = Twitter::HTTPAuth.new(config['email'], config['password'])
base = Twitter::Base.new(httpauth)

base.friends.each do |friend|
  if !base.friendship_exists?(friend.screen_name, 'listode')
    base.friendship_destroy(friend.id)
    puts "Destroyed friendship with #{friend.screen_name}"
  end
end

A Quick Note

Keep in mind that, unless you've been white-listed, your account is limited to 100 API calls per hour. That shouldn't be an issue with the first script, since it only makes one call to get the list and one call for each friend creation. You should stay below the cap (unless you have more than 100 followers who you aren't following).

The second script is a different story. It makes one call to get the list of friends, one call for each friend to check following, and one call to destroy the friendship if they aren't following. This can easily burn up the API limit if you have more than 100 friends. I haven't figured out a way to reduce the number of API calls for that script. If you have any tips, leave them in the comments.

Tagged: twittertutorialruby

Rails DELETE requests with JQuery

May 20, 2009  -  Comments

When I write admin controls for for a Rails app, I like to make the delete links just fire off an Ajax request, delete the record, and remove the element from the page. This is easy with the Rails default setup using Prototype. All you have to do is use link_to_remote with a :method => 'delete' to make Rails do all the work. The problem is, this is the code that Rails generates:

<%= link_to_remote("Delete", :url => admin_post_path(post), :method => :delete) %>
<!- turns into: -->
<a href="#" onclick="new Ajax.Request('/users/63', {asynchronous:true, evalScripts:true, method:'delete'});); return false;">Delete</a>

While this isn't terrible, it's certainly not very pretty. Surely we can make the code cleaner and simpler!

Well, we've recently switched to using JQuery for all of our new apps at Plexus. Unless you use the JRails plugin, the link_to_remote helper no longer works when using JQuery. I figured this would be a good time to switch the remote calls to be handled separately by JavaScript, instead of spitting out a ton of script in the HTML.

It's easy to just hijack links via JavaScript and have them submit via Ajax, but how do we handle REST requests that require the DELETE method?

I decided to just add a specific class to all links that I wanted to be hijacked by JavaScript, then add the code in my js file to handle the links with Ajax. My DELETE links would look like this:

<!- Link that uses DELETE method -->
<%= link_to 'Delete', entry_path(entry), :class => 'remote-delete' %>

The key to hijacking the link is to include the hidden _method attribute set to 'delete'. My JavaScript to hijack the link would look like this:

$(document).ready(function() {
  $('a.remote-delete').click(function() {
    // we just need to add the key/value pair for the DELETE method
    // as the second argument to the JQuery $.post() call
    $.post(this.href, { _method: 'delete' }, null, "script");
    return false;
  });
});

This converts every link that has the remote-delete class to an Ajax request with the DELETE method!

Tagged: jquerydeleterest

Ruby Script to Add Apache Virtual Host Entry

Aug 10, 2009  -  Comments

When you're working with Rails, you never really have to add a virtual host entry for development (unless you use Passenger). You can always just fire up script/server and navigate to http://localhost:3000.

At my new job, I'm doing a lot of work on PHP and Drupal sites, which require you to add an entry to your host file and add a virtual host conf file for Apache. After only going through this process twice, I was already tired of it. I wrote the following script to automate the process for me.

#!/usr/bin/ruby

########################################
##### VARIABLES YOU NEED TO CHANGE #####
########################################

host_dir  = '/etc/hosts/index.html'            # path to your hosts file
sites_dir = '/Library/Webserver/index.html'    # path to the directory where you keep your sites (NO TRAILING SLASH!!!)
conf_dir  = '/etc/apache2/sites/index.html'    # path to directory where named conf files live

########################################

unless ARGV[0]
  puts "Usage: add_site sitename [hostname_for_url]"
  puts "Example: add_site sample sample.dev"
  exit
end

name = ARGV[0].strip
hostname = ARGV[1].nil? ? ARGV[0] : ARGV[1].strip

# first things first: make sure named conf file doesn't exist already
if File.exists?("#{conf_dir}/#{name}.conf")
  puts "Conf file named #{name}.conf already exists!"
  exit
end

# check to make sure host file exists
if File.exists?(host_dir)
  
  puts "Adding entry to #{host_dir}."
  File.open(host_dir, 'a') do |host_file|
    # append host entry to end of file
    host_file.puts "127.0.0.1\t#{hostname}"
  end
  puts "Host entry added!"
  
  puts "Adding named conf file."
  File.open("#{conf_dir}/#{name}.conf", 'a') do |host_file|
    # add entry
    host_file.puts <<EOF
<VirtualHost *:80>
  ServerName #{hostname}

  DocumentRoot "#{sites_dir}/#{name}"
	DirectoryIndex index.php
	<Directory "#{sites_dir}/#{name}">
		Options FollowSymLinks MultiViews Includes
		AllowOverride All
		Order allow,deny
		Allow from all
	</Directory>
</VirtualHost>
EOF
  end
  puts "Conf entry added!\n"
  
  puts "Restarting apache.\n"
  system "apachectl graceful"
  puts "Done!"
  
end

What you need to do to get this to work:

  1. Change the variables at the top of the file (path to your hosts file, path to the folder where you keep your development site, and path to the directory where you want to keep your named conf files).
  2. Rename the file to add_site(with no extension) and move to your /usr/bin directory.
  3. Chmod the file to be executable.

Now you can run the add_site command and provide it with the name of the site folder and optionally the name of the local domain you'd like to use.

Tagged: rubyscriptapache

CentOS Setup Script with Ruby, Apache, mySQL, Subverion, Git, Passenger, and ImageMagick

Aug 31, 2009  -  Comments

Recently, I participated in the Rails Rumble competition. If you've never heard of the Rails Rumble, it's a 48-hour coding contest where teams of 4 programmers/designers have to complete a fully-working application over the course of a weekend.

Linode, one of the sponsors, provided each team with virtual private server to host their application. This was great, but the servers were bare-bones and required a fair amount of setup. Knowing this in advance, I hoped to save some time by writing a script that would automate the process for our team so I didn't have to spend 30-45 minutes just trying to configure the server.

Here are the steps I took to get Ruby, Apache, mySQL, Subversion, Git, Passenger, and ImageMagick set up on our CentOS server.

Step 1: Add custom repository listing

The default CentOS repo only has Ruby 1.8.6. While there's nothing wrong with this, I wanted to get 1.8.7. To do that, copy the following text into a file called /etc/yum.repos.d/rubyworks.repo:

[rubyworks]
name=RubyWorks
baseurl=http://rubyworks.rubyforge.org/redhat/$releasever/RPMS/$basearch
enabled=1
gpgcheck=1
gpgkey=http://rubyworks.rubyforge.org/RubyWorks.GPG.key
priority=1

Step 2: Bash script

The only other step is to copy the following bash script to a file (called anything you'd like), chmod it to be executable, and just run it as root (very important!). It's magic!

#!/bin/bash

# You must run as sudo!!!!!

# update package list
yum update -y

# get all the packages required for compiling source
yum groupinstall 'Development Tools' -y

# install apache
yum install httpd mod_ssl -y

# install mySQL
yum --enablerepo=rubyworks install mysql-server mysql mysql-devel mysql-ruby -y

# set mysql and apache to start on boot
/sbin/chkconfig httpd on
/sbin/chkconfig --add mysqld
/sbin/chkconfig mysqld on
/sbin/service httpd start
/sbin/service mysqld start

# install postfix and subversion
yum install postfix subversion -y

# install git and dependencies
yum install gettext-devel expat-devel curl-devel zlib-devel openssl-devel -y
wget http://kernel.org/pub/software/scm/git/git-1.6.4.2.tar.gz
tar zxvf git-1.6.4.2.tar.gz
cd git-1.6.4.2
make prefix=/usr/local all 
sudo make prefix=/usr/local install
cd ..

# install ruby
yum install --enablerepo=rubyworks ruby ruby-devel ruby-irb ruby-rdoc ruby-ri

# install ImageMagick and all of it's required packages
yum install tcl-devel libpng-devel libjpeg-devel ghostscript-devel bzip2-devel freetype-devel libtiff-devel -y
wget ftp://ftp.imagemagick.org/pub/ImageMagick/ImageMagick.tar.gz
tar zxvf ImageMagick.tar.gz
cd ImageMagick-*
./configure --prefix=/usr --with-bzlib=yes --with-fontconfig=yes --with-freetype=yes --with-gslib=yes --with-gvc=yes --with-jpeg=yes --with-jp2=yes --with-png=yes --with-tiff=yes
make clean
make
make install
cd ..

# get rubygems
wget http://rubyforge.org/frs/download.php/57643/rubygems-1.3.4.tgz
tar xvzf rubygems-1.3.4.tgz
cd rubygems-1.3.4
ruby setup.rb
cd ..

# install rails
gem install rails --no-rdoc --no-ri

# install rmagick
gem install rmagick --no-rdoc --no-ri

# install mysql gem
gem install mysql --with-mysql-config=/usr/bin/mysql_config --no-ri --no-rdoc

# install passenger and apache module
gem install passenger --no-ri --no-rdoc
passenger-install-apache2-module

DISCLAIMER: This worked great for me on a Linode 360 running CentOS 5.2. If you have any additions or changes, leave them in the comments and I'll be sure to add them. You can also get the entire script in a gist here.

Tagged: centosscript

Rails Subdomain Caching

Oct 02, 2009  -  Comments

A while ago, I was working on a site that had dynamic subdomains based on cities in the database. For example, if an admin created a record for Nashville, it would create http://nashville.sitename.com. The site was pretty content heavy, but didn't change a whole lot, so I wanted to cache as much as I could. The problem with caching was that the content of each page depended on the subdomain. I couldn't use the normal caching strategy because of this.

To handle the subdomain routing and identification, I used the awesome subdomain-fu plugin (which worked great). Subdomain-fu, however, does not do the work of putting your cached files into folders named for your subdomain. Luckily, fixing this was as easy as adding a before filter to my ApplicationController.

class ApplicationController < ActionController::Base
  before_filter :update_cache_location
  
  def update_cache_location
    if current_subdomain.nil?
      ActionController::Base.page_cache_directory = "#{RAILS_ROOT}/public/cache/"
    else
      ActionController::Base.page_cache_directory = "#{RAILS_ROOT}/public/cache/#{current_subdomain}/"
    end
  end
end

That method will update the page_cache_directory value to be your current subdomain (or default if you are on the main domain). Now, this takes care of one problem, but how the hell do we retrieve the cached files? I had a little bit of trouble with this, mainly because I'm not a mod_rewrite specialist. After some tinkering and testing, I finally came up with the following rules to put in my conf file to properly retrieve the cached files based on the subdomain.

# Check for subdomain cached index
RewriteCond %{HTTP_HOST} !^www\.sitename\.com
RewriteCond %{HTTP_HOST} ^([^.]+)\.sitename\.com
RewriteRule ^/$ /cache/%1/index.html [QSA]

# Check for subdomain cached page
RewriteCond %{HTTP_HOST} !^www\.sitename\.com
RewriteCond %{HTTP_HOST} ^([^.]+)\.sitename\.com
RewriteRule ^([^.]+)$ /cache/%1/$1.html [QSA]

# Check for regular non-subdomain index
RewriteRule ^/$ /cache/index.html [QSA] 

# Check for regular non-subdomain page
RewriteRule ^([^.]+)$ /cache/$1.html [QSA]

Those first two entries check to make sure the subdomain isn't www, then checks the cache folder for a cached version of the page. The last two entries check for the regular www subdomain.

These entries worked great for my project, but, like I said, I'm no expert with mod_rewrite. If you see anything that can be simplified or a better way of doing something, please let me know in the comments.

Tagged: railscachingtutorial