From 252c2f70225595312151bcf77ee7c8f5aac0c831 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 28 Oct 2008 20:42:48 +0000 Subject: [PATCH] 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. --- config/environment.rb | 12 +- config/initializers/composite_primary_keys.rb | 3 - config/initializers/libxml.rb | 7 +- vendor/plugins/deadlock_retry/README | 10 + vendor/plugins/deadlock_retry/Rakefile | 10 + vendor/plugins/deadlock_retry/init.rb | 2 + .../deadlock_retry/lib/deadlock_retry.rb | 58 ++ .../test/deadlock_retry_test.rb | 65 ++ vendor/plugins/file_column/CHANGELOG | 69 ++ vendor/plugins/file_column/README | 54 ++ vendor/plugins/file_column/Rakefile | 36 + vendor/plugins/file_column/TODO | 6 + vendor/plugins/file_column/init.rb | 13 + vendor/plugins/file_column/lib/file_column.rb | 720 ++++++++++++++++++ .../file_column/lib/file_column_helper.rb | 150 ++++ vendor/plugins/file_column/lib/file_compat.rb | 28 + .../file_column/lib/magick_file_column.rb | 260 +++++++ .../file_column/lib/rails_file_column.rb | 19 + vendor/plugins/file_column/lib/test_case.rb | 124 +++ vendor/plugins/file_column/lib/validations.rb | 112 +++ .../plugins/file_column/test/abstract_unit.rb | 63 ++ vendor/plugins/file_column/test/connection.rb | 17 + .../test/file_column_helper_test.rb | 97 +++ .../file_column/test/file_column_test.rb | 650 ++++++++++++++++ .../file_column/test/fixtures/entry.rb | 32 + .../test/fixtures/invalid-image.jpg | 1 + .../file_column/test/fixtures/kerb.jpg | Bin 0 -> 87582 bytes .../file_column/test/fixtures/mysql.sql | 25 + .../file_column/test/fixtures/schema.rb | 10 + .../file_column/test/fixtures/skanthak.png | Bin 0 -> 12629 bytes .../plugins/file_column/test/magick_test.rb | 380 +++++++++ .../file_column/test/magick_view_only_test.rb | 21 + vendor/plugins/sql_session_store/LICENSE | 20 + vendor/plugins/sql_session_store/README | 60 ++ vendor/plugins/sql_session_store/Rakefile | 22 + .../generators/sql_session_store/USAGE | 17 + .../sql_session_store_generator.rb | 25 + .../sql_session_store/templates/migration.rb | 38 + vendor/plugins/sql_session_store/init.rb | 1 + vendor/plugins/sql_session_store/install.rb | 2 + .../sql_session_store/lib/mysql_session.rb | 132 ++++ .../sql_session_store/lib/oracle_session.rb | 143 ++++ .../lib/postgresql_session.rb | 136 ++++ .../sql_session_store/lib/sql_session.rb | 27 + .../lib/sql_session_store.rb | 116 +++ .../sql_session_store/lib/sqlite_session.rb | 133 ++++ 46 files changed, 3919 insertions(+), 7 deletions(-) delete mode 100644 config/initializers/composite_primary_keys.rb create mode 100644 vendor/plugins/deadlock_retry/README create mode 100644 vendor/plugins/deadlock_retry/Rakefile create mode 100644 vendor/plugins/deadlock_retry/init.rb create mode 100644 vendor/plugins/deadlock_retry/lib/deadlock_retry.rb create mode 100644 vendor/plugins/deadlock_retry/test/deadlock_retry_test.rb create mode 100644 vendor/plugins/file_column/CHANGELOG create mode 100644 vendor/plugins/file_column/README create mode 100644 vendor/plugins/file_column/Rakefile create mode 100644 vendor/plugins/file_column/TODO create mode 100644 vendor/plugins/file_column/init.rb create mode 100644 vendor/plugins/file_column/lib/file_column.rb create mode 100644 vendor/plugins/file_column/lib/file_column_helper.rb create mode 100644 vendor/plugins/file_column/lib/file_compat.rb create mode 100644 vendor/plugins/file_column/lib/magick_file_column.rb create mode 100644 vendor/plugins/file_column/lib/rails_file_column.rb create mode 100644 vendor/plugins/file_column/lib/test_case.rb create mode 100644 vendor/plugins/file_column/lib/validations.rb create mode 100644 vendor/plugins/file_column/test/abstract_unit.rb create mode 100644 vendor/plugins/file_column/test/connection.rb create mode 100644 vendor/plugins/file_column/test/file_column_helper_test.rb create mode 100755 vendor/plugins/file_column/test/file_column_test.rb create mode 100644 vendor/plugins/file_column/test/fixtures/entry.rb create mode 100644 vendor/plugins/file_column/test/fixtures/invalid-image.jpg create mode 100644 vendor/plugins/file_column/test/fixtures/kerb.jpg create mode 100644 vendor/plugins/file_column/test/fixtures/mysql.sql create mode 100644 vendor/plugins/file_column/test/fixtures/schema.rb create mode 100644 vendor/plugins/file_column/test/fixtures/skanthak.png create mode 100644 vendor/plugins/file_column/test/magick_test.rb create mode 100644 vendor/plugins/file_column/test/magick_view_only_test.rb create mode 100644 vendor/plugins/sql_session_store/LICENSE create mode 100755 vendor/plugins/sql_session_store/README create mode 100755 vendor/plugins/sql_session_store/Rakefile create mode 100755 vendor/plugins/sql_session_store/generators/sql_session_store/USAGE create mode 100755 vendor/plugins/sql_session_store/generators/sql_session_store/sql_session_store_generator.rb create mode 100755 vendor/plugins/sql_session_store/generators/sql_session_store/templates/migration.rb create mode 100755 vendor/plugins/sql_session_store/init.rb create mode 100755 vendor/plugins/sql_session_store/install.rb create mode 100755 vendor/plugins/sql_session_store/lib/mysql_session.rb create mode 100755 vendor/plugins/sql_session_store/lib/oracle_session.rb create mode 100755 vendor/plugins/sql_session_store/lib/postgresql_session.rb create mode 100644 vendor/plugins/sql_session_store/lib/sql_session.rb create mode 100755 vendor/plugins/sql_session_store/lib/sql_session_store.rb create mode 100755 vendor/plugins/sql_session_store/lib/sqlite_session.rb 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 0000000000000000000000000000000000000000..083138e4fe1702478499964ecd9fb3fbcfd351bf GIT binary patch literal 87582 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

3Q=3cgdr&fN>PAx03Cs@q4q%H`>q7>OZ}6? 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 nOx<=ys)O30l)Qw!nAAE1f 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 0000000000000000000000000000000000000000..7415eb6e4cbffb07f962be309205c15763935e42 GIT binary patch literal 12629 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®X?*?>a>xXxx9zrVTV|*o%D{-6 z=&YL=Y`^W-fB*LN>D$LopFe&0n>;-J=l3t)pT0>S4o*^0MT*+IE;L;zy6t_8Bqf;) z<;3DaQ97m{&r2~=brXfCO?lSD=U}Q(71P6)3K2>sRphV#eto)a|5%q_9^a0fSMoEY zuS+^7#`*v0mn7+#+_vp@yG4X}!QsctDR3}z_f^&RHeUXG+OGBK%h!K(KL7SbUM}y} zvYIoqwN}is%!o!N#mSL?Z*%A|NaTeyl>vuJ$#Sr>(`WdG?M=VOMM2VR2sQS00000NkvXXu0mjf Dba$3x literal 0 HcmV?d00001 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. -- 2.39.5