Professional Documents
Culture Documents
Real Life Rails: .CLDC - Version" Value "1.0"
Real Life Rails: .CLDC - Version" Value "1.0"
.cldc.version" value="1.0"/>
an Developer eBook
contents
[ ] Real Life Rails
20
1
[ Real Life Rails ]
T
he Ruby language's conciseness makes it great for
development—fewer lines of code means reduced Rails development (in particular, creating a Rails appli-
development time and maintenance costs. Rails, the cation and using the Rails command-line tools) and ver-
Ruby-based Web application framework, can be equally sion control (in particular, installing and using the com-
developer-friendly, but some developers have encountered mand-line SVN tools — see the sidebar. It also assumes
scaling limitations when deploying large-scale Rails Web you know how to set up a Linux server for production
applications. While scaling Rails (with Ruby, Gem, and Rails
applications over many servers to installed). It will not provide
handle many thousands of concur- instructions for setting these up.
rent users can indeed lead to prob- To get the most out of this article,
lems, with the proper tools and you will need the following tech-
techniques developers can deploy nologies in addition to Rails and
small- and medium-scale Rails your Linux server: NetBeans 6.0,
applications effectively. the nginx HTTP and proxy server,
the Mongrel server, and the
I'm going to explain how to do memcached system.
just that using my preferred Ruby
and Rails development IDE Leveraging NetBeans
NetBeans 6.0, some open source
server and caching systems, and
6.0 Ruby and Rails
the typical Rails application I work Support
with: running on a single server The first step toward using
and needing to support only 100 Jupiterimages
NetBeans 6.0 for your Ruby and
or so concurrent users. Let's start Rails development is installing
by walking you through how to set up NetBeans 6.0 for NetBeans and the Ruby plugins. Download the Ruby
Ruby and Rails development, and then moves on to NetBeans version 6.0 bundle I use at
techniques for deploying Rails applications efficiently. http://download.netbeans.org/netbeans/6.0/final/. Add
“
Scaling Rails applications over many servers to handle many thousands
of concurrent users can indeed lead to problems.
config.action_controller.fragment_cache_store = :mem_cache_store
config.action_controller.session_store = :mem_cache_store
Next, I create a deployment directory, create the account, and set ownership of the deployment directory:
Now, I use SSH to remotely login to my server using the account cookingspace and pull the latest version of my
Web application from my subversion server:
cd /var/mongrel/cookingspace/
svn co svn+ssh://MY_ACCOUNT@MY_SERVER.com/home/svn/svn-repos/cookingspace .
I am using MySQL for my Web application so, on my Linux server, I need to set up the databases:
Before going any further, it is a good idea to test the Web application running a single Mongrel, but in production
mode:
cd /var/mongrel/cookingspace/
script/server --environment=production
When logged in as user cookingspace, configure three Mongrels and start and stop the cluster to test it:
cd /var/mongrel/cookingspace/
mongrel_rails cluster::configure -e production -p 3001 -n 3 -a 127.0.0.1 --user cookingspace --group cookingspace
mongrel_rails cluster::start
mongrel_rails cluster::stop
cd nginx-0.5.35
./configure
make
sudo make install
This installs nginx in /usr/local/nginx, and you need to edit the file /usr/local/nginx/conf/nginx.conf to look like this:
worker_processes 3;
pid logs/nginx.pid;
for Version Control
While NetBeans supports Subversion (svn), I pre-
events { fer to use the command line svn tools because I
worker_connections 1024; am so used to running svn on remote servers in a
} SSH shell. I strongly recommend that you start
using svn immediately after using the rails com-
http { mand to create an empty Rails project. After per-
include conf/mime.types; forming an svn import on your new empty Rails
default_type application/octet-stream; project, do a fresh checkout of your project and
tcp_nopush on;
let svn know what to ignore:
keepalive_timeout 65; svn propset svn:ignore "*.log" log
tcp_nodelay on; svn propset svn:ignore "schema.rb" db
svn revert public/index.html
upstream mongrel { rm public/index.html
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
For large teams, you should set up Subversion
repositories to use separate directories for trunk,
}
tags, and branches. If you are deploying to an
inexpensive virtual private server (VPS), I assume
gzip on;
that your Rails project is small- or medium-scale
gzip_min_length 1100; with just one or two developers. In these cases,
gzip_buffers 4 8k; simply develop and deploy from trunk.
server {
listen 80;
server_name cookingspace.com;
root /var/mongrel/cookingspace/public;
access_log off;
rewrite_log on;
location / {
proxy_pass http://mongrel;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
To test, make sure that your Mongrel cluster has started and run nginx manually:
cd /usr/local/nginx/
sbin/nginx -c conf/nginx.conf
This is a little simplistic, but it has the advantage of likely working on most systems.
T
he Ruby language and the Ruby on Rails (Rails for
short) Web application framework have created a lot need to have three core pieces of software installed:
of buzz in the software programming industry and for
good reason. While Ruby has been around for more than a • Ruby interpreter
decade, the Rails framework is relatively new but it is • RubyGems packaging system
becoming a darling of many programmers who are tired of • Rails framework
the overly complicated world of
Web frameworks. In addition, you will need a
Ruby gem named RedCloth as
This article demonstrates just the text markup engine. The
how easy it is to write a simple following is a list of the software
Wiki application using Ruby and versions used for this article
and Rails together. While Rails (note: this software must be
is used mostly to build data- installed in the order shown):
base applications, this article
goes the file-based persistence 1. Ruby interpreter (I used ver-
route because I personally like sion 1.8.5.)
using Wiki systems that are file 2. RubyGems packaging system
based and coincidentally they (I used version 0.9.2.)
are easy to set up. In addition, 3. Gems:
very few resources on the Web • Ruby on Rails (rails-1.2.2)
discuss developing non-data- Alternatively, you can simply
base applications using Rails. Jupiterimages type gem install rails -- include-
Still, switching to a database using the Rails database dependencies.
support should be relatively painless if you choose to • RedCloth textile markup language - (3.0.4) using
go that route. the gem install redcloth command.
“
While Rails is used mostly to build database applications, this article
goes the file-based persistence route because I personally like using Wiki
systems that are file based and coincidentally they are easy to set up.
1. ruby --help
2. gem --help (or better yet, gem list --local)
3. rails –help
One of the beautiful things about Rails is that it gener- The generate command will create several files, some
ates a lot of code, directories, and files in order to give of which include the following:
you a jumpstart for developing a new Web application.
In order to generate files for RailsWiki, the very first • app/controllers/wiki_controller.rb
command you will type is the rails command from the • app/views/layouts/application.rhtml
top-level directory, under which you want your applica- • app/views/wiki/edit.rhtml
tion's parent and sub-directories created (for example, • app/views/wiki/help.rhtml
/users/anil/dev/ or c:\anil\dev), as shown here: • app/views/wiki/index.rhtml
• app/views/wiki/print.rhtml
> rails railswiki • app/views/wiki/view.rhtml
If the rails command worked properly, you will see a At this point, you already have a working Web applica-
bunch of directories and files created for you. Some of tion. To test your stub application, start the bundled
the directories you will work with in this article include WEBrick HTTP server by typing the following com-
the following: mand:
Since Rails is typically used to develop database- • The find method reads in an entire Wiki file and
backed Web applications, most developers working returns it as a string to the caller. (While find as a
with such applications typically use the script/generate method name might sound a bit misleading since it
command to generate the model and related files. reads in a file, I chose this name to stay consistent
However, since this example uses file-based persist- with the Rails naming convention. By following similar
ence, you will write your model class from scratch (see naming conventions, you should be able to switch to
Listing 1) while still following as many of the Rails con- a database-based persistence model easily using the
ventions as possible. For example, you will place your Rails model support.)
model class, Wiki, in the app/models directory (as
app/models/wiki.rb) where Rails would normally place • The find_wikis method returns an array of file
generated model files. Furthermore, the method nam- names found in basedir ending with the value of the
ing conventions are similar to those found in the class variable @@extension, which in this case hap-
ActiveRecord::Base class provided by Rails. Listing 1 pens to default to ".Wiki".
shows the complete Wiki model class.
• The save method simply opens a file for writing and
A few of the notable methods in this class include writes the entire contents of the method parameter
basedir, find, find_wikis, and save, so let's take a closer content to it.
look at these methods:
The other notable public methods include delete,
• The basedir method allows you to set a static direc- exists?, and attributes. The delete method deletes a
tory name where all the Wiki files will be stored. If Wiki file, the exists? method indicates whether a Wiki
this method is not called, then the current directory is file exists or not, and the attributes method provides
used by default as specified in the class variable attributes of a file (such as modified date and file size).
class Wiki
@@basedir = "."
@@extension = "wiki"
def self.basedir(basedir)
@@basedir = basedir
end
# Test delete
Wiki.delete @filename
# Test exists?
assert !Wiki.exists?(@filename)
You should be aware of a couple of problems—and their corresponding solutions—related to testing database-less
rails applications. You typically would unit test the model classes by typing the command rake test:units (rake
test_units in previous versions of rails) from the top-level directory of a Rails application. However, since Rails tries
to connect to the database by default and the RailsWiki application isn't using a database, you need to make two
def self.attributes(basefilename)
File.stat(getfullpath(basefilename))
end
def self.file_extension
@@extension
end
First, you need to include a file under lib/tasks ending with the extension .rake. Accordingly, in the downloadable
source code for this article, you will find a file named testing.rake. This file must contain the following line of Ruby
code:
Rake::Task[:'test:units'].prerequisites.clear
Secondly, you need to fix the test/test_helper.rb file by replacing its code with the following:
ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) \
+ "/../config/environment")
require 'application'
require 'test/unit'
require 'action_controller/test_process'
require 'breakpoint'
Now running the command rake test:units will generate output similar to this:
Started
.
Finished in 0.094 seconds.
file.close
end
end
getline
end
Recall that when you ran the Rails script/generate controller command earlier, it generated a few stub files.
However, I added code to the generated methods and also added some additional methods that are not tied to
any view but serve as specific actions used by other views. The list of public methods in WikiController includes:
initialize, index, edit, view, print, help, create, delete, and save. Let's take a closer look at some of these methods:
• The initialize method sets the name of the directory where your Wiki files will be stored. This name is config-
ured in the config/environment.rb file via the WIKI_DIR property—this is the only configuration you will need to
change to store your Wiki files under a more appropriate directory, if needed:
WIKI_DIR = "/tmp/railswiki"
• The index method calls Wiki.find_wikis to generate a list of existing Wiki file names and passes it onto the
index.rhtml view using the instance variable @filelist. This view loops through the array and populates a HTML
<SELECT> drop down box:
<select name="f">
<% @filelist.each do |file| %>
<option><%= file %></option>
<% end %>
</select>
files
end
private
def self.mkdir()
Dir.mkdir(@@basedir) if !File.exist?(@@basedir)
end
def self.getfullpath(basefilename)
@@basedir + "/" + getfullname(basefilename)
end
def self.getfullname(basefilename)
basefilename + "." + @@extension
end
end
def get_content
begin
@filename = get_filename params[:f]
@filestat = Wiki.attributes @filename
@content = Wiki.find @filename
rescue Errno::ENOENT => exception
flash[:error] = exception
redirect_to :action => :index
end
end
Apart from reading in the contents and attributes of the requested file, the get_content method also redirects the
page to your index page, along with an error message in case of an exception. The error message is set using the
Rails flash feature (essentially, a hash containing application errors).
def initialize
Wiki.basedir WIKI_DIR
end
def index
@filelist = Wiki.find_wikis
end
def edit
get_content
end
def view
get_content
end
def print
get_content
end
def help
@filename = get_filename params[:f] # need this for other actions continued
The to_html helper method can be found in the app/helpers/wiki_helper.rb file, a Rails-generated helper module
(helper modules are a nice way to make methods available to your views). This is also where the gem named
RedCloth, discussed earlier, comes into play since it does the leg work of converting the raw Wiki file text to
HTML, as shown in this code contained in our wiki_helper.rb file:
require 'redcloth'
module WikiHelper
# parse and return data as HTML
def to_html(rawtext)
return "" if rawtext.nil?
r = RedCloth.new rawtext
r.to_html
end
end
The one other pre-generated file, which I have tweaked slightly, is config/routes.rb. By modifying one of the map-
pings and deleting the generated public/index.html file, you can access the index page without the trailing URI
(that is, /wiki/):
private
def get_filename(f)
f = "untitled" if (f.nil? || f.empty?)
f
end
# load file into @content using param :f; for errors go to index page
def get_content
begin
@filename = get_filename params[:f]
@filestat = Wiki.attributes @filename
@content = Wiki.find @filename
rescue Errno::ENOENT => exception
flash[:error] = exception
redirect_to :action => :index
end
end
end
For example, with the above line added, a URL such as http://localhost:3000/wiki/view?f=untitled would look like
http://localhost:3000/wiki/view/untitled. This sort of URL formatting is something the Rails helper methods (such as
link_to, redirect_to, url_for, and others) automatically handle for you.
What Next?
You have successfully built a completely functional Wiki system, but you obviously can add to and enhance this
basic system. If you are interested in learning more Rails or simply building upon the downloadable application,
here are a couple of suggestions:
1. Leverage the strengths of Rails and have your application persist its Wiki pages in a database (perhaps using a
text data type for the actual Wiki text).
2. Add search capabilities to the application using an existing Ruby gem that provides indexing and searching
capabilities. For example, one such gem named Ferret can be found at ferret.davebalmain.com/trac.
3. Add support for commonly found Wiki features such as WikiWord, change history, and so on.
Apart from that, have fun coding with Ruby and Rails! I
O
ne of the core advantages of wiki systems is the
ability to edit Web pages stored on a server very application for a couple of months now to manage
through a Web browser. However, this function also a dozen or so encrypted wiki pages. So this system has
allows administrators to look at the contents of your wiki practical, real world applications.
pages, particularly if your wiki uses a shared server. If you
store confidential information on Changes to the
that server, this is an especially
undesirable compromise. A sim-
Views
ple solution to this dilemma is to Adding authentication and
store the Web pages in an encryption to the RailsWiki
encrypted form and use application requires some
authentication to access them changes in the client and serv-
in a readable form. er-side code. For the client
side, you need to add a logon
Building on the wiki file-man- screen as shown in Figure 2.
agement system created in
the previous article, we're Notice that the logon screen
now going to demonstrate contains not only a user name
how to add authentication and password but also an
and encryption security fea- encryption key. (You will see
tures to the system (see the how to use this key to encrypt
sidebar for an overview of data later.) In addition to the
Jupiterimages
these security concepts). This new logon screen, you also
simple system, called RailsWiki, offers basic Web page need to modify the old "Home" screen by adding a
management features such as view, edit, print, and so checkbox for the user to choose whether a new file
on (Figure 1 presents its basic design). should be encrypted or not. Figure 3 reflects this
change.
“
Building on the wiki file-management system created in the previous
article, we're now going to demonstrate how to add authentication and
encryption security features to the system
Figure 2
WikiUser
WikiUser essentially provides authentication-related
services by leveraging the Ruby Crypt library. Based on
the descriptions of the various block ciphers supported
by this library, I chose to use Blowfish.
def create_account(plain_password)
blowfish = Crypt::Blowfish.new(PASSWORD_KEY)
f = open_file "w"
f.print(blowfish.encrypt_string(plain_password))
f.close
end
The remaining methods in the WikiUser class essentially provide additional authentication services such as validat-
ing an existing password, determining if the user account already exists, and so on.
WikiDocument
The WikiDocument class is a bit more involved. It provides file-management services such as opening, saving, and
deleting wiki files, getting a list of wiki file names in the user's directory, etc. Let's review the methods you need to
add and change from the original RailsWiki application.
The other notable method in the WikiDocument class is find_encrypted. The key difference between this method
and the find method that originally was part of the Wiki class is the extra step of decrypting the encrypted text
using the decrypt_string method in the Crypt::Blowfish class, as shown in the following code excerpt:
content = self.find(path)
return "" if content.nil?
blowfish = Crypt::Blowfish.new(key)
blowfish.decrypt_string content
end
1. Authentication is the process of verifying that a person or software application is who it says it is. This is com-
monly done using the combination of a logon user ID and a password.
2. Authorization generally follows authentication. After the person or application has been verified, authorization
determines which role the user fits into and accordingly what the user is permitted to do in the software appli-
cation.
3. Encryption is the process of converting data to a form that cannot be understood by unauthorized people or
programs.
wu = WikiUser.new user
wu.create_account pass
flash[:error] = "Account created; you may login now."
The login method authenticates the user using the WikiUser model class. If the user's credentials are valid, the sys-
tem establishes a session for the user and forwards him or her to the home view. Otherwise, it sends the user back
to the index view with an error message. The following code excerpt demonstrates all of this:
unless wikiuser.valid_password?(pass)
flash[:error] = "Invalid password."
redirect_to :action => :index
else
session[:user] = wikiuser
redirect_to :action => :home, :f => DEFAULT_FILENAME
end
@filelist = []
list1 = WikiDocument.find_wikis(get_basedir, PLAIN_EXTENSION)
list2 = WikiDocument.find_wikis(get_basedir, ENCRYPTED_EXTENSION)
@filelist.concat(list1) unless list1.nil?
@filelist.concat(list2) unless list2.nil?
The create method creates a blank wiki file with the appropriate extension, as shown here:
The save method calls the internal/private method save_document, which in turn determines the appropriate save
method to call on the WikiDocument model class based on whether encryption is required or not, as demonstrat-
ed here:
f = open_file "r"
encrypted_password = f.gets(nil)
f.close
blowfish = Crypt::Blowfish.new(PASSWORD_KEY)
compare_password == blowfish.decrypt_string(encrypted_password)
end
def self.exists?(user)
File.exists?(WIKI_DIR + "/" + user + "/" + PASSWORD_FILE)
end
:private
def open_file(mode)
basedir = WIKI_DIR + "/" + @user
begin
Dir.mkdir basedir
rescue
end
File.open(basedir + "/" + PASSWORD_FILE, mode)
end
end
Once a wiki file has been created, these files are read using the private method get_content, which determines
whether the file needs to be decrypted or not, as shown here:
content = self.find(path)
return "" if content.nil?
blowfish = Crypt::Blowfish.new(key)
blowfish.decrypt_string content
end
if (extension.nil? || extension.empty?)
Find.find(path) do |f|
if FileTest.directory?(f) && (f != path)
Find.prune # Don't enter sub-directories
else
files << File.basename(f)
end
end
else
Find.find(path) do |f| continued
Configuration File
The other file worth inspecting is the config/environment.rb file, since it contains various default settings for
RailsWiki, as shown here:
WIKI_DIR = "/users"
PLAIN_EXTENSION = ".wiki"
ENCRYPTED_EXTENSION = ".swiki"
DEFAULT_FILENAME = "untitled" + PLAIN_EXTENSION
PASSWORD_KEY = "WikiUserKey"
PASSWORD_FILE = ".railswiki_pass"
PATH_SEPARATOR = "/"
files
end
private
def self.mkdir(path)
dir = File.dirname(path)
Dir.mkdir(dir) if !File.exists?(dir)
end
end
• Add role-based security (that is, authorization) This content was adapted from Internet.com's DevX
• Add email alerts for notifying others when changes Web site. Contributors: Mark Watson and Anil
to a file occur Hemrajani .
if (user.nil? || user.empty?)
flash[:error] = "User is required."
elsif (WikiUser.exists? user)
flash[:error] = "Account already exists."
elsif (pass.nil? || pass.empty?)
flash[:error] = "Password is required."
else
wu = WikiUser.new user
wu.create_account pass
flash[:error] = "Account created; you may login now."
end
@filelist = []
list1 = WikiDocument.find_wikis(get_basedir, PLAIN_EXTENSION)
list2 = WikiDocument.find_wikis(get_basedir, ENCRYPTED_EXTENSION)
@filelist.concat(list1) unless list1.nil?
@filelist.concat(list2) unless list2.nil?
get_content
end
extname = File.extname(@filename)
end
private
# Save a file in either encrypted or plain form
def save_document(filename, content, encrypt)
if (encrypt)
WikiDocument.save_encrypted((get_fullpath filename, false), content,
(get_session_object).key)
else
WikiDocument.save((get_fullpath filename, false), content)
end
end
def authenticate
begin
wikiuser = get_session_object
raise "no user found in session" if wikiuser.nil?
user = wikiuser.user
raise "no user found in session" if (user.nil? || user.empty?)
rescue => exception
flash[:error] = exception
redirect_to :action => :index
end
end
def get_filename(f)
f = DEFAULT_FILENAME if (f.nil? || f.empty?)
f
end
end
def get_basedir
WIKI_DIR + PATH_SEPARATOR + (get_session_object).user
end
# Load file into @content using param :f; for errors go to index page
def get_content
begin
@filename = get_filename params[:f]
if @filename == DEFAULT_FILENAME
begin
save_document(@filename, "", false) unless
WikiDocument.exists?(get_fullpath(@filename))
rescue
end
end
@filestat = WikiDocument.attributes(get_fullpath(@filename))
extname = File.extname(@filename)
if (extname == ENCRYPTED_EXTENSION)
@content = WikiDocument.find_encrypted(get_fullpath(@filename),
(get_session_object).key)
else
@content = WikiDocument.find(get_fullpath(@filename))
end
rescue Errno::ENOENT => exception
flash[:error] = exception
redirect_to :action => :home
rescue RuntimeError => exception
flash[:error] = exception
redirect_to :action => :home
end
end
end