Updating to use Rails 2.1.2. Moving the gem dependancies to the config/environment.rb file. Moving the vendor/plugins externals into our svn.

This commit is contained in:
Shaun McDonald 2008-10-28 20:42:48 +00:00
parent 38f4e17865
commit 252c2f7022
46 changed files with 3919 additions and 7 deletions

View file

@ -5,7 +5,7 @@
ENV['RAILS_ENV'] ||= 'production'
# Specifies gem version of Rails to use when vendor/rails is not present
RAILS_GEM_VERSION = '2.0.2' unless defined? RAILS_GEM_VERSION
RAILS_GEM_VERSION = '2.1.2' unless defined? RAILS_GEM_VERSION
# Set the server URL
SERVER_URL = ENV['OSM_SERVER_URL'] || 'www.openstreetmap.org'
@ -40,6 +40,16 @@ Rails::Initializer.run do |config|
config.frameworks -= [ :active_record ]
end
# Specify gems that this application depends on.
# They can then be installed with "rake gems:install" on new installations.
# config.gem "bj"
# config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net"
# config.gem "aws-s3", :lib => "aws/s3"
config.gem 'composite_primary_keys', :version => '1.0.10'
config.gem 'libxml-ruby', :version => '>= 0.8.3', :lib => 'libxml'
config.gem 'rmagick', :lib => 'RMagick'
config.gem 'mysql'
# Only load the plugins named here, in the order given. By default, all plugins
# in vendor/plugins are loaded in alphabetical order.
# :all can be used as a placeholder for all plugins not explicitly named

View file

@ -1,3 +0,0 @@
require 'rubygems'
gem 'composite_primary_keys', '= 0.9.93'
require 'composite_primary_keys'

View file

@ -1,7 +1,8 @@
require 'rubygems'
gem 'libxml-ruby', '>= 0.8.3'
require 'libxml'
#require 'rubygems'
#gem 'libxml-ruby', '>= 0.8.3'
#require 'libxml'
# Is this really needed?
LibXML::XML::Parser.register_error_handler do |message|
raise message
end

10
vendor/plugins/deadlock_retry/README vendored Normal file
View file

@ -0,0 +1,10 @@
Deadlock Retry
==============
Deadlock retry allows the database adapter (currently only tested with the
MySQLAdapter) to retry transactions that fall into deadlock. It will retry
such transactions three times before finally failing.
This capability is automatically added to ActiveRecord. No code changes or otherwise are required.
Copyright (c) 2005 Jamis Buck, released under the MIT license

10
vendor/plugins/deadlock_retry/Rakefile vendored Normal file
View file

@ -0,0 +1,10 @@
require 'rake'
require 'rake/testtask'
desc "Default task"
task :default => [ :test ]
Rake::TestTask.new do |t|
t.test_files = Dir["test/**/*_test.rb"]
t.verbose = true
end

2
vendor/plugins/deadlock_retry/init.rb vendored Normal file
View file

@ -0,0 +1,2 @@
require 'deadlock_retry'
ActiveRecord::Base.send :include, DeadlockRetry

View file

@ -0,0 +1,58 @@
# Copyright (c) 2005 Jamis Buck
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module DeadlockRetry
def self.append_features(base)
super
base.extend(ClassMethods)
base.class_eval do
class <<self
alias_method :transaction_without_deadlock_handling, :transaction
alias_method :transaction, :transaction_with_deadlock_handling
end
end
end
module ClassMethods
DEADLOCK_ERROR_MESSAGES = [
"Deadlock found when trying to get lock",
"Lock wait timeout exceeded"
]
MAXIMUM_RETRIES_ON_DEADLOCK = 3
def transaction_with_deadlock_handling(*objects, &block)
retry_count = 0
begin
transaction_without_deadlock_handling(*objects, &block)
rescue ActiveRecord::StatementInvalid => error
if DEADLOCK_ERROR_MESSAGES.any? { |msg| error.message =~ /#{Regexp.escape(msg)}/ }
raise if retry_count >= MAXIMUM_RETRIES_ON_DEADLOCK
retry_count += 1
logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
retry
else
raise
end
end
end
end
end

View file

@ -0,0 +1,65 @@
begin
require 'active_record'
rescue LoadError
if ENV['ACTIVERECORD_PATH'].nil?
abort <<MSG
Please set the ACTIVERECORD_PATH environment variable to the directory
containing the active_record.rb file.
MSG
else
$LOAD_PATH.unshift << ENV['ACTIVERECORD_PATH']
begin
require 'active_record'
rescue LoadError
abort "ActiveRecord could not be found."
end
end
end
require 'test/unit'
require "#{File.dirname(__FILE__)}/../lib/deadlock_retry"
class MockModel
def self.transaction(*objects, &block)
block.call
end
def self.logger
@logger ||= Logger.new(nil)
end
include DeadlockRetry
end
class DeadlockRetryTest < Test::Unit::TestCase
DEADLOCK_ERROR = "MySQL::Error: Deadlock found when trying to get lock"
TIMEOUT_ERROR = "MySQL::Error: Lock wait timeout exceeded"
def test_no_errors
assert_equal :success, MockModel.transaction { :success }
end
def test_no_errors_with_deadlock
errors = [ DEADLOCK_ERROR ] * 3
assert_equal :success, MockModel.transaction { raise ActiveRecord::StatementInvalid, errors.shift unless errors.empty?; :success }
assert errors.empty?
end
def test_no_errors_with_lock_timeout
errors = [ TIMEOUT_ERROR ] * 3
assert_equal :success, MockModel.transaction { raise ActiveRecord::StatementInvalid, errors.shift unless errors.empty?; :success }
assert errors.empty?
end
def test_error_if_limit_exceeded
assert_raise(ActiveRecord::StatementInvalid) do
MockModel.transaction { raise ActiveRecord::StatementInvalid, DEADLOCK_ERROR }
end
end
def test_error_if_unrecognized_error
assert_raise(ActiveRecord::StatementInvalid) do
MockModel.transaction { raise ActiveRecord::StatementInvalid, "Something else" }
end
end
end

69
vendor/plugins/file_column/CHANGELOG vendored Normal file
View file

@ -0,0 +1,69 @@
*svn*
* allow for directories in file_column dirs as well
* use subdirs for versions instead of fiddling with filename
* url_for_image_column_helper for dynamic resizing of images from views
* new "crop" feature [Sean Treadway]
* url_for_file_column helper: do not require model objects to be stored in
instance variables
* allow more fined-grained control over :store_dir via callback
methods [Gerret Apelt]
* allow assignment of regular file objects
* validation of file format and file size [Kyle Maxwell]
* validation of image dimensions [Lee O'Mara]
* file permissions can be set via :permissions option
* fixed bug that prevents deleting of file via assigning nil if
column is declared as NON NULL on some databases
* don't expand absolute paths. This is necessary for file_column to work
when your rails app is deployed into a sub-directory via a symbolic link
* url_for_*_column will no longer return absolute URLs! Instead, although the
generated URL starts with a slash, it will be relative to your application's
root URL. This is so, because rails' image_tag helper will automatically
convert it to an absolute URL. If you need an absolute URL (e.g., to pass
it to link_to) use url_for_file_column's :absolute => true option.
* added support for file_column enabled unit tests [Manuel Holtgrewe]
* support for custom transformation of images [Frederik Fix]
* allow setting of image attributes (e.g., quality) [Frederik Fix]
* :magick columns can optionally ignore non-images (i.e., do not try to
resize them)
0.3.1
* make object with file_columns serializable
* use normal require for RMagick, so that it works with gem
and custom install as well
0.3
* fixed bug where empty file uploads were not recognized with some browsers
* fixed bug on windows when "file" utility is not present
* added option to disable automatic file extension correction
* Only allow one attribute per call to file_column, so that options only
apply to one argument
* try to detect when people forget to set the form encoding to
'multipart/form-data'
* converted to rails plugin
* easy integration with RMagick
0.2
* complete rewrite using state pattern
* fixed sanitize filename [Michael Raidel]
* fixed bug when no file was uploaded [Michael Raidel]
* try to fix filename extensions [Michael Raidel]
* Feed absolute paths through File.expand_path to make them as simple as possible
* Make file_column_field helper work with auto-ids (e.g., "event[]")
0.1.3
* test cases with more than 1 file_column
* fixed bug when file_column was called with several arguments
* treat empty ("") file_columns as nil
* support for binary files on windows
0.1.2
* better rails integration, so that you do not have to include the modules yourself. You
just have to "require 'rails_file_column'" in your "config/environment.rb"
* Rakefile for testing and packaging
0.1.1 (2005-08-11)
* fixed nasty bug in url_for_file_column that made it unusable on Apache
* prepared for public release
0.1 (2005-08-10)
* initial release

54
vendor/plugins/file_column/README vendored Normal file
View file

@ -0,0 +1,54 @@
FEATURES
========
Let's assume an model class named Entry, where we want to define the "image" column
as a "file_upload" column.
class Entry < ActiveRecord::Base
file_column :image
end
* every entry can have one uploaded file, the filename will be stored in the "image" column
* files will be stored in "public/entry/image/<entry.id>/filename.ext"
* Newly uploaded files will be stored in "public/entry/tmp/<random>/filename.ext" so that
they can be reused in form redisplays (due to validation etc.)
* in a view, "<%= file_column_field 'entry', 'image' %> will create a file upload field as well
as a hidden field to recover files uploaded before in a case of a form redisplay
* in a view, "<%= url_for_file_column 'entry', 'image' %> will create an URL to access the
uploaded file. Note that you need an Entry object in the instance variable @entry for this
to work.
* easy integration with RMagick to resize images and/or create thumb-nails.
USAGE
=====
Just drop the whole directory into your application's "vendor/plugins" directory. Starting
with version 1.0rc of rails, it will be automatically picked for you by rails plugin
mechanism.
DOCUMENTATION
=============
Please look at the rdoc-generated documentation in the "doc" directory.
RUNNING UNITTESTS
=================
There are extensive unittests in the "test" directory. Currently, only MySQL is supported, but
you should be able to easily fix this by looking at "connection.rb". You have to create a
database for the tests and put the connection information into "connection.rb". The schema
for MySQL can be found in "test/fixtures/mysql.sql".
You can run the tests by starting the "*_test.rb" in the directory "test"
BUGS & FEEDBACK
===============
Bug reports (as well as patches) and feedback are very welcome. Please send it to
sebastian.kanthak@muehlheim.de

36
vendor/plugins/file_column/Rakefile vendored Normal file
View file

@ -0,0 +1,36 @@
task :default => [:test]
PKG_NAME = "file-column"
PKG_VERSION = "0.3.1"
PKG_DIR = "release/#{PKG_NAME}-#{PKG_VERSION}"
task :clean do
rm_rf "release"
end
task :setup_directories do
mkpath "release"
end
task :checkout_release => :setup_directories do
rm_rf PKG_DIR
revision = ENV["REVISION"] || "HEAD"
sh "svn export -r #{revision} . #{PKG_DIR}"
end
task :release_docs => :checkout_release do
sh "cd #{PKG_DIR}; rdoc lib"
end
task :package => [:checkout_release, :release_docs] do
sh "cd release; tar czf #{PKG_NAME}-#{PKG_VERSION}.tar.gz #{PKG_NAME}-#{PKG_VERSION}"
end
task :test do
sh "cd test; ruby file_column_test.rb"
sh "cd test; ruby file_column_helper_test.rb"
sh "cd test; ruby magick_test.rb"
sh "cd test; ruby magick_view_only_test.rb"
end

6
vendor/plugins/file_column/TODO vendored Normal file
View file

@ -0,0 +1,6 @@
* document configuration options better
* support setting of permissions
* validation methods for file format/size
* delete stale files from tmp directories
* ensure valid URLs are created even when deployed at sub-path (compute_public_url?)

13
vendor/plugins/file_column/init.rb vendored Normal file
View file

@ -0,0 +1,13 @@
# plugin init file for rails
# this file will be picked up by rails automatically and
# add the file_column extensions to rails
require 'file_column'
require 'file_compat'
require 'file_column_helper'
require 'validations'
require 'test_case'
ActiveRecord::Base.send(:include, FileColumn)
ActionView::Base.send(:include, FileColumnHelper)
ActiveRecord::Base.send(:include, FileColumn::Validations)

View file

@ -0,0 +1,720 @@
require 'fileutils'
require 'tempfile'
require 'magick_file_column'
module FileColumn # :nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
def self.create_state(instance,attr)
filename = instance[attr]
if filename.nil? or filename.empty?
NoUploadedFile.new(instance,attr)
else
PermanentUploadedFile.new(instance,attr)
end
end
def self.init_options(defaults, model, attr)
options = defaults.dup
options[:store_dir] ||= File.join(options[:root_path], model, attr)
unless options[:store_dir].is_a?(Symbol)
options[:tmp_base_dir] ||= File.join(options[:store_dir], "tmp")
end
options[:base_url] ||= options[:web_root] + File.join(model, attr)
[:store_dir, :tmp_base_dir].each do |dir_sym|
if options[dir_sym].is_a?(String) and !File.exists?(options[dir_sym])
FileUtils.mkpath(options[dir_sym])
end
end
options
end
class BaseUploadedFile # :nodoc:
def initialize(instance,attr)
@instance, @attr = instance, attr
@options_method = "#{attr}_options".to_sym
end
def assign(file)
if file.is_a? File
# this did not come in via a CGI request. However,
# assigning files directly may be useful, so we
# make just this file object similar enough to an uploaded
# file that we can handle it.
file.extend FileColumn::FileCompat
end
if file.nil?
delete
else
if file.size == 0
# user did not submit a file, so we
# can simply ignore this
self
else
if file.is_a?(String)
# if file is a non-empty string it is most probably
# the filename and the user forgot to set the encoding
# to multipart/form-data. Since we would raise an exception
# because of the missing "original_filename" method anyways,
# we raise a more meaningful exception rightaway.
raise TypeError.new("Do not know how to handle a string with value '#{file}' that was passed to a file_column. Check if the form's encoding has been set to 'multipart/form-data'.")
end
upload(file)
end
end
end
def just_uploaded?
@just_uploaded
end
def on_save(&blk)
@on_save ||= []
@on_save << Proc.new
end
# the following methods are overriden by sub-classes if needed
def temp_path
nil
end
def absolute_dir
if absolute_path then File.dirname(absolute_path) else nil end
end
def relative_dir
if relative_path then File.dirname(relative_path) else nil end
end
def after_save
@on_save.each { |blk| blk.call } if @on_save
self
end
def after_destroy
end
def options
@instance.send(@options_method)
end
private
def store_dir
if options[:store_dir].is_a? Symbol
raise ArgumentError.new("'#{options[:store_dir]}' is not an instance method of class #{@instance.class.name}") unless @instance.respond_to?(options[:store_dir])
dir = File.join(options[:root_path], @instance.send(options[:store_dir]))
FileUtils.mkpath(dir) unless File.exists?(dir)
dir
else
options[:store_dir]
end
end
def tmp_base_dir
if options[:tmp_base_dir]
options[:tmp_base_dir]
else
dir = File.join(store_dir, "tmp")
FileUtils.mkpath(dir) unless File.exists?(dir)
dir
end
end
def clone_as(klass)
klass.new(@instance, @attr)
end
end
class NoUploadedFile < BaseUploadedFile # :nodoc:
def delete
# we do not have a file so deleting is easy
self
end
def upload(file)
# replace ourselves with a TempUploadedFile
temp = clone_as TempUploadedFile
temp.store_upload(file)
temp
end
def absolute_path(subdir=nil)
nil
end
def relative_path(subdir=nil)
nil
end
def assign_temp(temp_path)
return self if temp_path.nil? or temp_path.empty?
temp = clone_as TempUploadedFile
temp.parse_temp_path temp_path
temp
end
end
class RealUploadedFile < BaseUploadedFile # :nodoc:
def absolute_path(subdir=nil)
if subdir
File.join(@dir, subdir, @filename)
else
File.join(@dir, @filename)
end
end
def relative_path(subdir=nil)
if subdir
File.join(relative_path_prefix, subdir, @filename)
else
File.join(relative_path_prefix, @filename)
end
end
private
# regular expressions to try for identifying extensions
EXT_REGEXPS = [
/^(.+)\.([^.]+\.[^.]+)$/, # matches "something.tar.gz"
/^(.+)\.([^.]+)$/ # matches "something.jpg"
]
def split_extension(filename,fallback=nil)
EXT_REGEXPS.each do |regexp|
if filename =~ regexp
base,ext = $1, $2
return [base, ext] if options[:extensions].include?(ext.downcase)
end
end
if fallback and filename =~ EXT_REGEXPS.last
return [$1, $2]
end
[filename, ""]
end
end
class TempUploadedFile < RealUploadedFile # :nodoc:
def store_upload(file)
@tmp_dir = FileColumn.generate_temp_name
@dir = File.join(tmp_base_dir, @tmp_dir)
FileUtils.mkdir(@dir)
@filename = FileColumn::sanitize_filename(file.original_filename)
local_file_path = File.join(tmp_base_dir,@tmp_dir,@filename)
# stored uploaded file into local_file_path
# If it was a Tempfile object, the temporary file will be
# cleaned up automatically, so we do not have to care for this
if file.respond_to?(:local_path) and file.local_path and File.exists?(file.local_path)
FileUtils.copy_file(file.local_path, local_file_path)
elsif file.respond_to?(:read)
File.open(local_file_path, "wb") { |f| f.write(file.read) }
else
raise ArgumentError.new("Do not know how to handle #{file.inspect}")
end
File.chmod(options[:permissions], local_file_path)
if options[:fix_file_extensions]
# try to determine correct file extension and fix
# if necessary
content_type = get_content_type((file.content_type.chomp if file.content_type))
if content_type and options[:mime_extensions][content_type]
@filename = correct_extension(@filename,options[:mime_extensions][content_type])
end
new_local_file_path = File.join(tmp_base_dir,@tmp_dir,@filename)
File.rename(local_file_path, new_local_file_path) unless new_local_file_path == local_file_path
local_file_path = new_local_file_path
end
@instance[@attr] = @filename
@just_uploaded = true
end
# tries to identify and strip the extension of filename
# if an regular expresion from EXT_REGEXPS matches and the
# downcased extension is a known extension (in options[:extensions])
# we'll strip this extension
def strip_extension(filename)
split_extension(filename).first
end
def correct_extension(filename, ext)
strip_extension(filename) << ".#{ext}"
end
def parse_temp_path(temp_path, instance_options=nil)
raise ArgumentError.new("invalid format of '#{temp_path}'") unless temp_path =~ %r{^((\d+\.)+\d+)/([^/].+)$}
@tmp_dir, @filename = $1, FileColumn.sanitize_filename($3)
@dir = File.join(tmp_base_dir, @tmp_dir)
@instance[@attr] = @filename unless instance_options == :ignore_instance
end
def upload(file)
# store new file
temp = clone_as TempUploadedFile
temp.store_upload(file)
# delete old copy
delete_files
# and return new TempUploadedFile object
temp
end
def delete
delete_files
@instance[@attr] = ""
clone_as NoUploadedFile
end
def assign_temp(temp_path)
return self if temp_path.nil? or temp_path.empty?
# we can ignore this since we've already received a newly uploaded file
# however, we delete the old temporary files
temp = clone_as TempUploadedFile
temp.parse_temp_path(temp_path, :ignore_instance)
temp.delete_files
self
end
def temp_path
File.join(@tmp_dir, @filename)
end
def after_save
super
# we have a newly uploaded image, move it to the correct location
file = clone_as PermanentUploadedFile
file.move_from(File.join(tmp_base_dir, @tmp_dir), @just_uploaded)
# delete temporary files
delete_files
# replace with the new PermanentUploadedFile object
file
end
def delete_files
FileUtils.rm_rf(File.join(tmp_base_dir, @tmp_dir))
end
def get_content_type(fallback=nil)
if options[:file_exec]
begin
content_type = `#{options[:file_exec]} -bi "#{File.join(@dir,@filename)}"`.chomp
content_type = fallback unless $?.success?
content_type.gsub!(/;.+$/,"") if content_type
content_type
rescue
fallback
end
else
fallback
end
end
private
def relative_path_prefix
File.join("tmp", @tmp_dir)
end
end
class PermanentUploadedFile < RealUploadedFile # :nodoc:
def initialize(*args)
super *args
@dir = File.join(store_dir, relative_path_prefix)
@filename = @instance[@attr]
@filename = nil if @filename.empty?
end
def move_from(local_dir, just_uploaded)
# remove old permament dir first
# this creates a short moment, where neither the old nor
# the new files exist but we can't do much about this as
# filesystems aren't transactional.
FileUtils.rm_rf @dir
FileUtils.mv local_dir, @dir
@just_uploaded = just_uploaded
end
def upload(file)
temp = clone_as TempUploadedFile
temp.store_upload(file)
temp
end
def delete
file = clone_as NoUploadedFile
@instance[@attr] = ""
file.on_save { delete_files }
file
end
def assign_temp(temp_path)
return nil if temp_path.nil? or temp_path.empty?
temp = clone_as TempUploadedFile
temp.parse_temp_path(temp_path)
temp
end
def after_destroy
delete_files
end
def delete_files
FileUtils.rm_rf @dir
end
private
def relative_path_prefix
raise RuntimeError.new("Trying to access file_column, but primary key got lost.") if @instance.id.to_s.empty?
@instance.id.to_s
end
end
# The FileColumn module allows you to easily handle file uploads. You can designate
# one or more columns of your model's table as "file columns" like this:
#
# class Entry < ActiveRecord::Base
#
# file_column :image
# end
#
# Now, by default, an uploaded file "test.png" for an entry object with primary key 42 will
# be stored in in "public/entry/image/42/test.png". The filename "test.png" will be stored
# in the record's "image" column. The "entries" table should have a +VARCHAR+ column
# named "image".
#
# The methods of this module are automatically included into <tt>ActiveRecord::Base</tt>
# as class methods, so that you can use them in your models.
#
# == Generated Methods
#
# After calling "<tt>file_column :image</tt>" as in the example above, a number of instance methods
# will automatically be generated, all prefixed by "image":
#
# * <tt>Entry#image=(uploaded_file)</tt>: this will handle a newly uploaded file
# (see below). Note that
# you can simply call your upload field "entry[image]" in your view (or use the
# helper).
# * <tt>Entry#image(subdir=nil)</tt>: This will return an absolute path (as a
# string) to the currently uploaded file
# or nil if no file has been uploaded
# * <tt>Entry#image_relative_path(subdir=nil)</tt>: This will return a path relative to
# this file column's base directory
# as a string or nil if no file has been uploaded. This would be "42/test.png" in the example.
# * <tt>Entry#image_just_uploaded?</tt>: Returns true if a new file has been uploaded to this instance.
# You can use this in your code to perform certain actions (e. g., validation,
# custom post-processing) only on newly uploaded files.
#
# You can access the raw value of the "image" column (which will contain the filename) via the
# <tt>ActiveRecord::Base#attributes</tt> or <tt>ActiveRecord::Base#[]</tt> methods like this:
#
# entry['image'] # e.g."test.png"
#
# == Storage of uploaded files
#
# For a model class +Entry+ and a column +image+, all files will be stored under
# "public/entry/image". A sub-directory named after the primary key of the object will
# be created, so that files can be stored using their real filename. For example, a file
# "test.png" stored in an Entry object with id 42 will be stored in
#
# public/entry/image/42/test.png
#
# Files will be moved to this location in an +after_save+ callback. They will be stored in
# a temporary location previously as explained in the next section.
#
# By default, files will be created with unix permissions of <tt>0644</tt> (i. e., owner has
# read/write access, group and others only have read access). You can customize
# this by passing the desired mode as a <tt>:permissions</tt> options. The value
# you give here is passed directly to <tt>File::chmod</tt>, so on Unix you should
# give some octal value like 0644, for example.
#
# == Handling of form redisplay
#
# Suppose you have a form for creating a new object where the user can upload an image. The form may
# have to be re-displayed because of validation errors. The uploaded file has to be stored somewhere so
# that the user does not have to upload it again. FileColumn will store these in a temporary directory
# (called "tmp" and located under the column's base directory by default) so that it can be moved to
# the final location if the object is successfully created. If the form is never completed, though, you
# can easily remove all the images in this "tmp" directory once per day or so.
#
# So in the example above, the image "test.png" would first be stored in
# "public/entry/image/tmp/<some_random_key>/test.png" and be moved to
# "public/entry/image/<primary_key>/test.png".
#
# This temporary location of newly uploaded files has another advantage when updating objects. If the
# update fails for some reasons (e.g. due to validations), the existing image will not be overwritten, so
# it has a kind of "transactional behaviour".
#
# == Additional Files and Directories
#
# FileColumn allows you to keep more than one file in a directory and will move/delete
# all the files and directories it finds in a model object's directory when necessary.
#
# As a convenience you can access files stored in sub-directories via the +subdir+
# parameter if they have the same filename.
#
# Suppose your uploaded file is named "vancouver.jpg" and you want to create a
# thumb-nail and store it in the "thumb" directory. If you call
# <tt>image("thumb")</tt>, you
# will receive an absolute path for the file "thumb/vancouver.jpg" in the same
# directory "vancouver.jpg" is stored. Look at the documentation of FileColumn::Magick
# for more examples and how to create these thumb-nails automatically.
#
# == File Extensions
#
# FileColumn will try to fix the file extension of uploaded files, so that
# the files are served with the correct mime-type by your web-server. Most
# web-servers are setting the mime-type based on the file's extension. You
# can disable this behaviour by passing the <tt>:fix_file_extensions</tt> option
# with a value of +nil+ to +file_column+.
#
# In order to set the correct extension, FileColumn tries to determine
# the files mime-type first. It then uses the +MIME_EXTENSIONS+ hash to
# choose the corresponding file extension. You can override this hash
# by passing in a <tt>:mime_extensions</tt> option to +file_column+.
#
# The mime-type of the uploaded file is determined with the following steps:
#
# 1. Run the external "file" utility. You can specify the full path to
# the executable in the <tt>:file_exec</tt> option or set this option
# to +nil+ to disable this step
#
# 2. If the file utility couldn't determine the mime-type or the utility was not
# present, the content-type provided by the user's browser is used
# as a fallback.
#
# == Custom Storage Directories
#
# FileColumn's storage location is determined in the following way. All
# files are saved below the so-called "root_path" directory, which defaults to
# "RAILS_ROOT/public". For every file_column, you can set a separte "store_dir"
# option. It defaults to "model_name/attribute_name".
#
# Files will always be stored in sub-directories of the store_dir path. The
# subdirectory is named after the instance's +id+ attribute for a saved model,
# or "tmp/<randomkey>" for unsaved models.
#
# You can specify a custom root_path by setting the <tt>:root_path</tt> option.
#
# You can specify a custom storage_dir by setting the <tt>:storage_dir</tt> option.
#
# For setting a static storage_dir that doesn't change with respect to a particular
# instance, you assign <tt>:storage_dir</tt> a String representing a directory
# as an absolute path.
#
# If you need more fine-grained control over the storage directory, you
# can use the name of a callback-method as a symbol for the
# <tt>:store_dir</tt> option. This method has to be defined as an
# instance method in your model. It will be called without any arguments
# whenever the storage directory for an uploaded file is needed. It should return
# a String representing a directory relativeo to root_path.
#
# Uploaded files for unsaved models objects will be stored in a temporary
# directory. By default this directory will be a "tmp" directory in
# your <tt>:store_dir</tt>. You can override this via the
# <tt>:tmp_base_dir</tt> option.
module ClassMethods
# default mapping of mime-types to file extensions. FileColumn will try to
# rename a file to the correct extension if it detects a known mime-type
MIME_EXTENSIONS = {
"image/gif" => "gif",
"image/jpeg" => "jpg",
"image/pjpeg" => "jpg",
"image/x-png" => "png",
"image/jpg" => "jpg",
"image/png" => "png",
"application/x-shockwave-flash" => "swf",
"application/pdf" => "pdf",
"application/pgp-signature" => "sig",
"application/futuresplash" => "spl",
"application/msword" => "doc",
"application/postscript" => "ps",
"application/x-bittorrent" => "torrent",
"application/x-dvi" => "dvi",
"application/x-gzip" => "gz",
"application/x-ns-proxy-autoconfig" => "pac",
"application/x-shockwave-flash" => "swf",
"application/x-tgz" => "tar.gz",
"application/x-tar" => "tar",
"application/zip" => "zip",
"audio/mpeg" => "mp3",
"audio/x-mpegurl" => "m3u",
"audio/x-ms-wma" => "wma",
"audio/x-ms-wax" => "wax",
"audio/x-wav" => "wav",
"image/x-xbitmap" => "xbm",
"image/x-xpixmap" => "xpm",
"image/x-xwindowdump" => "xwd",
"text/css" => "css",
"text/html" => "html",
"text/javascript" => "js",
"text/plain" => "txt",
"text/xml" => "xml",
"video/mpeg" => "mpeg",
"video/quicktime" => "mov",
"video/x-msvideo" => "avi",
"video/x-ms-asf" => "asf",
"video/x-ms-wmv" => "wmv"
}
EXTENSIONS = Set.new MIME_EXTENSIONS.values
EXTENSIONS.merge %w(jpeg)
# default options. You can override these with +file_column+'s +options+ parameter
DEFAULT_OPTIONS = {
:root_path => File.join(RAILS_ROOT, "public"),
:web_root => "",
:mime_extensions => MIME_EXTENSIONS,
:extensions => EXTENSIONS,
:fix_file_extensions => true,
:permissions => 0644,
# path to the unix "file" executbale for
# guessing the content-type of files
:file_exec => "file"
}
# handle the +attr+ attribute as a "file-upload" column, generating additional methods as explained
# above. You should pass the attribute's name as a symbol, like this:
#
# file_column :image
#
# You can pass in an options hash that overrides the options
# in +DEFAULT_OPTIONS+.
def file_column(attr, options={})
options = DEFAULT_OPTIONS.merge(options) if options
my_options = FileColumn::init_options(options,
ActiveSupport::Inflector.underscore(self.name).to_s,
attr.to_s)
state_attr = "@#{attr}_state".to_sym
state_method = "#{attr}_state".to_sym
define_method state_method do
result = instance_variable_get state_attr
if result.nil?
result = FileColumn::create_state(self, attr.to_s)
instance_variable_set state_attr, result
end
result
end
private state_method
define_method attr do |*args|
send(state_method).absolute_path *args
end
define_method "#{attr}_relative_path" do |*args|
send(state_method).relative_path *args
end
define_method "#{attr}_dir" do
send(state_method).absolute_dir
end
define_method "#{attr}_relative_dir" do
send(state_method).relative_dir
end
define_method "#{attr}=" do |file|
state = send(state_method).assign(file)
instance_variable_set state_attr, state
if state.options[:after_upload] and state.just_uploaded?
state.options[:after_upload].each do |sym|
self.send sym
end
end
end
define_method "#{attr}_temp" do
send(state_method).temp_path
end
define_method "#{attr}_temp=" do |temp_path|
instance_variable_set state_attr, send(state_method).assign_temp(temp_path)
end
after_save_method = "#{attr}_after_save".to_sym
define_method after_save_method do
instance_variable_set state_attr, send(state_method).after_save
end
after_save after_save_method
after_destroy_method = "#{attr}_after_destroy".to_sym
define_method after_destroy_method do
send(state_method).after_destroy
end
after_destroy after_destroy_method
define_method "#{attr}_just_uploaded?" do
send(state_method).just_uploaded?
end
# this creates a closure keeping a reference to my_options
# right now that's the only way we store the options. We
# might use a class attribute as well
define_method "#{attr}_options" do
my_options
end
private after_save_method, after_destroy_method
FileColumn::MagickExtension::file_column(self, attr, my_options) if options[:magick]
end
end
private
def self.generate_temp_name
now = Time.now
"#{now.to_i}.#{now.usec}.#{Process.pid}"
end
def self.sanitize_filename(filename)
filename = File.basename(filename.gsub("\\", "/")) # work-around for IE
filename.gsub!(/[^a-zA-Z0-9\.\-\+_]/,"_")
filename = "_#{filename}" if filename =~ /^\.+$/
filename = "unnamed" if filename.size == 0
filename
end
end

View file

@ -0,0 +1,150 @@
# This module contains helper methods for displaying and uploading files
# for attributes created by +FileColumn+'s +file_column+ method. It will be
# automatically included into ActionView::Base, thereby making this module's
# methods available in all your views.
module FileColumnHelper
# Use this helper to create an upload field for a file_column attribute. This will generate
# an additional hidden field to keep uploaded files during form-redisplays. For example,
# when called with
#
# <%= file_column_field("entry", "image") %>
#
# the following HTML will be generated (assuming the form is redisplayed and something has
# already been uploaded):
#
# <input type="hidden" name="entry[image_temp]" value="..." />
# <input type="file" name="entry[image]" />
#
# You can use the +option+ argument to pass additional options to the file-field tag.
#
# Be sure to set the enclosing form's encoding to 'multipart/form-data', by
# using something like this:
#
# <%= form_tag {:action => "create", ...}, :multipart => true %>
def file_column_field(object, method, options={})
result = ActionView::Helpers::InstanceTag.new(object.dup, method.to_s+"_temp", self).to_input_field_tag("hidden", {})
result << ActionView::Helpers::InstanceTag.new(object.dup, method, self).to_input_field_tag("file", options)
end
# Creates an URL where an uploaded file can be accessed. When called for an Entry object with
# id 42 (stored in <tt>@entry</tt>) like this
#
# <%= url_for_file_column(@entry, "image")
#
# the following URL will be produced, assuming the file "test.png" has been stored in
# the "image"-column of an Entry object stored in <tt>@entry</tt>:
#
# /entry/image/42/test.png
#
# This will produce a valid URL even for temporary uploaded files, e.g. files where the object
# they are belonging to has not been saved in the database yet.
#
# The URL produces, although starting with a slash, will be relative
# to your app's root. If you pass it to one rails' +image_tag+
# helper, rails will properly convert it to an absolute
# URL. However, this will not be the case, if you create a link with
# the +link_to+ helper. In this case, you can pass <tt>:absolute =>
# true</tt> to +options+, which will make sure, the generated URL is
# absolute on your server. Examples:
#
# <%= image_tag url_for_file_column(@entry, "image") %>
# <%= link_to "Download", url_for_file_column(@entry, "image", :absolute => true) %>
#
# If there is currently no uploaded file stored in the object's column this method will
# return +nil+.
def url_for_file_column(object, method, options=nil)
case object
when String, Symbol
object = instance_variable_get("@#{object.to_s}")
end
# parse options
subdir = nil
absolute = false
if options
case options
when Hash
subdir = options[:subdir]
absolute = options[:absolute]
when String, Symbol
subdir = options
end
end
relative_path = object.send("#{method}_relative_path", subdir)
return nil unless relative_path
url = ""
url << request.relative_url_root.to_s if absolute
url << "/"
url << object.send("#{method}_options")[:base_url] << "/"
url << relative_path
end
# Same as +url_for_file_colum+ but allows you to access different versions
# of the image that have been processed by RMagick.
#
# If your +options+ parameter is non-nil this will
# access a different version of an image that will be produced by
# RMagick. You can use the following types for +options+:
#
# * a <tt>:symbol</tt> will select a version defined in the model
# via FileColumn::Magick's <tt>:versions</tt> feature.
# * a <tt>geometry_string</tt> will dynamically create an
# image resized as specified by <tt>geometry_string</tt>. The image will
# be stored so that it does not have to be recomputed the next time the
# same version string is used.
# * <tt>some_hash</tt> will dynamically create an image
# that is created according to the options in <tt>some_hash</tt>. This
# accepts exactly the same options as Magick's version feature.
#
# The version produced by RMagick will be stored in a special sub-directory.
# The directory's name will be derived from the options you specified
# (via a hash function) but if you want
# to set it yourself, you can use the <tt>:name => name</tt> option.
#
# Examples:
#
# <%= url_for_image_column @entry, "image", "640x480" %>
#
# will produce an URL like this
#
# /entry/image/42/bdn19n/filename.jpg
# # "640x480".hash.abs.to_s(36) == "bdn19n"
#
# and
#
# <%= url_for_image_column @entry, "image",
# :size => "50x50", :crop => "1:1", :name => "thumb" %>
#
# will produce something like this:
#
# /entry/image/42/thumb/filename.jpg
#
# Hint: If you are using the same geometry string / options hash multiple times, you should
# define it in a helper to stay with DRY. Another option is to define it in the model via
# FileColumn::Magick's <tt>:versions</tt> feature and then refer to it via a symbol.
#
# The URL produced by this method is relative to your application's root URL,
# although it will start with a slash.
# If you pass this URL to rails' +image_tag+ helper, it will be converted to an
# absolute URL automatically.
# If there is currently no image uploaded, or there is a problem while loading
# the image this method will return +nil+.
def url_for_image_column(object, method, options=nil)
case object
when String, Symbol
object = instance_variable_get("@#{object.to_s}")
end
subdir = nil
if options
subdir = object.send("#{method}_state").create_magick_version_if_needed(options)
end
if subdir.nil?
nil
else
url_for_file_column(object, method, subdir)
end
end
end

View file

@ -0,0 +1,28 @@
module FileColumn
# This bit of code allows you to pass regular old files to
# file_column. file_column depends on a few extra methods that the
# CGI uploaded file class adds. We will add the equivalent methods
# to file objects if necessary by extending them with this module. This
# avoids opening up the standard File class which might result in
# naming conflicts.
module FileCompat # :nodoc:
def original_filename
File.basename(path)
end
def size
File.size(path)
end
def local_path
path
end
def content_type
nil
end
end
end

View file

@ -0,0 +1,260 @@
module FileColumn # :nodoc:
class BaseUploadedFile # :nodoc:
def transform_with_magick
if needs_transform?
begin
img = ::Magick::Image::read(absolute_path).first
rescue ::Magick::ImageMagickError
if options[:magick][:image_required]
@magick_errors ||= []
@magick_errors << "invalid image"
end
return
end
if options[:magick][:versions]
options[:magick][:versions].each_pair do |version, version_options|
next if version_options[:lazy]
dirname = version_options[:name]
FileUtils.mkdir File.join(@dir, dirname)
transform_image(img, version_options, absolute_path(dirname))
end
end
if options[:magick][:size] or options[:magick][:crop] or options[:magick][:transformation] or options[:magick][:attributes]
transform_image(img, options[:magick], absolute_path)
end
GC.start
end
end
def create_magick_version_if_needed(version)
# RMagick might not have been loaded so far.
# We do not want to require it on every call of this method
# as this might be fairly expensive, so we just try if ::Magick
# exists and require it if not.
begin
::Magick
rescue NameError
require 'RMagick'
end
if version.is_a?(Symbol)
version_options = options[:magick][:versions][version]
else
version_options = MagickExtension::process_options(version)
end
unless File.exists?(absolute_path(version_options[:name]))
begin
img = ::Magick::Image::read(absolute_path).first
rescue ::Magick::ImageMagickError
# we might be called directly from the view here
# so we just return nil if we cannot load the image
return nil
end
dirname = version_options[:name]
FileUtils.mkdir File.join(@dir, dirname)
transform_image(img, version_options, absolute_path(dirname))
end
version_options[:name]
end
attr_reader :magick_errors
def has_magick_errors?
@magick_errors and !@magick_errors.empty?
end
private
def needs_transform?
options[:magick] and just_uploaded? and
(options[:magick][:size] or options[:magick][:versions] or options[:magick][:transformation] or options[:magick][:attributes])
end
def transform_image(img, img_options, dest_path)
begin
if img_options[:transformation]
if img_options[:transformation].is_a?(Symbol)
img = @instance.send(img_options[:transformation], img)
else
img = img_options[:transformation].call(img)
end
end
if img_options[:crop]
dx, dy = img_options[:crop].split(':').map { |x| x.to_f }
w, h = (img.rows * dx / dy), (img.columns * dy / dx)
img = img.crop(::Magick::CenterGravity, [img.columns, w].min,
[img.rows, h].min, true)
end
if img_options[:size]
img = img.change_geometry(img_options[:size]) do |c, r, i|
i.resize(c, r)
end
end
ensure
img.write(dest_path) do
if img_options[:attributes]
img_options[:attributes].each_pair do |property, value|
self.send "#{property}=", value
end
end
end
File.chmod options[:permissions], dest_path
end
end
end
# If you are using file_column to upload images, you can
# directly process the images with RMagick,
# a ruby extension
# for accessing the popular imagemagick libraries. You can find
# more information about RMagick at http://rmagick.rubyforge.org.
#
# You can control what to do by adding a <tt>:magick</tt> option
# to your options hash. All operations are performed immediately
# after a new file is assigned to the file_column attribute (i.e.,
# when a new file has been uploaded).
#
# == Resizing images
#
# To resize the uploaded image according to an imagemagick geometry
# string, just use the <tt>:size</tt> option:
#
# file_column :image, :magick => {:size => "800x600>"}
#
# If the uploaded file cannot be loaded by RMagick, file_column will
# signal a validation error for the corresponding attribute. If you
# want to allow non-image files to be uploaded in a column that uses
# the <tt>:magick</tt> option, you can set the <tt>:image_required</tt>
# attribute to +false+:
#
# file_column :image, :magick => {:size => "800x600>",
# :image_required => false }
#
# == Multiple versions
#
# You can also create additional versions of your image, for example
# thumb-nails, like this:
# file_column :image, :magick => {:versions => {
# :thumb => {:size => "50x50"},
# :medium => {:size => "640x480>"}
# }
#
# These versions will be stored in separate sub-directories, named like the
# symbol you used to identify the version. So in the previous example, the
# image versions will be stored in "thumb", "screen" and "widescreen"
# directories, resp.
# A name different from the symbol can be set via the <tt>:name</tt> option.
#
# These versions can be accessed via FileColumnHelper's +url_for_image_column+
# method like this:
#
# <%= url_for_image_column "entry", "image", :thumb %>
#
# == Cropping images
#
# If you wish to crop your images with a size ratio before scaling
# them according to your version geometry, you can use the :crop directive.
# file_column :image, :magick => {:versions => {
# :square => {:crop => "1:1", :size => "50x50", :name => "thumb"},
# :screen => {:crop => "4:3", :size => "640x480>"},
# :widescreen => {:crop => "16:9", :size => "640x360!"},
# }
# }
#
# == Custom attributes
#
# To change some of the image properties like compression level before they
# are saved you can set the <tt>:attributes</tt> option.
# For a list of available attributes go to http://www.simplesystems.org/RMagick/doc/info.html
#
# file_column :image, :magick => { :attributes => { :quality => 30 } }
#
# == Custom transformations
#
# To perform custom transformations on uploaded images, you can pass a
# callback to file_column:
# file_column :image, :magick =>
# Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) }
#
# The callback you give, receives one argument, which is an instance
# of Magick::Image, the RMagick image class. It should return a transformed
# image. Instead of passing a <tt>Proc</tt> object, you can also give a
# <tt>Symbol</tt>, the name of an instance method of your model.
#
# Custom transformations can be combined via the standard :size and :crop
# features, by using the :transformation option:
# file_column :image, :magick => {
# :transformation => Proc.new { |image| ... },
# :size => "640x480"
# }
#
# In this case, the standard resizing operations will be performed after the
# custom transformation.
#
# Of course, custom transformations can be used in versions, as well.
#
# <b>Note:</b> You'll need the
# RMagick extension being installed in order to use file_column's
# imagemagick integration.
module MagickExtension
def self.file_column(klass, attr, options) # :nodoc:
require 'RMagick'
options[:magick] = process_options(options[:magick],false) if options[:magick]
if options[:magick][:versions]
options[:magick][:versions].each_pair do |name, value|
options[:magick][:versions][name] = process_options(value, name.to_s)
end
end
state_method = "#{attr}_state".to_sym
after_assign_method = "#{attr}_magick_after_assign".to_sym
klass.send(:define_method, after_assign_method) do
self.send(state_method).transform_with_magick
end
options[:after_upload] ||= []
options[:after_upload] << after_assign_method
klass.validate do |record|
state = record.send(state_method)
if state.has_magick_errors?
state.magick_errors.each do |error|
record.errors.add attr, error
end
end
end
end
def self.process_options(options,create_name=true)
case options
when String then options = {:size => options}
when Proc, Symbol then options = {:transformation => options }
end
if options[:geometry]
options[:size] = options.delete(:geometry)
end
options[:image_required] = true unless options.key?(:image_required)
if options[:name].nil? and create_name
if create_name == true
hash = 0
for key in [:size, :crop]
hash = hash ^ options[key].hash if options[key]
end
options[:name] = hash.abs.to_s(36)
else
options[:name] = create_name
end
end
options
end
end
end

View file

@ -0,0 +1,19 @@
# require this file from your "config/environment.rb" (after rails has been loaded)
# to integrate the file_column extension into rails.
require 'file_column'
require 'file_column_helper'
module ActiveRecord # :nodoc:
class Base # :nodoc:
# make file_column method available in all active record decendants
include FileColumn
end
end
module ActionView # :nodoc:
class Base # :nodoc:
include FileColumnHelper
end
end

View file

@ -0,0 +1,124 @@
require 'test/unit'
# Add the methods +upload+, the <tt>setup_file_fixtures</tt> and
# <tt>teardown_file_fixtures</tt> to the class Test::Unit::TestCase.
class Test::Unit::TestCase
# Returns a +Tempfile+ object as it would have been generated on file upload.
# Use this method to create the parameters when emulating form posts with
# file fields.
#
# === Example:
#
# def test_file_column_post
# entry = { :title => 'foo', :file => upload('/tmp/foo.txt')}
# post :upload, :entry => entry
#
# # ...
# end
#
# === Parameters
#
# * <tt>path</tt> The path to the file to upload.
# * <tt>content_type</tt> The MIME type of the file. If it is <tt>:guess</tt>,
# the method will try to guess it.
def upload(path, content_type=:guess, type=:tempfile)
if content_type == :guess
case path
when /\.jpg$/ then content_type = "image/jpeg"
when /\.png$/ then content_type = "image/png"
else content_type = nil
end
end
uploaded_file(path, content_type, File.basename(path), type)
end
# Copies the fixture files from "RAILS_ROOT/test/fixtures/file_column" into
# the temporary storage directory used for testing
# ("RAILS_ROOT/test/tmp/file_column"). Call this method in your
# <tt>setup</tt> methods to get the file fixtures (images, for example) into
# the directory used by file_column in testing.
#
# Note that the files and directories in the "fixtures/file_column" directory
# must have the same structure as you would expect in your "/public" directory
# after uploading with FileColumn.
#
# For example, the directory structure could look like this:
#
# test/fixtures/file_column/
# `-- container
# |-- first_image
# | |-- 1
# | | `-- image1.jpg
# | `-- tmp
# `-- second_image
# |-- 1
# | `-- image2.jpg
# `-- tmp
#
# Your fixture file for this one "container" class fixture could look like this:
#
# first:
# id: 1
# first_image: image1.jpg
# second_image: image1.jpg
#
# A usage example:
#
# def setup
# setup_fixture_files
#
# # ...
# end
def setup_fixture_files
tmp_path = File.join(RAILS_ROOT, "test", "tmp", "file_column")
file_fixtures = Dir.glob File.join(RAILS_ROOT, "test", "fixtures", "file_column", "*")
FileUtils.mkdir_p tmp_path unless File.exists?(tmp_path)
FileUtils.cp_r file_fixtures, tmp_path
end
# Removes the directory "RAILS_ROOT/test/tmp/file_column/" so the files
# copied on test startup are removed. Call this in your unit test's +teardown+
# method.
#
# A usage example:
#
# def teardown
# teardown_fixture_files
#
# # ...
# end
def teardown_fixture_files
FileUtils.rm_rf File.join(RAILS_ROOT, "test", "tmp", "file_column")
end
private
def uploaded_file(path, content_type, filename, type=:tempfile) # :nodoc:
if type == :tempfile
t = Tempfile.new(File.basename(filename))
FileUtils.copy_file(path, t.path)
else
if path
t = StringIO.new(IO.read(path))
else
t = StringIO.new
end
end
(class << t; self; end).class_eval do
alias local_path path if type == :tempfile
define_method(:local_path) { "" } if type == :stringio
define_method(:original_filename) {filename}
define_method(:content_type) {content_type}
end
return t
end
end
# If we are running in the "test" environment, we overwrite the default
# settings for FileColumn so that files are not uploaded into "/public/"
# in tests but rather into the directory "/test/tmp/file_column".
if RAILS_ENV == "test"
FileColumn::ClassMethods::DEFAULT_OPTIONS[:root_path] =
File.join(RAILS_ROOT, "test", "tmp", "file_column")
end

View file

@ -0,0 +1,112 @@
module FileColumn
module Validations #:nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
# This module contains methods to create validations of uploaded files. All methods
# in this module will be included as class methods into <tt>ActiveRecord::Base</tt>
# so that you can use them in your models like this:
#
# class Entry < ActiveRecord::Base
# file_column :image
# validates_filesize_of :image, :in => 0..1.megabyte
# end
module ClassMethods
EXT_REGEXP = /\.([A-z0-9]+)$/
# This validates the file type of one or more file_columns. A list of file columns
# should be given followed by an options hash.
#
# Required options:
# * <tt>:in</tt> => list of extensions or mime types. If mime types are used they
# will be mapped into an extension via FileColumn::ClassMethods::MIME_EXTENSIONS.
#
# Examples:
# validates_file_format_of :field, :in => ["gif", "png", "jpg"]
# validates_file_format_of :field, :in => ["image/jpeg"]
def validates_file_format_of(*attrs)
options = attrs.pop if attrs.last.is_a?Hash
raise ArgumentError, "Please include the :in option." if !options || !options[:in]
options[:in] = [options[:in]] if options[:in].is_a?String
raise ArgumentError, "Invalid value for option :in" unless options[:in].is_a?Array
validates_each(attrs, options) do |record, attr, value|
unless value.blank?
mime_extensions = record.send("#{attr}_options")[:mime_extensions]
extensions = options[:in].map{|o| mime_extensions[o] || o }
record.errors.add attr, "is not a valid format." unless extensions.include?(value.scan(EXT_REGEXP).flatten.first)
end
end
end
# This validates the file size of one or more file_columns. A list of file columns
# should be given followed by an options hash.
#
# Required options:
# * <tt>:in</tt> => A size range. Note that you can use ActiveSupport's
# numeric extensions for kilobytes, etc.
#
# Examples:
# validates_filesize_of :field, :in => 0..100.megabytes
# validates_filesize_of :field, :in => 15.kilobytes..1.megabyte
def validates_filesize_of(*attrs)
options = attrs.pop if attrs.last.is_a?Hash
raise ArgumentError, "Please include the :in option." if !options || !options[:in]
raise ArgumentError, "Invalid value for option :in" unless options[:in].is_a?Range
validates_each(attrs, options) do |record, attr, value|
unless value.blank?
size = File.size(value)
record.errors.add attr, "is smaller than the allowed size range." if size < options[:in].first
record.errors.add attr, "is larger than the allowed size range." if size > options[:in].last
end
end
end
IMAGE_SIZE_REGEXP = /^(\d+)x(\d+)$/
# Validates the image size of one or more file_columns. A list of file columns
# should be given followed by an options hash. The validation will pass
# if both image dimensions (rows and columns) are at least as big as
# given in the <tt>:min</tt> option.
#
# Required options:
# * <tt>:min</tt> => minimum image dimension string, in the format NNxNN
# (columns x rows).
#
# Example:
# validates_image_size :field, :min => "1200x1800"
#
# This validation requires RMagick to be installed on your system
# to check the image's size.
def validates_image_size(*attrs)
options = attrs.pop if attrs.last.is_a?Hash
raise ArgumentError, "Please include a :min option." if !options || !options[:min]
minimums = options[:min].scan(IMAGE_SIZE_REGEXP).first.collect{|n| n.to_i} rescue []
raise ArgumentError, "Invalid value for option :min (should be 'XXxYY')" unless minimums.size == 2
require 'RMagick'
validates_each(attrs, options) do |record, attr, value|
unless value.blank?
begin
img = ::Magick::Image::read(value).first
record.errors.add('image', "is too small, must be at least #{minimums[0]}x#{minimums[1]}") if ( img.rows < minimums[1] || img.columns < minimums[0] )
rescue ::Magick::ImageMagickError
record.errors.add('image', "invalid image")
end
img = nil
GC.start
end
end
end
end
end
end

View file

@ -0,0 +1,63 @@
require 'test/unit'
require 'rubygems'
require 'active_support'
require 'active_record'
require 'action_view'
require File.dirname(__FILE__) + '/connection'
require 'stringio'
RAILS_ROOT = File.dirname(__FILE__)
RAILS_ENV = ""
$: << "../lib"
require 'file_column'
require 'file_compat'
require 'validations'
require 'test_case'
# do not use the file executable normally in our tests as
# it may not be present on the machine we are running on
FileColumn::ClassMethods::DEFAULT_OPTIONS =
FileColumn::ClassMethods::DEFAULT_OPTIONS.merge({:file_exec => nil})
class ActiveRecord::Base
include FileColumn
include FileColumn::Validations
end
class RequestMock
attr_accessor :relative_url_root
def initialize
@relative_url_root = ""
end
end
class Test::Unit::TestCase
def assert_equal_paths(expected_path, path)
assert_equal normalize_path(expected_path), normalize_path(path)
end
private
def normalize_path(path)
Pathname.new(path).realpath
end
def clear_validations
[:validate, :validate_on_create, :validate_on_update].each do |attr|
Entry.write_inheritable_attribute attr, []
Movie.write_inheritable_attribute attr, []
end
end
def file_path(filename)
File.expand_path("#{File.dirname(__FILE__)}/fixtures/#{filename}")
end
alias_method :f, :file_path
end

View file

@ -0,0 +1,17 @@
print "Using native MySQL\n"
require 'logger'
ActiveRecord::Base.logger = Logger.new("debug.log")
db = 'file_column_test'
ActiveRecord::Base.establish_connection(
:adapter => "mysql",
:host => "localhost",
:username => "rails",
:password => "",
:database => db,
:socket => "/var/run/mysqld/mysqld.sock"
)
load File.dirname(__FILE__) + "/fixtures/schema.rb"

View file

@ -0,0 +1,97 @@
require File.dirname(__FILE__) + '/abstract_unit'
require File.dirname(__FILE__) + '/fixtures/entry'
class UrlForFileColumnTest < Test::Unit::TestCase
include FileColumnHelper
def setup
Entry.file_column :image
@request = RequestMock.new
end
def test_url_for_file_column_with_temp_entry
@e = Entry.new(:image => upload(f("skanthak.png")))
url = url_for_file_column("e", "image")
assert_match %r{^/entry/image/tmp/\d+(\.\d+)+/skanthak.png$}, url
end
def test_url_for_file_column_with_saved_entry
@e = Entry.new(:image => upload(f("skanthak.png")))
assert @e.save
url = url_for_file_column("e", "image")
assert_equal "/entry/image/#{@e.id}/skanthak.png", url
end
def test_url_for_file_column_works_with_symbol
@e = Entry.new(:image => upload(f("skanthak.png")))
assert @e.save
url = url_for_file_column(:e, :image)
assert_equal "/entry/image/#{@e.id}/skanthak.png", url
end
def test_url_for_file_column_works_with_object
e = Entry.new(:image => upload(f("skanthak.png")))
assert e.save
url = url_for_file_column(e, "image")
assert_equal "/entry/image/#{e.id}/skanthak.png", url
end
def test_url_for_file_column_should_return_nil_on_no_uploaded_file
e = Entry.new
assert_nil url_for_file_column(e, "image")
end
def test_url_for_file_column_without_extension
e = Entry.new
e.image = uploaded_file(file_path("kerb.jpg"), "something/unknown", "local_filename")
assert e.save
assert_equal "/entry/image/#{e.id}/local_filename", url_for_file_column(e, "image")
end
end
class UrlForFileColumnTest < Test::Unit::TestCase
include FileColumnHelper
include ActionView::Helpers::AssetTagHelper
include ActionView::Helpers::TagHelper
include ActionView::Helpers::UrlHelper
def setup
Entry.file_column :image
# mock up some request data structures for AssetTagHelper
@request = RequestMock.new
@request.relative_url_root = "/foo/bar"
@controller = self
end
def request
@request
end
IMAGE_URL = %r{^/foo/bar/entry/image/.+/skanthak.png$}
def test_with_image_tag
e = Entry.new(:image => upload(f("skanthak.png")))
html = image_tag url_for_file_column(e, "image")
url = html.scan(/src=\"(.+)\"/).first.first
assert_match IMAGE_URL, url
end
def test_with_link_to_tag
e = Entry.new(:image => upload(f("skanthak.png")))
html = link_to "Download", url_for_file_column(e, "image", :absolute => true)
url = html.scan(/href=\"(.+)\"/).first.first
assert_match IMAGE_URL, url
end
def test_relative_url_root_not_modified
e = Entry.new(:image => upload(f("skanthak.png")))
url_for_file_column(e, "image", :absolute => true)
assert_equal "/foo/bar", @request.relative_url_root
end
end

View file

@ -0,0 +1,650 @@
require File.dirname(__FILE__) + '/abstract_unit'
require File.dirname(__FILE__) + '/fixtures/entry'
class Movie < ActiveRecord::Base
end
class FileColumnTest < Test::Unit::TestCase
def setup
# we define the file_columns here so that we can change
# settings easily in a single test
Entry.file_column :image
Entry.file_column :file
Movie.file_column :movie
clear_validations
end
def teardown
FileUtils.rm_rf File.dirname(__FILE__)+"/public/entry/"
FileUtils.rm_rf File.dirname(__FILE__)+"/public/movie/"
FileUtils.rm_rf File.dirname(__FILE__)+"/public/my_store_dir/"
end
def test_column_write_method
assert Entry.new.respond_to?("image=")
end
def test_column_read_method
assert Entry.new.respond_to?("image")
end
def test_sanitize_filename
assert_equal "test.jpg", FileColumn::sanitize_filename("test.jpg")
assert FileColumn::sanitize_filename("../../very_tricky/foo.bar") !~ /[\\\/]/, "slashes not removed"
assert_equal "__foo", FileColumn::sanitize_filename('`*foo')
assert_equal "foo.txt", FileColumn::sanitize_filename('c:\temp\foo.txt')
assert_equal "_.", FileColumn::sanitize_filename(".")
end
def test_default_options
e = Entry.new
assert_match %r{/public/entry/image}, e.image_options[:store_dir]
assert_match %r{/public/entry/image/tmp}, e.image_options[:tmp_base_dir]
end
def test_assign_without_save_with_tempfile
do_test_assign_without_save(:tempfile)
end
def test_assign_without_save_with_stringio
do_test_assign_without_save(:stringio)
end
def do_test_assign_without_save(upload_type)
e = Entry.new
e.image = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png", upload_type)
assert e.image.is_a?(String), "#{e.image.inspect} is not a String"
assert File.exists?(e.image)
assert FileUtils.identical?(e.image, file_path("skanthak.png"))
end
def test_filename_preserved
e = Entry.new
e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "local_filename.jpg")
assert_equal "local_filename.jpg", File.basename(e.image)
end
def test_filename_stored_in_attribute
e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
assert_equal "kerb.jpg", e["image"]
end
def test_extension_added
e = Entry.new
e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "local_filename")
assert_equal "local_filename.jpg", File.basename(e.image)
assert_equal "local_filename.jpg", e["image"]
end
def test_no_extension_without_content_type
e = Entry.new
e.image = uploaded_file(file_path("kerb.jpg"), "something/unknown", "local_filename")
assert_equal "local_filename", File.basename(e.image)
assert_equal "local_filename", e["image"]
end
def test_extension_unknown_type
e = Entry.new
e.image = uploaded_file(file_path("kerb.jpg"), "not/known", "local_filename")
assert_equal "local_filename", File.basename(e.image)
assert_equal "local_filename", e["image"]
end
def test_extension_unknown_type_with_extension
e = Entry.new
e.image = uploaded_file(file_path("kerb.jpg"), "not/known", "local_filename.abc")
assert_equal "local_filename.abc", File.basename(e.image)
assert_equal "local_filename.abc", e["image"]
end
def test_extension_corrected
e = Entry.new
e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "local_filename.jpeg")
assert_equal "local_filename.jpg", File.basename(e.image)
assert_equal "local_filename.jpg", e["image"]
end
def test_double_extension
e = Entry.new
e.image = uploaded_file(file_path("kerb.jpg"), "application/x-tgz", "local_filename.tar.gz")
assert_equal "local_filename.tar.gz", File.basename(e.image)
assert_equal "local_filename.tar.gz", e["image"]
end
FILE_UTILITY = "/usr/bin/file"
def test_get_content_type_with_file
Entry.file_column :image, :file_exec => FILE_UTILITY
# run this test only if the machine we are running on
# has the file utility installed
if File.executable?(FILE_UTILITY)
e = Entry.new
file = FileColumn::TempUploadedFile.new(e, "image")
file.instance_variable_set :@dir, File.dirname(file_path("kerb.jpg"))
file.instance_variable_set :@filename, File.basename(file_path("kerb.jpg"))
assert_equal "image/jpeg", file.get_content_type
else
puts "Warning: Skipping test_get_content_type_with_file test as '#{options[:file_exec]}' does not exist"
end
end
def test_fix_extension_with_file
Entry.file_column :image, :file_exec => FILE_UTILITY
# run this test only if the machine we are running on
# has the file utility installed
if File.executable?(FILE_UTILITY)
e = Entry.new(:image => uploaded_file(file_path("skanthak.png"), "", "skanthak.jpg"))
assert_equal "skanthak.png", File.basename(e.image)
else
puts "Warning: Skipping test_fix_extension_with_file test as '#{options[:file_exec]}' does not exist"
end
end
def test_do_not_fix_file_extensions
Entry.file_column :image, :fix_file_extensions => false
e = Entry.new(:image => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb"))
assert_equal "kerb", File.basename(e.image)
end
def test_correct_extension
e = Entry.new
file = FileColumn::TempUploadedFile.new(e, "image")
assert_equal "filename.jpg", file.correct_extension("filename.jpeg","jpg")
assert_equal "filename.tar.gz", file.correct_extension("filename.jpg","tar.gz")
assert_equal "filename.jpg", file.correct_extension("filename.tar.gz","jpg")
assert_equal "Protokoll_01.09.2005.doc", file.correct_extension("Protokoll_01.09.2005","doc")
assert_equal "strange.filenames.exist.jpg", file.correct_extension("strange.filenames.exist","jpg")
assert_equal "another.strange.one.jpg", file.correct_extension("another.strange.one.png","jpg")
end
def test_assign_with_save
e = Entry.new
e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")
tmp_file_path = e.image
assert e.save
assert File.exists?(e.image)
assert FileUtils.identical?(e.image, file_path("kerb.jpg"))
assert_equal "#{e.id}/kerb.jpg", e.image_relative_path
assert !File.exists?(tmp_file_path), "temporary file '#{tmp_file_path}' not removed"
assert !File.exists?(File.dirname(tmp_file_path)), "temporary directory '#{File.dirname(tmp_file_path)}' not removed"
local_path = e.image
e = Entry.find(e.id)
assert_equal local_path, e.image
end
def test_dir_methods
e = Entry.new
e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")
e.save
assert_equal_paths File.join(RAILS_ROOT, "public", "entry", "image", e.id.to_s), e.image_dir
assert_equal File.join(e.id.to_s), e.image_relative_dir
end
def test_store_dir_callback
Entry.file_column :image, {:store_dir => :my_store_dir}
e = Entry.new
e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")
assert e.save
assert_equal_paths File.join(RAILS_ROOT, "public", "my_store_dir", e.id), e.image_dir
end
def test_tmp_dir_with_store_dir_callback
Entry.file_column :image, {:store_dir => :my_store_dir}
e = Entry.new
e.image = upload(f("kerb.jpg"))
assert_equal File.expand_path(File.join(RAILS_ROOT, "public", "my_store_dir", "tmp")), File.expand_path(File.join(e.image_dir,".."))
end
def test_invalid_store_dir_callback
Entry.file_column :image, {:store_dir => :my_store_dir_doesnt_exit}
e = Entry.new
assert_raise(ArgumentError) {
e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")
e.save
}
end
def test_subdir_parameter
e = Entry.new
assert_nil e.image("thumb")
assert_nil e.image_relative_path("thumb")
assert_nil e.image(nil)
e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")
assert_equal "kerb.jpg", File.basename(e.image("thumb"))
assert_equal "kerb.jpg", File.basename(e.image_relative_path("thumb"))
assert_equal File.join(e.image_dir,"thumb","kerb.jpg"), e.image("thumb")
assert_match %r{/thumb/kerb\.jpg$}, e.image_relative_path("thumb")
assert_equal e.image, e.image(nil)
assert_equal e.image_relative_path, e.image_relative_path(nil)
end
def test_cleanup_after_destroy
e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
assert e.save
local_path = e.image
assert File.exists?(local_path)
assert e.destroy
assert !File.exists?(local_path), "'#{local_path}' still exists although entry was destroyed"
assert !File.exists?(File.dirname(local_path))
end
def test_keep_tmp_image
e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
e.validation_should_fail = true
assert !e.save, "e should not save due to validation errors"
assert File.exists?(local_path = e.image)
image_temp = e.image_temp
e = Entry.new("image_temp" => image_temp)
assert_equal local_path, e.image
assert e.save
assert FileUtils.identical?(e.image, file_path("kerb.jpg"))
end
def test_keep_tmp_image_with_existing_image
e = Entry.new("image" =>uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
assert e.save
assert File.exists?(local_path = e.image)
e = Entry.find(e.id)
e.image = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
e.validation_should_fail = true
assert !e.save
temp_path = e.image_temp
e = Entry.find(e.id)
e.image_temp = temp_path
assert e.save
assert FileUtils.identical?(e.image, file_path("skanthak.png"))
assert !File.exists?(local_path), "old image has not been deleted"
end
def test_replace_tmp_image_temp_first
do_test_replace_tmp_image([:image_temp, :image])
end
def test_replace_tmp_image_temp_last
do_test_replace_tmp_image([:image, :image_temp])
end
def do_test_replace_tmp_image(order)
e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
e.validation_should_fail = true
assert !e.save
image_temp = e.image_temp
temp_path = e.image
new_img = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
e = Entry.new
for method in order
case method
when :image_temp then e.image_temp = image_temp
when :image then e.image = new_img
end
end
assert e.save
assert FileUtils.identical?(e.image, file_path("skanthak.png")), "'#{e.image}' is not the expected 'skanthak.png'"
assert !File.exists?(temp_path), "temporary file '#{temp_path}' is not cleaned up"
assert !File.exists?(File.dirname(temp_path)), "temporary directory not cleaned up"
assert e.image_just_uploaded?
end
def test_replace_image_on_saved_object
e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
assert e.save
old_file = e.image
e = Entry.find(e.id)
e.image = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
assert e.save
assert FileUtils.identical?(file_path("skanthak.png"), e.image)
assert old_file != e.image
assert !File.exists?(old_file), "'#{old_file}' has not been cleaned up"
end
def test_edit_without_touching_image
e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
assert e.save
e = Entry.find(e.id)
assert e.save
assert FileUtils.identical?(file_path("kerb.jpg"), e.image)
end
def test_save_without_image
e = Entry.new
assert e.save
e.reload
assert_nil e.image
end
def test_delete_saved_image
e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
assert e.save
local_path = e.image
e.image = nil
assert_nil e.image
assert File.exists?(local_path), "file '#{local_path}' should not be deleted until transaction is saved"
assert e.save
assert_nil e.image
assert !File.exists?(local_path)
e.reload
assert e["image"].blank?
e = Entry.find(e.id)
assert_nil e.image
end
def test_delete_tmp_image
e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
local_path = e.image
e.image = nil
assert_nil e.image
assert e["image"].blank?
assert !File.exists?(local_path)
end
def test_delete_nonexistant_image
e = Entry.new
e.image = nil
assert e.save
assert_nil e.image
end
def test_delete_image_on_non_null_column
e = Entry.new("file" => upload(f("skanthak.png")))
assert e.save
local_path = e.file
assert File.exists?(local_path)
e.file = nil
assert e.save
assert !File.exists?(local_path)
end
def test_ie_filename
e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", 'c:\images\kerb.jpg'))
assert e.image_relative_path =~ /^tmp\/[\d\.]+\/kerb\.jpg$/, "relative path '#{e.image_relative_path}' was not as expected"
assert File.exists?(e.image)
end
def test_just_uploaded?
e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", 'c:\images\kerb.jpg'))
assert e.image_just_uploaded?
assert e.save
assert e.image_just_uploaded?
e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", 'kerb.jpg'))
temp_path = e.image_temp
e = Entry.new("image_temp" => temp_path)
assert !e.image_just_uploaded?
assert e.save
assert !e.image_just_uploaded?
end
def test_empty_tmp
e = Entry.new
e.image_temp = ""
assert_nil e.image
end
def test_empty_tmp_with_image
e = Entry.new
e.image_temp = ""
e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", 'c:\images\kerb.jpg')
local_path = e.image
assert File.exists?(local_path)
e.image_temp = ""
assert local_path, e.image
end
def test_empty_filename
e = Entry.new
assert_equal "", e["file"]
assert_nil e.file
assert_nil e["image"]
assert_nil e.image
end
def test_with_two_file_columns
e = Entry.new
e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")
e.file = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
assert e.save
assert_match %{/entry/image/}, e.image
assert_match %{/entry/file/}, e.file
assert FileUtils.identical?(e.image, file_path("kerb.jpg"))
assert FileUtils.identical?(e.file, file_path("skanthak.png"))
end
def test_with_two_models
e = Entry.new(:image => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
m = Movie.new(:movie => uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png"))
assert e.save
assert m.save
assert_match %{/entry/image/}, e.image
assert_match %{/movie/movie/}, m.movie
assert FileUtils.identical?(e.image, file_path("kerb.jpg"))
assert FileUtils.identical?(m.movie, file_path("skanthak.png"))
end
def test_no_file_uploaded
e = Entry.new
assert_nothing_raised { e.image =
uploaded_file(nil, "application/octet-stream", "", :stringio) }
assert_equal nil, e.image
end
# when safari submits a form where no file has been
# selected, it does not transmit a content-type and
# the result is an empty string ""
def test_no_file_uploaded_with_safari
e = Entry.new
assert_nothing_raised { e.image = "" }
assert_equal nil, e.image
end
def test_detect_wrong_encoding
e = Entry.new
assert_raise(TypeError) { e.image ="img42.jpg" }
end
def test_serializable_before_save
e = Entry.new
e.image = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
assert_nothing_raised {
flash = Marshal.dump(e)
e = Marshal.load(flash)
}
assert File.exists?(e.image)
end
def test_should_call_after_upload_on_new_upload
Entry.file_column :image, :after_upload => [:after_assign]
e = Entry.new
e.image = upload(f("skanthak.png"))
assert e.after_assign_called?
end
def test_should_call_user_after_save_on_save
e = Entry.new(:image => upload(f("skanthak.png")))
assert e.save
assert_kind_of FileColumn::PermanentUploadedFile, e.send(:image_state)
assert e.after_save_called?
end
def test_assign_standard_files
e = Entry.new
e.image = File.new(file_path('skanthak.png'))
assert_equal 'skanthak.png', File.basename(e.image)
assert FileUtils.identical?(file_path('skanthak.png'), e.image)
assert e.save
end
def test_validates_filesize
Entry.validates_filesize_of :image, :in => 50.kilobytes..100.kilobytes
e = Entry.new(:image => upload(f("kerb.jpg")))
assert e.save
e.image = upload(f("skanthak.png"))
assert !e.save
assert e.errors.invalid?("image")
end
def test_validates_file_format_simple
e = Entry.new(:image => upload(f("skanthak.png")))
assert e.save
Entry.validates_file_format_of :image, :in => ["jpg"]
e.image = upload(f("kerb.jpg"))
assert e.save
e.image = upload(f("mysql.sql"))
assert !e.save
assert e.errors.invalid?("image")
end
def test_validates_image_size
Entry.validates_image_size :image, :min => "640x480"
e = Entry.new(:image => upload(f("kerb.jpg")))
assert e.save
e = Entry.new(:image => upload(f("skanthak.png")))
assert !e.save
assert e.errors.invalid?("image")
end
def do_permission_test(uploaded_file, permissions=0641)
Entry.file_column :image, :permissions => permissions
e = Entry.new(:image => uploaded_file)
assert e.save
assert_equal permissions, (File.stat(e.image).mode & 0777)
end
def test_permissions_with_small_file
do_permission_test upload(f("skanthak.png"), :guess, :stringio)
end
def test_permission_with_big_file
do_permission_test upload(f("kerb.jpg"))
end
def test_permission_that_overrides_umask
do_permission_test upload(f("skanthak.png"), :guess, :stringio), 0666
do_permission_test upload(f("kerb.jpg")), 0666
end
def test_access_with_empty_id
# an empty id might happen after a clone or through some other
# strange event. Since we would create a path that contains nothing
# where the id would have been, we should fail fast with an exception
# in this case
e = Entry.new(:image => upload(f("skanthak.png")))
assert e.save
id = e.id
e = Entry.find(id)
e["id"] = ""
assert_raise(RuntimeError) { e.image }
e = Entry.find(id)
e["id"] = nil
assert_raise(RuntimeError) { e.image }
end
end
# Tests for moving temp dir to permanent dir
class FileColumnMoveTest < Test::Unit::TestCase
def setup
# we define the file_columns here so that we can change
# settings easily in a single test
Entry.file_column :image
end
def teardown
FileUtils.rm_rf File.dirname(__FILE__)+"/public/entry/"
end
def test_should_move_additional_files_from_tmp
e = Entry.new
e.image = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
FileUtils.cp file_path("kerb.jpg"), File.dirname(e.image)
assert e.save
dir = File.dirname(e.image)
assert File.exists?(File.join(dir, "skanthak.png"))
assert File.exists?(File.join(dir, "kerb.jpg"))
end
def test_should_move_direcotries_on_save
e = Entry.new(:image => upload(f("skanthak.png")))
FileUtils.mkdir( e.image_dir+"/foo" )
FileUtils.cp file_path("kerb.jpg"), e.image_dir+"/foo/kerb.jpg"
assert e.save
assert File.exists?(e.image)
assert File.exists?(File.dirname(e.image)+"/foo/kerb.jpg")
end
def test_should_overwrite_dirs_with_files_on_reupload
e = Entry.new(:image => upload(f("skanthak.png")))
FileUtils.mkdir( e.image_dir+"/kerb.jpg")
FileUtils.cp file_path("kerb.jpg"), e.image_dir+"/kerb.jpg/"
assert e.save
e.image = upload(f("kerb.jpg"))
assert e.save
assert File.file?(e.image_dir+"/kerb.jpg")
end
def test_should_overwrite_files_with_dirs_on_reupload
e = Entry.new(:image => upload(f("skanthak.png")))
assert e.save
assert File.file?(e.image_dir+"/skanthak.png")
e.image = upload(f("kerb.jpg"))
FileUtils.mkdir(e.image_dir+"/skanthak.png")
assert e.save
assert File.file?(e.image_dir+"/kerb.jpg")
assert !File.file?(e.image_dir+"/skanthak.png")
assert File.directory?(e.image_dir+"/skanthak.png")
end
end

View file

@ -0,0 +1,32 @@
class Entry < ActiveRecord::Base
attr_accessor :validation_should_fail
def validate
errors.add("image","some stupid error") if @validation_should_fail
end
def after_assign
@after_assign_called = true
end
def after_assign_called?
@after_assign_called
end
def after_save
@after_save_called = true
end
def after_save_called?
@after_save_called
end
def my_store_dir
# not really dynamic but at least it could be...
"my_store_dir"
end
def load_image_with_rmagick(path)
Magick::Image::read(path).first
end
end

View file

@ -0,0 +1 @@
this is certainly not a JPEG image

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View file

@ -0,0 +1,25 @@
-- MySQL dump 9.11
--
-- Host: localhost Database: file_column_test
-- ------------------------------------------------------
-- Server version 4.0.24
--
-- Table structure for table `entries`
--
DROP TABLE IF EXISTS entries;
CREATE TABLE entries (
id int(11) NOT NULL auto_increment,
image varchar(200) default NULL,
file varchar(200) NOT NULL,
PRIMARY KEY (id)
) TYPE=MyISAM;
DROP TABLE IF EXISTS movies;
CREATE TABLE movies (
id int(11) NOT NULL auto_increment,
movie varchar(200) default NULL,
PRIMARY KEY (id)
) TYPE=MyISAM;

View file

@ -0,0 +1,10 @@
ActiveRecord::Schema.define do
create_table :entries, :force => true do |t|
t.column :image, :string, :null => true
t.column :file, :string, :null => false
end
create_table :movies, :force => true do |t|
t.column :movie, :string
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,380 @@
require File.dirname(__FILE__) + '/abstract_unit'
require 'RMagick'
require File.dirname(__FILE__) + '/fixtures/entry'
class AbstractRMagickTest < Test::Unit::TestCase
def teardown
FileUtils.rm_rf File.dirname(__FILE__)+"/public/entry/"
end
def test_truth
assert true
end
private
def read_image(path)
Magick::Image::read(path).first
end
def assert_max_image_size(img, s)
assert img.columns <= s, "img has #{img.columns} columns, expected: #{s}"
assert img.rows <= s, "img has #{img.rows} rows, expected: #{s}"
assert_equal s, [img.columns, img.rows].max
end
end
class RMagickSimpleTest < AbstractRMagickTest
def setup
Entry.file_column :image, :magick => { :geometry => "100x100" }
end
def test_simple_resize_without_save
e = Entry.new
e.image = upload(f("kerb.jpg"))
img = read_image(e.image)
assert_max_image_size img, 100
end
def test_simple_resize_with_save
e = Entry.new
e.image = upload(f("kerb.jpg"))
assert e.save
e.reload
img = read_image(e.image)
assert_max_image_size img, 100
end
def test_resize_on_saved_image
Entry.file_column :image, :magick => { :geometry => "100x100" }
e = Entry.new
e.image = upload(f("skanthak.png"))
assert e.save
e.reload
old_path = e.image
e.image = upload(f("kerb.jpg"))
assert e.save
assert "kerb.jpg", File.basename(e.image)
assert !File.exists?(old_path), "old image '#{old_path}' still exists"
img = read_image(e.image)
assert_max_image_size img, 100
end
def test_invalid_image
e = Entry.new
assert_nothing_raised { e.image = upload(f("invalid-image.jpg")) }
assert !e.valid?
end
def test_serializable
e = Entry.new
e.image = upload(f("skanthak.png"))
assert_nothing_raised {
flash = Marshal.dump(e)
e = Marshal.load(flash)
}
assert File.exists?(e.image)
end
def test_imagemagick_still_usable
e = Entry.new
assert_nothing_raised {
img = e.load_image_with_rmagick(file_path("skanthak.png"))
assert img.kind_of?(Magick::Image)
}
end
end
class RMagickRequiresImageTest < AbstractRMagickTest
def setup
Entry.file_column :image, :magick => {
:size => "100x100>",
:image_required => false,
:versions => {
:thumb => "80x80>",
:large => {:size => "200x200>", :lazy => true}
}
}
end
def test_image_required_with_image
e = Entry.new(:image => upload(f("skanthak.png")))
assert_max_image_size read_image(e.image), 100
assert e.valid?
end
def test_image_required_with_invalid_image
e = Entry.new(:image => upload(f("invalid-image.jpg")))
assert e.valid?, "did not ignore invalid image"
assert FileUtils.identical?(e.image, f("invalid-image.jpg")), "uploaded file has not been left alone"
end
def test_versions_with_invalid_image
e = Entry.new(:image => upload(f("invalid-image.jpg")))
assert e.valid?
image_state = e.send(:image_state)
assert_nil image_state.create_magick_version_if_needed(:thumb)
assert_nil image_state.create_magick_version_if_needed(:large)
assert_nil image_state.create_magick_version_if_needed("300x300>")
end
end
class RMagickCustomAttributesTest < AbstractRMagickTest
def assert_image_property(img, property, value, text = nil)
assert File.exists?(img), "the image does not exist"
assert_equal value, read_image(img).send(property), text
end
def test_simple_attributes
Entry.file_column :image, :magick => { :attributes => { :quality => 20 } }
e = Entry.new("image" => upload(f("kerb.jpg")))
assert_image_property e.image, :quality, 20, "the quality was not set"
end
def test_version_attributes
Entry.file_column :image, :magick => {
:versions => {
:thumb => { :attributes => { :quality => 20 } }
}
}
e = Entry.new("image" => upload(f("kerb.jpg")))
assert_image_property e.image("thumb"), :quality, 20, "the quality was not set"
end
def test_lazy_attributes
Entry.file_column :image, :magick => {
:versions => {
:thumb => { :attributes => { :quality => 20 }, :lazy => true }
}
}
e = Entry.new("image" => upload(f("kerb.jpg")))
e.send(:image_state).create_magick_version_if_needed(:thumb)
assert_image_property e.image("thumb"), :quality, 20, "the quality was not set"
end
end
class RMagickVersionsTest < AbstractRMagickTest
def setup
Entry.file_column :image, :magick => {:geometry => "200x200",
:versions => {
:thumb => "50x50",
:medium => {:geometry => "100x100", :name => "100_100"},
:large => {:geometry => "150x150", :lazy => true}
}
}
end
def test_should_create_thumb
e = Entry.new("image" => upload(f("skanthak.png")))
assert File.exists?(e.image("thumb")), "thumb-nail not created"
assert_max_image_size read_image(e.image("thumb")), 50
end
def test_version_name_can_be_different_from_key
e = Entry.new("image" => upload(f("skanthak.png")))
assert File.exists?(e.image("100_100"))
assert !File.exists?(e.image("medium"))
end
def test_should_not_create_lazy_versions
e = Entry.new("image" => upload(f("skanthak.png")))
assert !File.exists?(e.image("large")), "lazy versions should not be created unless needed"
end
def test_should_create_lazy_version_on_demand
e = Entry.new("image" => upload(f("skanthak.png")))
e.send(:image_state).create_magick_version_if_needed(:large)
assert File.exists?(e.image("large")), "lazy version should be created on demand"
assert_max_image_size read_image(e.image("large")), 150
end
def test_generated_name_should_not_change
e = Entry.new("image" => upload(f("skanthak.png")))
name1 = e.send(:image_state).create_magick_version_if_needed("50x50")
name2 = e.send(:image_state).create_magick_version_if_needed("50x50")
name3 = e.send(:image_state).create_magick_version_if_needed(:geometry => "50x50")
assert_equal name1, name2, "hash value has changed"
assert_equal name1, name3, "hash value has changed"
end
def test_should_create_version_with_string
e = Entry.new("image" => upload(f("skanthak.png")))
name = e.send(:image_state).create_magick_version_if_needed("32x32")
assert File.exists?(e.image(name))
assert_max_image_size read_image(e.image(name)), 32
end
def test_should_create_safe_auto_id
e = Entry.new("image" => upload(f("skanthak.png")))
name = e.send(:image_state).create_magick_version_if_needed("32x32")
assert_match /^[a-zA-Z0-9]+$/, name
end
end
class RMagickCroppingTest < AbstractRMagickTest
def setup
Entry.file_column :image, :magick => {:geometry => "200x200",
:versions => {
:thumb => {:crop => "1:1", :geometry => "50x50"}
}
}
end
def test_should_crop_image_on_upload
e = Entry.new("image" => upload(f("skanthak.png")))
img = read_image(e.image("thumb"))
assert_equal 50, img.rows
assert_equal 50, img.columns
end
end
class UrlForImageColumnTest < AbstractRMagickTest
include FileColumnHelper
def setup
Entry.file_column :image, :magick => {
:versions => {:thumb => "50x50"}
}
@request = RequestMock.new
end
def test_should_use_version_on_symbol_option
e = Entry.new(:image => upload(f("skanthak.png")))
url = url_for_image_column(e, "image", :thumb)
assert_match %r{^/entry/image/tmp/.+/thumb/skanthak.png$}, url
end
def test_should_use_string_as_size
e = Entry.new(:image => upload(f("skanthak.png")))
url = url_for_image_column(e, "image", "50x50")
assert_match %r{^/entry/image/tmp/.+/.+/skanthak.png$}, url
url =~ /\/([^\/]+)\/skanthak.png$/
dirname = $1
assert_max_image_size read_image(e.image(dirname)), 50
end
def test_should_accept_version_hash
e = Entry.new(:image => upload(f("skanthak.png")))
url = url_for_image_column(e, "image", :size => "50x50", :crop => "1:1", :name => "small")
assert_match %r{^/entry/image/tmp/.+/small/skanthak.png$}, url
img = read_image(e.image("small"))
assert_equal 50, img.rows
assert_equal 50, img.columns
end
end
class RMagickPermissionsTest < AbstractRMagickTest
def setup
Entry.file_column :image, :magick => {:geometry => "200x200",
:versions => {
:thumb => {:crop => "1:1", :geometry => "50x50"}
}
}, :permissions => 0616
end
def check_permissions(e)
assert_equal 0616, (File.stat(e.image).mode & 0777)
assert_equal 0616, (File.stat(e.image("thumb")).mode & 0777)
end
def test_permissions_with_rmagick
e = Entry.new(:image => upload(f("skanthak.png")))
check_permissions e
assert e.save
check_permissions e
end
end
class Entry
def transform_grey(img)
img.quantize(256, Magick::GRAYColorspace)
end
end
class RMagickTransformationTest < AbstractRMagickTest
def assert_transformed(image)
assert File.exists?(image), "the image does not exist"
assert 256 > read_image(image).number_colors, "the number of colors was not changed"
end
def test_simple_transformation
Entry.file_column :image, :magick => { :transformation => Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) } }
e = Entry.new("image" => upload(f("skanthak.png")))
assert_transformed(e.image)
end
def test_simple_version_transformation
Entry.file_column :image, :magick => {
:versions => { :thumb => Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) } }
}
e = Entry.new("image" => upload(f("skanthak.png")))
assert_transformed(e.image("thumb"))
end
def test_complex_version_transformation
Entry.file_column :image, :magick => {
:versions => {
:thumb => { :transformation => Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) } }
}
}
e = Entry.new("image" => upload(f("skanthak.png")))
assert_transformed(e.image("thumb"))
end
def test_lazy_transformation
Entry.file_column :image, :magick => {
:versions => {
:thumb => { :transformation => Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) }, :lazy => true }
}
}
e = Entry.new("image" => upload(f("skanthak.png")))
e.send(:image_state).create_magick_version_if_needed(:thumb)
assert_transformed(e.image("thumb"))
end
def test_simple_callback_transformation
Entry.file_column :image, :magick => :transform_grey
e = Entry.new(:image => upload(f("skanthak.png")))
assert_transformed(e.image)
end
def test_complex_callback_transformation
Entry.file_column :image, :magick => { :transformation => :transform_grey }
e = Entry.new(:image => upload(f("skanthak.png")))
assert_transformed(e.image)
end
end

View file

@ -0,0 +1,21 @@
require File.dirname(__FILE__) + '/abstract_unit'
require File.dirname(__FILE__) + '/fixtures/entry'
class RMagickViewOnlyTest < Test::Unit::TestCase
include FileColumnHelper
def setup
Entry.file_column :image
@request = RequestMock.new
end
def teardown
FileUtils.rm_rf File.dirname(__FILE__)+"/public/entry/"
end
def test_url_for_image_column_without_model_versions
e = Entry.new(:image => upload(f("skanthak.png")))
assert_nothing_raised { url_for_image_column e, "image", "50x50" }
end
end

View file

@ -0,0 +1,20 @@
Copyright (c) 2006-2008 Dr.-Ing. Stefan Kaes
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

60
vendor/plugins/sql_session_store/README vendored Executable file
View file

@ -0,0 +1,60 @@
== SqlSessionStore
See http://railsexpress.de/blog/articles/2005/12/19/roll-your-own-sql-session-store
Only Mysql, Postgres and Oracle are currently supported (others work,
but you won't see much performance improvement).
== Step 1
If you have generated your sessions table using rake db:sessions:create, go to Step 2
If you're using an old version of sql_session_store, run
script/generate sql_session_store DB
where DB is mysql, postgresql or oracle
Then run
rake migrate
or
rake db:migrate
for edge rails.
== Step 2
Add the code below after the initializer config section:
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS.
update(:database_manager => SqlSessionStore)
Finally, depending on your database type, add
SqlSessionStore.session_class = MysqlSession
or
SqlSessionStore.session_class = PostgresqlSession
or
SqlSessionStore.session_class = OracleSession
after the initializer section in environment.rb
== Step 3 (optional)
If you want to use a database separate from your default one to store
your sessions, specify a configuration in your database.yml file (say
sessions), and establish the connection on SqlSession in
environment.rb:
SqlSession.establish_connection :sessions
== IMPORTANT NOTES
1. The class name SQLSessionStore has changed to SqlSessionStore to
let Rails work its autoload magic.
2. You will need the binary drivers for Mysql or Postgresql.
These have been verified to work:
* ruby-postgres (0.7.1.2005.12.21) with postgreql 8.1
* ruby-mysql 2.7.1 with Mysql 4.1
* ruby-mysql 2.7.2 with Mysql 5.0

22
vendor/plugins/sql_session_store/Rakefile vendored Executable file
View file

@ -0,0 +1,22 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the sql_session_store plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the sql_session_store plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'SqlSessionStore'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,17 @@
Description:
The sql_session_store generator creates a migration for use with
the sql session store. It takes one argument: the database
type. Only mysql and postgreql are currently supported.
Example:
./script/generate sql_session_store mysql
This will create the following migration:
db/migrate/XXX_add_sql_session.rb
Use
./script/generate sql_session_store postgreql
to get a migration for postgres.

View file

@ -0,0 +1,25 @@
class SqlSessionStoreGenerator < Rails::Generator::NamedBase
def initialize(runtime_args, runtime_options = {})
runtime_args.insert(0, 'add_sql_session')
if runtime_args.include?('postgresql')
@_database = 'postgresql'
elsif runtime_args.include?('mysql')
@_database = 'mysql'
elsif runtime_args.include?('oracle')
@_database = 'oracle'
else
puts "error: database type not given.\nvalid arguments are: mysql or postgresql"
exit
end
super
end
def manifest
record do |m|
m.migration_template("migration.rb", 'db/migrate',
:assigns => { :migration_name => "SqlSessionStoreSetup", :database => @_database },
:migration_file_name => "sql_session_store_setup"
)
end
end
end

View file

@ -0,0 +1,38 @@
class <%= migration_name %> < ActiveRecord::Migration
class Session < ActiveRecord::Base; end
def self.up
c = ActiveRecord::Base.connection
if c.tables.include?('sessions')
if (columns = Session.column_names).include?('sessid')
rename_column :sessions, :sessid, :session_id
else
add_column :sessions, :session_id, :string unless columns.include?('session_id')
add_column :sessions, :data, :text unless columns.include?('data')
if columns.include?('created_on')
rename_column :sessions, :created_on, :created_at
else
add_column :sessions, :created_at, :timestamp unless columns.include?('created_at')
end
if columns.include?('updated_on')
rename_column :sessions, :updated_on, :updated_at
else
add_column :sessions, :updated_at, :timestamp unless columns.include?('updated_at')
end
end
else
create_table :sessions, :options => '<%= database == "mysql" ? "ENGINE=MyISAM" : "" %>' do |t|
t.column :session_id, :string
t.column :data, :text
t.column :created_at, :timestamp
t.column :updated_at, :timestamp
end
add_index :sessions, :session_id, :name => 'session_id_idx'
end
end
def self.down
raise IrreversibleMigration
end
end

1
vendor/plugins/sql_session_store/init.rb vendored Executable file
View file

@ -0,0 +1 @@
require 'sql_session_store'

2
vendor/plugins/sql_session_store/install.rb vendored Executable file
View file

@ -0,0 +1,2 @@
# Install hook code here
puts IO.read(File.join(File.dirname(__FILE__), 'README'))

View file

@ -0,0 +1,132 @@
require 'mysql'
# allow access to the real Mysql connection
class ActiveRecord::ConnectionAdapters::MysqlAdapter
attr_reader :connection
end
# MysqlSession is a down to the bare metal session store
# implementation to be used with +SQLSessionStore+. It is much faster
# than the default ActiveRecord implementation.
#
# The implementation assumes that the table column names are 'id',
# 'data', 'created_at' and 'updated_at'. If you want use other names,
# you will need to change the SQL statments in the code.
class MysqlSession
# if you need Rails components, and you have a pages which create
# new sessions, and embed components insides this pages that need
# session access, then you *must* set +eager_session_creation+ to
# true (as of Rails 1.0).
cattr_accessor :eager_session_creation
@@eager_session_creation = false
attr_accessor :id, :session_id, :data
def initialize(session_id, data)
@session_id = session_id
@data = data
@id = nil
end
class << self
# retrieve the session table connection and get the 'raw' Mysql connection from it
def session_connection
SqlSession.connection.connection
end
# try to find a session with a given +session_id+. returns nil if
# no such session exists. note that we don't retrieve
# +created_at+ and +updated_at+ as they are not accessed anywhyere
# outside this class
def find_session(session_id)
connection = session_connection
connection.query_with_result = true
session_id = Mysql::quote(session_id)
result = connection.query("SELECT id, data FROM sessions WHERE `session_id`='#{session_id}' LIMIT 1")
my_session = nil
# each is used below, as other methods barf on my 64bit linux machine
# I suspect this to be a bug in mysql-ruby
result.each do |row|
my_session = new(session_id, row[1])
my_session.id = row[0]
end
result.free
my_session
end
# create a new session with given +session_id+ and +data+
# and save it immediately to the database
def create_session(session_id, data)
session_id = Mysql::quote(session_id)
new_session = new(session_id, data)
if @@eager_session_creation
connection = session_connection
connection.query("INSERT INTO sessions (`created_at`, `updated_at`, `session_id`, `data`) VALUES (NOW(), NOW(), '#{session_id}', '#{Mysql::quote(data)}')")
new_session.id = connection.insert_id
end
new_session
end
# delete all sessions meeting a given +condition+. it is the
# caller's responsibility to pass a valid sql condition
def delete_all(condition=nil)
if condition
session_connection.query("DELETE FROM sessions WHERE #{condition}")
else
session_connection.query("DELETE FROM sessions")
end
end
end # class methods
# update session with given +data+.
# unlike the default implementation using ActiveRecord, updating of
# column `updated_at` will be done by the datbase itself
def update_session(data)
connection = self.class.session_connection
if @id
# if @id is not nil, this is a session already stored in the database
# update the relevant field using @id as key
connection.query("UPDATE sessions SET `updated_at`=NOW(), `data`='#{Mysql::quote(data)}' WHERE id=#{@id}")
else
# if @id is nil, we need to create a new session in the database
# and set @id to the primary key of the inserted record
connection.query("INSERT INTO sessions (`created_at`, `updated_at`, `session_id`, `data`) VALUES (NOW(), NOW(), '#{@session_id}', '#{Mysql::quote(data)}')")
@id = connection.insert_id
end
end
# destroy the current session
def destroy
self.class.delete_all("session_id='#{session_id}'")
end
end
__END__
# This software is released under the MIT license
#
# Copyright (c) 2005-2008 Stefan Kaes
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,143 @@
require 'oci8'
# allow access to the real Oracle connection
class ActiveRecord::ConnectionAdapters::OracleAdapter
attr_reader :connection
end
# OracleSession is a down to the bare metal session store
# implementation to be used with +SQLSessionStore+. It is much faster
# than the default ActiveRecord implementation.
#
# The implementation assumes that the table column names are 'id',
# 'session_id', 'data', 'created_at' and 'updated_at'. If you want use
# other names, you will need to change the SQL statments in the code.
#
# This table layout is compatible with ActiveRecordStore.
class OracleSession
# if you need Rails components, and you have a pages which create
# new sessions, and embed components insides these pages that need
# session access, then you *must* set +eager_session_creation+ to
# true (as of Rails 1.0). Not needed for Rails 1.1 and up.
cattr_accessor :eager_session_creation
@@eager_session_creation = false
attr_accessor :id, :session_id, :data
def initialize(session_id, data)
@session_id = session_id
@data = data
@id = nil
end
class << self
# retrieve the session table connection and get the 'raw' Oracle connection from it
def session_connection
SqlSession.connection.connection
end
# try to find a session with a given +session_id+. returns nil if
# no such session exists. note that we don't retrieve
# +created_at+ and +updated_at+ as they are not accessed anywhyere
# outside this class.
def find_session(session_id)
new_session = nil
connection = session_connection
result = connection.exec("SELECT id, data FROM sessions WHERE session_id = :a and rownum=1", session_id)
# Make sure to save the @id if we find an existing session
while row = result.fetch
new_session = new(session_id,row[1].read)
new_session.id = row[0]
end
result.close
new_session
end
# create a new session with given +session_id+ and +data+
# and save it immediately to the database
def create_session(session_id, data)
new_session = new(session_id, data)
if @@eager_session_creation
connection = session_connection
connection.exec("INSERT INTO sessions (id, created_at, updated_at, session_id, data)"+
" VALUES (sessions_seq.nextval, SYSDATE, SYSDATE, :a, :b)",
session_id, data)
result = connection.exec("SELECT sessions_seq.currval FROM dual")
row = result.fetch
new_session.id = row[0].to_i
end
new_session
end
# delete all sessions meeting a given +condition+. it is the
# caller's responsibility to pass a valid sql condition
def delete_all(condition=nil)
if condition
session_connection.exec("DELETE FROM sessions WHERE #{condition}")
else
session_connection.exec("DELETE FROM sessions")
end
end
end # class methods
# update session with given +data+.
# unlike the default implementation using ActiveRecord, updating of
# column `updated_at` will be done by the database itself
def update_session(data)
connection = self.class.session_connection
if @id
# if @id is not nil, this is a session already stored in the database
# update the relevant field using @id as key
connection.exec("UPDATE sessions SET updated_at = SYSDATE, data = :a WHERE id = :b",
data, @id)
else
# if @id is nil, we need to create a new session in the database
# and set @id to the primary key of the inserted record
connection.exec("INSERT INTO sessions (id, created_at, updated_at, session_id, data)"+
" VALUES (sessions_seq.nextval, SYSDATE, SYSDATE, :a, :b)",
@session_id, data)
result = connection.exec("SELECT sessions_seq.currval FROM dual")
row = result.fetch
@id = row[0].to_i
end
end
# destroy the current session
def destroy
self.class.delete_all("session_id='#{session_id}'")
end
end
__END__
# This software is released under the MIT license
#
# Copyright (c) 2006-2008 Stefan Kaes
# Copyright (c) 2006-2008 Tiago Macedo
# Copyright (c) 2007-2008 Nate Wiger
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,136 @@
require 'postgres'
# allow access to the real Mysql connection
class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
attr_reader :connection
end
# PostgresqlSession is a down to the bare metal session store
# implementation to be used with +SQLSessionStore+. It is much faster
# than the default ActiveRecord implementation.
#
# The implementation assumes that the table column names are 'id',
# 'session_id', 'data', 'created_at' and 'updated_at'. If you want use
# other names, you will need to change the SQL statments in the code.
#
# This table layout is compatible with ActiveRecordStore.
class PostgresqlSession
# if you need Rails components, and you have a pages which create
# new sessions, and embed components insides these pages that need
# session access, then you *must* set +eager_session_creation+ to
# true (as of Rails 1.0). Not needed for Rails 1.1 and up.
cattr_accessor :eager_session_creation
@@eager_session_creation = false
attr_accessor :id, :session_id, :data
def initialize(session_id, data)
@session_id = session_id
@data = data
@id = nil
end
class << self
# retrieve the session table connection and get the 'raw' Postgresql connection from it
def session_connection
SqlSession.connection.connection
end
# try to find a session with a given +session_id+. returns nil if
# no such session exists. note that we don't retrieve
# +created_at+ and +updated_at+ as they are not accessed anywhyere
# outside this class.
def find_session(session_id)
connection = session_connection
# postgres adds string delimiters when quoting, so strip them off
session_id = PGconn::quote(session_id)[1..-2]
result = connection.query("SELECT id, data FROM sessions WHERE session_id='#{session_id}' LIMIT 1")
my_session = nil
# each is used below, as other methods barf on my 64bit linux machine
# I suspect this to be a bug in mysql-ruby
result.each do |row|
my_session = new(session_id, row[1])
my_session.id = row[0]
end
result.clear
my_session
end
# create a new session with given +session_id+ and +data+
# and save it immediately to the database
def create_session(session_id, data)
# postgres adds string delimiters when quoting, so strip them off
session_id = PGconn::quote(session_id)[1..-2]
new_session = new(session_id, data)
if @@eager_session_creation
connection = session_connection
connection.query("INSERT INTO sessions (\"created_at\", \"updated_at\", \"session_id\", \"data\") VALUES (NOW(), NOW(), '#{session_id}', #{PGconn::quote(data)})")
new_session.id = connection.lastval
end
new_session
end
# delete all sessions meeting a given +condition+. it is the
# caller's responsibility to pass a valid sql condition
def delete_all(condition=nil)
if condition
session_connection.query("DELETE FROM sessions WHERE #{condition}")
else
session_connection.query("DELETE FROM sessions")
end
end
end # class methods
# update session with given +data+.
# unlike the default implementation using ActiveRecord, updating of
# column `updated_at` will be done by the database itself
def update_session(data)
connection = self.class.session_connection
if @id
# if @id is not nil, this is a session already stored in the database
# update the relevant field using @id as key
connection.query("UPDATE sessions SET \"updated_at\"=NOW(), \"data\"=#{PGconn::quote(data)} WHERE id=#{@id}")
else
# if @id is nil, we need to create a new session in the database
# and set @id to the primary key of the inserted record
connection.query("INSERT INTO sessions (\"created_at\", \"updated_at\", \"session_id\", \"data\") VALUES (NOW(), NOW(), '#{@session_id}', #{PGconn::quote(data)})")
@id = connection.lastval rescue connection.query("select lastval()").first[0]
end
end
# destroy the current session
def destroy
self.class.delete_all("session_id=#{PGconn.quote(session_id)}")
end
end
__END__
# This software is released under the MIT license
#
# Copyright (c) 2006-2008 Stefan Kaes
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,27 @@
# An ActiveRecord class which corresponds to the database table
# +sessions+. Functions +find_session+, +create_session+,
# +update_session+ and +destroy+ constitute the interface to class
# +SqlSessionStore+.
class SqlSession < ActiveRecord::Base
# this class should not be reloaded
def self.reloadable?
false
end
# retrieve session data for a given +session_id+ from the database,
# return nil if no such session exists
def self.find_session(session_id)
find :first, :conditions => "session_id='#{session_id}'"
end
# create a new session with given +session_id+ and +data+
def self.create_session(session_id, data)
new(:session_id => session_id, :data => data)
end
# update session data and store it in the database
def update_session(data)
update_attribute('data', data)
end
end

View file

@ -0,0 +1,116 @@
require 'active_record'
require 'cgi'
require 'cgi/session'
begin
require 'base64'
rescue LoadError
end
# +SqlSessionStore+ is a stripped down, optimized for speed version of
# class +ActiveRecordStore+.
class SqlSessionStore
# The class to be used for creating, retrieving and updating sessions.
# Defaults to SqlSessionStore::Session, which is derived from +ActiveRecord::Base+.
#
# In order to achieve acceptable performance you should implement
# your own session class, similar to the one provided for Myqsl.
#
# Only functions +find_session+, +create_session+,
# +update_session+ and +destroy+ are required. See file +mysql_session.rb+.
cattr_accessor :session_class
@@session_class = SqlSession
# Create a new SqlSessionStore instance.
#
# +session+ is the session for which this instance is being created.
#
# +option+ is currently ignored as no options are recognized.
def initialize(session, option=nil)
if @session = @@session_class.find_session(session.session_id)
@data = unmarshalize(@session.data)
else
@session = @@session_class.create_session(session.session_id, marshalize({}))
@data = {}
end
end
# Update the database and disassociate the session object
def close
if @session
@session.update_session(marshalize(@data))
@session = nil
end
end
# Delete the current session, disassociate and destroy session object
def delete
if @session
@session.destroy
@session = nil
end
end
# Restore session data from the session object
def restore
if @session
@data = unmarshalize(@session.data)
end
end
# Save session data in the session object
def update
if @session
@session.update_session(marshalize(@data))
end
end
private
if defined?(Base64)
def unmarshalize(data)
Marshal.load(Base64.decode64(data))
end
def marshalize(data)
Base64.encode64(Marshal.dump(data))
end
else
def unmarshalize(data)
Marshal.load(data.unpack("m").first)
end
def marshalize(data)
[Marshal.dump(data)].pack("m")
end
end
end
__END__
# This software is released under the MIT license
#
# Copyright (c) 2005-2008 Stefan Kaes
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,133 @@
require 'sqlite3'
# allow access to the real Sqlite connection
#class ActiveRecord::ConnectionAdapters::SQLiteAdapter
# attr_reader :connection
#end
# SqliteSession is a down to the bare metal session store
# implementation to be used with +SQLSessionStore+. It is much faster
# than the default ActiveRecord implementation.
#
# The implementation assumes that the table column names are 'id',
# 'data', 'created_at' and 'updated_at'. If you want use other names,
# you will need to change the SQL statments in the code.
class SqliteSession
# if you need Rails components, and you have a pages which create
# new sessions, and embed components insides this pages that need
# session access, then you *must* set +eager_session_creation+ to
# true (as of Rails 1.0).
cattr_accessor :eager_session_creation
@@eager_session_creation = false
attr_accessor :id, :session_id, :data
def initialize(session_id, data)
@session_id = session_id
@data = data
@id = nil
end
class << self
# retrieve the session table connection and get the 'raw' Sqlite connection from it
def session_connection
SqlSession.connection.instance_variable_get(:@connection)
end
# try to find a session with a given +session_id+. returns nil if
# no such session exists. note that we don't retrieve
# +created_at+ and +updated_at+ as they are not accessed anywhyere
# outside this class
def find_session(session_id)
connection = session_connection
session_id = SQLite3::Database.quote(session_id)
result = connection.execute("SELECT id, data FROM sessions WHERE `session_id`='#{session_id}' LIMIT 1")
my_session = nil
# each is used below, as other methods barf on my 64bit linux machine
# I suspect this to be a bug in sqlite-ruby
result.each do |row|
my_session = new(session_id, row[1])
my_session.id = row[0]
end
# result.free
my_session
end
# create a new session with given +session_id+ and +data+
# and save it immediately to the database
def create_session(session_id, data)
session_id = SQLite3::Database.quote(session_id)
new_session = new(session_id, data)
if @@eager_session_creation
connection = session_connection
connection.execute("INSERT INTO sessions ('id', `created_at`, `updated_at`, `session_id`, `data`) VALUES (NULL, datetime('now'), datetime('now'), '#{session_id}', '#{SQLite3::Database.quote(data)}')")
new_session.id = connection.last_insert_row_id()
end
new_session
end
# delete all sessions meeting a given +condition+. it is the
# caller's responsibility to pass a valid sql condition
def delete_all(condition=nil)
if condition
session_connection.execute("DELETE FROM sessions WHERE #{condition}")
else
session_connection.execute("DELETE FROM sessions")
end
end
end # class methods
# update session with given +data+.
# unlike the default implementation using ActiveRecord, updating of
# column `updated_at` will be done by the database itself
def update_session(data)
connection = SqlSession.connection.instance_variable_get(:@connection) #self.class.session_connection
if @id
# if @id is not nil, this is a session already stored in the database
# update the relevant field using @id as key
connection.execute("UPDATE sessions SET `updated_at`=datetime('now'), `data`='#{SQLite3::Database.quote(data)}' WHERE id=#{@id}")
else
# if @id is nil, we need to create a new session in the database
# and set @id to the primary key of the inserted record
connection.execute("INSERT INTO sessions ('id', `created_at`, `updated_at`, `session_id`, `data`) VALUES (NULL, datetime('now'), datetime('now'), '#{@session_id}', '#{SQLite3::Database.quote(data)}')")
@id = connection.last_insert_row_id()
end
end
# destroy the current session
def destroy
connection = SqlSession.connection.instance_variable_get(:@connection)
connection.execute("delete from sessions where session_id='#{session_id}'")
end
end
__END__
# This software is released under the MIT license
#
# Copyright (c) 2005-2008 Stefan Kaes
# Copyright (c) 2006-2008 Ted X Toth
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.