From: Shaun McDonald Date: Tue, 28 Oct 2008 20:42:48 +0000 (+0000) Subject: Updating to use Rails 2.1.2. Moving the gem dependancies to the config/environment... X-Git-Tag: live~7619^2~234 X-Git-Url: https://git.openstreetmap.org/rails.git/commitdiff_plain/252c2f70225595312151bcf77ee7c8f5aac0c831 Updating to use Rails 2.1.2. Moving the gem dependancies to the config/environment.rb file. Moving the vendor/plugins externals into our svn. --- diff --git a/config/environment.rb b/config/environment.rb index e42e87eb7..e23f23bfa 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -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 index 430bcfac2..000000000 --- a/config/initializers/composite_primary_keys.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'rubygems' -gem 'composite_primary_keys', '= 0.9.93' -require 'composite_primary_keys' diff --git a/config/initializers/libxml.rb b/config/initializers/libxml.rb index a1870dbab..4f71b6d0f 100644 --- a/config/initializers/libxml.rb +++ b/config/initializers/libxml.rb @@ -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 index 000000000..b5937ce0e --- /dev/null +++ b/vendor/plugins/deadlock_retry/README @@ -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 index 000000000..8063a6ed4 --- /dev/null +++ b/vendor/plugins/deadlock_retry/Rakefile @@ -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 index 000000000..e090f68af --- /dev/null +++ b/vendor/plugins/deadlock_retry/init.rb @@ -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 index 000000000..413cb823c --- /dev/null +++ b/vendor/plugins/deadlock_retry/lib/deadlock_retry.rb @@ -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 < 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 index 000000000..db0f6195d --- /dev/null +++ b/vendor/plugins/deadlock_retry/test/deadlock_retry_test.rb @@ -0,0 +1,65 @@ +begin + require 'active_record' +rescue LoadError + if ENV['ACTIVERECORD_PATH'].nil? + abort < 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 index 000000000..07a6e9661 --- /dev/null +++ b/vendor/plugins/file_column/README @@ -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//filename.ext" + +* Newly uploaded files will be stored in "public/entry/tmp//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 index 000000000..0a2468248 --- /dev/null +++ b/vendor/plugins/file_column/Rakefile @@ -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 index 000000000..d46e9fa80 --- /dev/null +++ b/vendor/plugins/file_column/TODO @@ -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 index 000000000..d31ef1b9c --- /dev/null +++ b/vendor/plugins/file_column/init.rb @@ -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 index 000000000..791a5be3e --- /dev/null +++ b/vendor/plugins/file_column/lib/file_column.rb @@ -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 ActiveRecord::Base + # as class methods, so that you can use them in your models. + # + # == Generated Methods + # + # After calling "file_column :image" as in the example above, a number of instance methods + # will automatically be generated, all prefixed by "image": + # + # * Entry#image=(uploaded_file): 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). + # * Entry#image(subdir=nil): This will return an absolute path (as a + # string) to the currently uploaded file + # or nil if no file has been uploaded + # * Entry#image_relative_path(subdir=nil): 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. + # * Entry#image_just_uploaded?: 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 + # ActiveRecord::Base#attributes or ActiveRecord::Base#[] 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 0644 (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 :permissions options. The value + # you give here is passed directly to File::chmod, 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//test.png" and be moved to + # "public/entry/image//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 + # image("thumb"), 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 :fix_file_extensions 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 :mime_extensions 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 :file_exec 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/" for unsaved models. + # + # You can specify a custom root_path by setting the :root_path option. + # + # You can specify a custom storage_dir by setting the :storage_dir option. + # + # For setting a static storage_dir that doesn't change with respect to a particular + # instance, you assign :storage_dir 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 + # :store_dir 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 :store_dir. You can override this via the + # :tmp_base_dir 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 index 000000000..f4ebe38e7 --- /dev/null +++ b/vendor/plugins/file_column/lib/file_column_helper.rb @@ -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): + # + # + # + # + # 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 @entry) 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 @entry: + # + # /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 :absolute => + # true 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 :symbol will select a version defined in the model + # via FileColumn::Magick's :versions feature. + # * a geometry_string will dynamically create an + # image resized as specified by geometry_string. The image will + # be stored so that it does not have to be recomputed the next time the + # same version string is used. + # * some_hash will dynamically create an image + # that is created according to the options in some_hash. 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 :name => name 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 :versions 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 index 000000000..f284410a3 --- /dev/null +++ b/vendor/plugins/file_column/lib/file_compat.rb @@ -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 index 000000000..c4dc06fc3 --- /dev/null +++ b/vendor/plugins/file_column/lib/magick_file_column.rb @@ -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 :magick 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 :size 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 :magick option, you can set the :image_required + # 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 :name 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 :attributes 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 Proc object, you can also give a + # Symbol, 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. + # + # Note: 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 index 000000000..af8c95a84 --- /dev/null +++ b/vendor/plugins/file_column/lib/rails_file_column.rb @@ -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 index 000000000..1416a1e7f --- /dev/null +++ b/vendor/plugins/file_column/lib/test_case.rb @@ -0,0 +1,124 @@ +require 'test/unit' + +# Add the methods +upload+, the setup_file_fixtures and +# teardown_file_fixtures 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 + # + # * path The path to the file to upload. + # * content_type The MIME type of the file. If it is :guess, + # 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 + # setup 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 index 000000000..5b961eb9c --- /dev/null +++ b/vendor/plugins/file_column/lib/validations.rb @@ -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 ActiveRecord::Base + # 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: + # * :in => 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: + # * :in => 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 :min option. + # + # Required options: + # * :min => 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 index 000000000..22bc53b70 --- /dev/null +++ b/vendor/plugins/file_column/test/abstract_unit.rb @@ -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 index 000000000..a2f28baca --- /dev/null +++ b/vendor/plugins/file_column/test/connection.rb @@ -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 index 000000000..ffb2c43b8 --- /dev/null +++ b/vendor/plugins/file_column/test/file_column_helper_test.rb @@ -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 index 000000000..452b7815d --- /dev/null +++ b/vendor/plugins/file_column/test/file_column_test.rb @@ -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 index 000000000..b9f7c954d --- /dev/null +++ b/vendor/plugins/file_column/test/fixtures/entry.rb @@ -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 index 000000000..bd4933b43 --- /dev/null +++ b/vendor/plugins/file_column/test/fixtures/invalid-image.jpg @@ -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 index 000000000..083138e4f 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 index 000000000..55143f27b --- /dev/null +++ b/vendor/plugins/file_column/test/fixtures/mysql.sql @@ -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 index 000000000..49b5ddbaa --- /dev/null +++ b/vendor/plugins/file_column/test/fixtures/schema.rb @@ -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 index 000000000..7415eb6e4 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 index 000000000..036271927 --- /dev/null +++ b/vendor/plugins/file_column/test/magick_test.rb @@ -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 index 000000000..a7daa6172 --- /dev/null +++ b/vendor/plugins/file_column/test/magick_view_only_test.rb @@ -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 index 000000000..5cb5c7b95 --- /dev/null +++ b/vendor/plugins/sql_session_store/LICENSE @@ -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 index 000000000..07b083343 --- /dev/null +++ b/vendor/plugins/sql_session_store/README @@ -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 index 000000000..0145def2f --- /dev/null +++ b/vendor/plugins/sql_session_store/Rakefile @@ -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 index 000000000..1e3f58a67 --- /dev/null +++ b/vendor/plugins/sql_session_store/generators/sql_session_store/USAGE @@ -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 index 000000000..6af6bd0bc --- /dev/null +++ b/vendor/plugins/sql_session_store/generators/sql_session_store/sql_session_store_generator.rb @@ -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 index 000000000..512650068 --- /dev/null +++ b/vendor/plugins/sql_session_store/generators/sql_session_store/templates/migration.rb @@ -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 index 000000000..956151ea7 --- /dev/null +++ b/vendor/plugins/sql_session_store/init.rb @@ -0,0 +1 @@ +require 'sql_session_store' diff --git a/vendor/plugins/sql_session_store/install.rb b/vendor/plugins/sql_session_store/install.rb new file mode 100755 index 000000000..f40549dfe --- /dev/null +++ b/vendor/plugins/sql_session_store/install.rb @@ -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 index 000000000..8c86384c9 --- /dev/null +++ b/vendor/plugins/sql_session_store/lib/mysql_session.rb @@ -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 index 000000000..0b82f6391 --- /dev/null +++ b/vendor/plugins/sql_session_store/lib/oracle_session.rb @@ -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 index 000000000..d922913aa --- /dev/null +++ b/vendor/plugins/sql_session_store/lib/postgresql_session.rb @@ -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 index 000000000..19d2ad51e --- /dev/null +++ b/vendor/plugins/sql_session_store/lib/sql_session.rb @@ -0,0 +1,27 @@ +# An ActiveRecord class which corresponds to the database table +# +sessions+. Functions +find_session+, +create_session+, +# +update_session+ and +destroy+ constitute the interface to class +# +SqlSessionStore+. + +class SqlSession < ActiveRecord::Base + # this class should not be reloaded + def self.reloadable? + false + end + + # retrieve session data for a given +session_id+ from the database, + # return nil if no such session exists + def self.find_session(session_id) + find :first, :conditions => "session_id='#{session_id}'" + end + + # create a new session with given +session_id+ and +data+ + def self.create_session(session_id, data) + new(:session_id => session_id, :data => data) + end + + # update session data and store it in the database + def update_session(data) + update_attribute('data', data) + end +end 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 index 000000000..8b0ff156f --- /dev/null +++ b/vendor/plugins/sql_session_store/lib/sql_session_store.rb @@ -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 index 000000000..822b23231 --- /dev/null +++ b/vendor/plugins/sql_session_store/lib/sqlite_session.rb @@ -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.