]> git.openstreetmap.org Git - rails.git/commitdiff
Updating to use Rails 2.1.2. Moving the gem dependancies to the config/environment...
authorShaun McDonald <shaun@shaunmcdonald.me.uk>
Tue, 28 Oct 2008 20:42:48 +0000 (20:42 +0000)
committerShaun McDonald <shaun@shaunmcdonald.me.uk>
Tue, 28 Oct 2008 20:42:48 +0000 (20:42 +0000)
46 files changed:
config/environment.rb
config/initializers/composite_primary_keys.rb [deleted file]
config/initializers/libxml.rb
vendor/plugins/deadlock_retry/README [new file with mode: 0644]
vendor/plugins/deadlock_retry/Rakefile [new file with mode: 0644]
vendor/plugins/deadlock_retry/init.rb [new file with mode: 0644]
vendor/plugins/deadlock_retry/lib/deadlock_retry.rb [new file with mode: 0644]
vendor/plugins/deadlock_retry/test/deadlock_retry_test.rb [new file with mode: 0644]
vendor/plugins/file_column/CHANGELOG [new file with mode: 0644]
vendor/plugins/file_column/README [new file with mode: 0644]
vendor/plugins/file_column/Rakefile [new file with mode: 0644]
vendor/plugins/file_column/TODO [new file with mode: 0644]
vendor/plugins/file_column/init.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/file_column.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/file_column_helper.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/file_compat.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/magick_file_column.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/rails_file_column.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/test_case.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/validations.rb [new file with mode: 0644]
vendor/plugins/file_column/test/abstract_unit.rb [new file with mode: 0644]
vendor/plugins/file_column/test/connection.rb [new file with mode: 0644]
vendor/plugins/file_column/test/file_column_helper_test.rb [new file with mode: 0644]
vendor/plugins/file_column/test/file_column_test.rb [new file with mode: 0755]
vendor/plugins/file_column/test/fixtures/entry.rb [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/invalid-image.jpg [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/kerb.jpg [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/mysql.sql [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/schema.rb [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/skanthak.png [new file with mode: 0644]
vendor/plugins/file_column/test/magick_test.rb [new file with mode: 0644]
vendor/plugins/file_column/test/magick_view_only_test.rb [new file with mode: 0644]
vendor/plugins/sql_session_store/LICENSE [new file with mode: 0644]
vendor/plugins/sql_session_store/README [new file with mode: 0755]
vendor/plugins/sql_session_store/Rakefile [new file with mode: 0755]
vendor/plugins/sql_session_store/generators/sql_session_store/USAGE [new file with mode: 0755]
vendor/plugins/sql_session_store/generators/sql_session_store/sql_session_store_generator.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/generators/sql_session_store/templates/migration.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/init.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/install.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/mysql_session.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/oracle_session.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/postgresql_session.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/sql_session.rb [new file with mode: 0644]
vendor/plugins/sql_session_store/lib/sql_session_store.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/sqlite_session.rb [new file with mode: 0755]

index e42e87eb70ab5ee11f20a342a8a81fe87149efba..e23f23bfa111b53b66d3fb97f0830bdb434c779e 100644 (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
diff --git a/config/initializers/composite_primary_keys.rb b/config/initializers/composite_primary_keys.rb
deleted file mode 100644 (file)
index 430bcfa..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-require 'rubygems'
-gem 'composite_primary_keys', '= 0.9.93'
-require 'composite_primary_keys'
index a1870dbab8b4aaaacfe5e5169bcb40e622f3dc11..4f71b6d0fd42ef0bad5b294a46d43633db46fb61 100644 (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
diff --git a/vendor/plugins/deadlock_retry/README b/vendor/plugins/deadlock_retry/README
new file mode 100644 (file)
index 0000000..b5937ce
--- /dev/null
@@ -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
\ No newline at end of file
diff --git a/vendor/plugins/deadlock_retry/Rakefile b/vendor/plugins/deadlock_retry/Rakefile
new file mode 100644 (file)
index 0000000..8063a6e
--- /dev/null
@@ -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
diff --git a/vendor/plugins/deadlock_retry/init.rb b/vendor/plugins/deadlock_retry/init.rb
new file mode 100644 (file)
index 0000000..e090f68
--- /dev/null
@@ -0,0 +1,2 @@
+require 'deadlock_retry'
+ActiveRecord::Base.send :include, DeadlockRetry
diff --git a/vendor/plugins/deadlock_retry/lib/deadlock_retry.rb b/vendor/plugins/deadlock_retry/lib/deadlock_retry.rb
new file mode 100644 (file)
index 0000000..413cb82
--- /dev/null
@@ -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
diff --git a/vendor/plugins/deadlock_retry/test/deadlock_retry_test.rb b/vendor/plugins/deadlock_retry/test/deadlock_retry_test.rb
new file mode 100644 (file)
index 0000000..db0f619
--- /dev/null
@@ -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
diff --git a/vendor/plugins/file_column/CHANGELOG b/vendor/plugins/file_column/CHANGELOG
new file mode 100644 (file)
index 0000000..bb4e5c6
--- /dev/null
@@ -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
diff --git a/vendor/plugins/file_column/README b/vendor/plugins/file_column/README
new file mode 100644 (file)
index 0000000..07a6e96
--- /dev/null
@@ -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
+
diff --git a/vendor/plugins/file_column/Rakefile b/vendor/plugins/file_column/Rakefile
new file mode 100644 (file)
index 0000000..0a24682
--- /dev/null
@@ -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
diff --git a/vendor/plugins/file_column/TODO b/vendor/plugins/file_column/TODO
new file mode 100644 (file)
index 0000000..d46e9fa
--- /dev/null
@@ -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?)
diff --git a/vendor/plugins/file_column/init.rb b/vendor/plugins/file_column/init.rb
new file mode 100644 (file)
index 0000000..d31ef1b
--- /dev/null
@@ -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)
\ No newline at end of file
diff --git a/vendor/plugins/file_column/lib/file_column.rb b/vendor/plugins/file_column/lib/file_column.rb
new file mode 100644 (file)
index 0000000..791a5be
--- /dev/null
@@ -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
+
+
diff --git a/vendor/plugins/file_column/lib/file_column_helper.rb b/vendor/plugins/file_column/lib/file_column_helper.rb
new file mode 100644 (file)
index 0000000..f4ebe38
--- /dev/null
@@ -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
diff --git a/vendor/plugins/file_column/lib/file_compat.rb b/vendor/plugins/file_column/lib/file_compat.rb
new file mode 100644 (file)
index 0000000..f284410
--- /dev/null
@@ -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
+
diff --git a/vendor/plugins/file_column/lib/magick_file_column.rb b/vendor/plugins/file_column/lib/magick_file_column.rb
new file mode 100644 (file)
index 0000000..c4dc06f
--- /dev/null
@@ -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
diff --git a/vendor/plugins/file_column/lib/rails_file_column.rb b/vendor/plugins/file_column/lib/rails_file_column.rb
new file mode 100644 (file)
index 0000000..af8c95a
--- /dev/null
@@ -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
diff --git a/vendor/plugins/file_column/lib/test_case.rb b/vendor/plugins/file_column/lib/test_case.rb
new file mode 100644 (file)
index 0000000..1416a1e
--- /dev/null
@@ -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
diff --git a/vendor/plugins/file_column/lib/validations.rb b/vendor/plugins/file_column/lib/validations.rb
new file mode 100644 (file)
index 0000000..5b961eb
--- /dev/null
@@ -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
diff --git a/vendor/plugins/file_column/test/abstract_unit.rb b/vendor/plugins/file_column/test/abstract_unit.rb
new file mode 100644 (file)
index 0000000..22bc53b
--- /dev/null
@@ -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
diff --git a/vendor/plugins/file_column/test/connection.rb b/vendor/plugins/file_column/test/connection.rb
new file mode 100644 (file)
index 0000000..a2f28ba
--- /dev/null
@@ -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"
diff --git a/vendor/plugins/file_column/test/file_column_helper_test.rb b/vendor/plugins/file_column/test/file_column_helper_test.rb
new file mode 100644 (file)
index 0000000..ffb2c43
--- /dev/null
@@ -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
diff --git a/vendor/plugins/file_column/test/file_column_test.rb b/vendor/plugins/file_column/test/file_column_test.rb
new file mode 100755 (executable)
index 0000000..452b781
--- /dev/null
@@ -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
+
diff --git a/vendor/plugins/file_column/test/fixtures/entry.rb b/vendor/plugins/file_column/test/fixtures/entry.rb
new file mode 100644 (file)
index 0000000..b9f7c95
--- /dev/null
@@ -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
diff --git a/vendor/plugins/file_column/test/fixtures/invalid-image.jpg b/vendor/plugins/file_column/test/fixtures/invalid-image.jpg
new file mode 100644 (file)
index 0000000..bd4933b
--- /dev/null
@@ -0,0 +1 @@
+this is certainly not a JPEG image
diff --git a/vendor/plugins/file_column/test/fixtures/kerb.jpg b/vendor/plugins/file_column/test/fixtures/kerb.jpg
new file mode 100644 (file)
index 0000000..083138e
Binary files /dev/null and b/vendor/plugins/file_column/test/fixtures/kerb.jpg differ
diff --git a/vendor/plugins/file_column/test/fixtures/mysql.sql b/vendor/plugins/file_column/test/fixtures/mysql.sql
new file mode 100644 (file)
index 0000000..55143f2
--- /dev/null
@@ -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;
+
diff --git a/vendor/plugins/file_column/test/fixtures/schema.rb b/vendor/plugins/file_column/test/fixtures/schema.rb
new file mode 100644 (file)
index 0000000..49b5ddb
--- /dev/null
@@ -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
diff --git a/vendor/plugins/file_column/test/fixtures/skanthak.png b/vendor/plugins/file_column/test/fixtures/skanthak.png
new file mode 100644 (file)
index 0000000..7415eb6
Binary files /dev/null and b/vendor/plugins/file_column/test/fixtures/skanthak.png differ
diff --git a/vendor/plugins/file_column/test/magick_test.rb b/vendor/plugins/file_column/test/magick_test.rb
new file mode 100644 (file)
index 0000000..0362719
--- /dev/null
@@ -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
diff --git a/vendor/plugins/file_column/test/magick_view_only_test.rb b/vendor/plugins/file_column/test/magick_view_only_test.rb
new file mode 100644 (file)
index 0000000..a7daa61
--- /dev/null
@@ -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
diff --git a/vendor/plugins/sql_session_store/LICENSE b/vendor/plugins/sql_session_store/LICENSE
new file mode 100644 (file)
index 0000000..5cb5c7b
--- /dev/null
@@ -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.
diff --git a/vendor/plugins/sql_session_store/README b/vendor/plugins/sql_session_store/README
new file mode 100755 (executable)
index 0000000..07b0833
--- /dev/null
@@ -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
diff --git a/vendor/plugins/sql_session_store/Rakefile b/vendor/plugins/sql_session_store/Rakefile
new file mode 100755 (executable)
index 0000000..0145def
--- /dev/null
@@ -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
diff --git a/vendor/plugins/sql_session_store/generators/sql_session_store/USAGE b/vendor/plugins/sql_session_store/generators/sql_session_store/USAGE
new file mode 100755 (executable)
index 0000000..1e3f58a
--- /dev/null
@@ -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.
diff --git a/vendor/plugins/sql_session_store/generators/sql_session_store/sql_session_store_generator.rb b/vendor/plugins/sql_session_store/generators/sql_session_store/sql_session_store_generator.rb
new file mode 100755 (executable)
index 0000000..6af6bd0
--- /dev/null
@@ -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
diff --git a/vendor/plugins/sql_session_store/generators/sql_session_store/templates/migration.rb b/vendor/plugins/sql_session_store/generators/sql_session_store/templates/migration.rb
new file mode 100755 (executable)
index 0000000..5126500
--- /dev/null
@@ -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
diff --git a/vendor/plugins/sql_session_store/init.rb b/vendor/plugins/sql_session_store/init.rb
new file mode 100755 (executable)
index 0000000..956151e
--- /dev/null
@@ -0,0 +1 @@
+require 'sql_session_store'\r
diff --git a/vendor/plugins/sql_session_store/install.rb b/vendor/plugins/sql_session_store/install.rb
new file mode 100755 (executable)
index 0000000..f40549d
--- /dev/null
@@ -0,0 +1,2 @@
+# Install hook code here
+puts IO.read(File.join(File.dirname(__FILE__), 'README'))
diff --git a/vendor/plugins/sql_session_store/lib/mysql_session.rb b/vendor/plugins/sql_session_store/lib/mysql_session.rb
new file mode 100755 (executable)
index 0000000..8c86384
--- /dev/null
@@ -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.
diff --git a/vendor/plugins/sql_session_store/lib/oracle_session.rb b/vendor/plugins/sql_session_store/lib/oracle_session.rb
new file mode 100755 (executable)
index 0000000..0b82f63
--- /dev/null
@@ -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.
+
diff --git a/vendor/plugins/sql_session_store/lib/postgresql_session.rb b/vendor/plugins/sql_session_store/lib/postgresql_session.rb
new file mode 100755 (executable)
index 0000000..d922913
--- /dev/null
@@ -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.
+
diff --git a/vendor/plugins/sql_session_store/lib/sql_session.rb b/vendor/plugins/sql_session_store/lib/sql_session.rb
new file mode 100644 (file)
index 0000000..19d2ad5
--- /dev/null
@@ -0,0 +1,27 @@
+# An ActiveRecord class which corresponds to the database table\r
+# +sessions+. Functions +find_session+, +create_session+,\r
+# +update_session+ and +destroy+ constitute the interface to class\r
+# +SqlSessionStore+.\r
+\r
+class SqlSession < ActiveRecord::Base\r
+  # this class should not be reloaded\r
+  def self.reloadable?\r
+    false\r
+  end\r
+\r
+  # retrieve session data for a given +session_id+ from the database,\r
+  # return nil if no such session exists\r
+  def self.find_session(session_id)\r
+    find :first, :conditions => "session_id='#{session_id}'"\r
+  end\r
+\r
+  # create a new session with given +session_id+ and +data+\r
+  def self.create_session(session_id, data)\r
+    new(:session_id => session_id, :data => data)\r
+  end\r
+\r
+  # update session data and store it in the database\r
+  def update_session(data)\r
+    update_attribute('data', data)\r
+  end\r
+end\r
diff --git a/vendor/plugins/sql_session_store/lib/sql_session_store.rb b/vendor/plugins/sql_session_store/lib/sql_session_store.rb
new file mode 100755 (executable)
index 0000000..8b0ff15
--- /dev/null
@@ -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.
+
diff --git a/vendor/plugins/sql_session_store/lib/sqlite_session.rb b/vendor/plugins/sql_session_store/lib/sqlite_session.rb
new file mode 100755 (executable)
index 0000000..822b232
--- /dev/null
@@ -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.