From cf1b60e0cfa7d1a8f4a80d686649cc12e73a634e Mon Sep 17 00:00:00 2001 From: hukl Date: Fri, 24 Apr 2009 11:43:08 +0200 Subject: Integrated basic Asset upload functionality. You can upload files now and use their url in pages. --- vendor/plugins/paperclip/lib/paperclip.rb | 318 ++++++++++++++++ .../plugins/paperclip/lib/paperclip/attachment.rb | 403 +++++++++++++++++++++ .../lib/paperclip/callback_compatability.rb | 33 ++ vendor/plugins/paperclip/lib/paperclip/geometry.rb | 115 ++++++ vendor/plugins/paperclip/lib/paperclip/iostream.rb | 58 +++ vendor/plugins/paperclip/lib/paperclip/matchers.rb | 4 + .../matchers/have_attached_file_matcher.rb | 49 +++ .../validate_attachment_content_type_matcher.rb | 66 ++++ .../validate_attachment_presence_matcher.rb | 48 +++ .../matchers/validate_attachment_size_matcher.rb | 83 +++++ .../plugins/paperclip/lib/paperclip/processor.rb | 48 +++ vendor/plugins/paperclip/lib/paperclip/storage.rb | 236 ++++++++++++ .../plugins/paperclip/lib/paperclip/thumbnail.rb | 70 ++++ vendor/plugins/paperclip/lib/paperclip/upfile.rb | 48 +++ 14 files changed, 1579 insertions(+) create mode 100644 vendor/plugins/paperclip/lib/paperclip.rb create mode 100644 vendor/plugins/paperclip/lib/paperclip/attachment.rb create mode 100644 vendor/plugins/paperclip/lib/paperclip/callback_compatability.rb create mode 100644 vendor/plugins/paperclip/lib/paperclip/geometry.rb create mode 100644 vendor/plugins/paperclip/lib/paperclip/iostream.rb create mode 100644 vendor/plugins/paperclip/lib/paperclip/matchers.rb create mode 100644 vendor/plugins/paperclip/lib/paperclip/matchers/have_attached_file_matcher.rb create mode 100644 vendor/plugins/paperclip/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb create mode 100644 vendor/plugins/paperclip/lib/paperclip/matchers/validate_attachment_presence_matcher.rb create mode 100644 vendor/plugins/paperclip/lib/paperclip/matchers/validate_attachment_size_matcher.rb create mode 100644 vendor/plugins/paperclip/lib/paperclip/processor.rb create mode 100644 vendor/plugins/paperclip/lib/paperclip/storage.rb create mode 100644 vendor/plugins/paperclip/lib/paperclip/thumbnail.rb create mode 100644 vendor/plugins/paperclip/lib/paperclip/upfile.rb (limited to 'vendor/plugins/paperclip/lib') diff --git a/vendor/plugins/paperclip/lib/paperclip.rb b/vendor/plugins/paperclip/lib/paperclip.rb new file mode 100644 index 0000000..74c3d79 --- /dev/null +++ b/vendor/plugins/paperclip/lib/paperclip.rb @@ -0,0 +1,318 @@ +# Paperclip allows file attachments that are stored in the filesystem. All graphical +# transformations are done using the Graphics/ImageMagick command line utilities and +# are stored in Tempfiles until the record is saved. Paperclip does not require a +# separate model for storing the attachment's information, instead adding a few simple +# columns to your table. +# +# Author:: Jon Yurek +# Copyright:: Copyright (c) 2008 thoughtbot, inc. +# License:: MIT License (http://www.opensource.org/licenses/mit-license.php) +# +# Paperclip defines an attachment as any file, though it makes special considerations +# for image files. You can declare that a model has an attached file with the +# +has_attached_file+ method: +# +# class User < ActiveRecord::Base +# has_attached_file :avatar, :styles => { :thumb => "100x100" } +# end +# +# user = User.new +# user.avatar = params[:user][:avatar] +# user.avatar.url +# # => "/users/avatars/4/original_me.jpg" +# user.avatar.url(:thumb) +# # => "/users/avatars/4/thumb_me.jpg" +# +# See the +has_attached_file+ documentation for more details. + +require 'tempfile' +require 'paperclip/upfile' +require 'paperclip/iostream' +require 'paperclip/geometry' +require 'paperclip/processor' +require 'paperclip/thumbnail' +require 'paperclip/storage' +require 'paperclip/attachment' +if defined? RAILS_ROOT + Dir.glob(File.join(File.expand_path(RAILS_ROOT), "lib", "paperclip_processors", "*.rb")).each do |processor| + require processor + end +end + +# The base module that gets included in ActiveRecord::Base. See the +# documentation for Paperclip::ClassMethods for more useful information. +module Paperclip + + VERSION = "2.2.8" + + class << self + # Provides configurability to Paperclip. There are a number of options available, such as: + # * whiny_thumbnails: Will raise an error if Paperclip cannot process thumbnails of + # an uploaded image. Defaults to true. + # * log: Logs progress to the Rails log. Uses ActiveRecord's logger, so honors + # log levels, etc. Defaults to true. + # * command_path: Defines the path at which to find the command line + # programs if they are not visible to Rails the system's search path. Defaults to + # nil, which uses the first executable found in the user's search path. + # * image_magick_path: Deprecated alias of command_path. + def options + @options ||= { + :whiny_thumbnails => true, + :image_magick_path => nil, + :command_path => nil, + :log => true, + :swallow_stderr => true + } + end + + def path_for_command command #:nodoc: + if options[:image_magick_path] + warn("[DEPRECATION] :image_magick_path is deprecated and will be removed. Use :command_path instead") + end + path = [options[:command_path] || options[:image_magick_path], command].compact + File.join(*path) + end + + def interpolates key, &block + Paperclip::Attachment.interpolations[key] = block + end + + # The run method takes a command to execute and a string of parameters + # that get passed to it. The command is prefixed with the :command_path + # option from Paperclip.options. If you have many commands to run and + # they are in different paths, the suggested course of action is to + # symlink them so they are all in the same directory. + # + # If the command returns with a result code that is not one of the + # expected_outcodes, a PaperclipCommandLineError will be raised. Generally + # a code of 0 is expected, but a list of codes may be passed if necessary. + def run cmd, params = "", expected_outcodes = 0 + command = %Q<#{%Q[#{path_for_command(cmd)} #{params}].gsub(/\s+/, " ")}> + command = "#{command} 2>#{bit_bucket}" if Paperclip.options[:swallow_stderr] + output = `#{command}` + unless [expected_outcodes].flatten.include?($?.exitstatus) + raise PaperclipCommandLineError, "Error while running #{cmd}" + end + output + end + + def bit_bucket #:nodoc: + File.exists?("/dev/null") ? "/dev/null" : "NUL" + end + + def included base #:nodoc: + base.extend ClassMethods + unless base.respond_to?(:define_callbacks) + base.send(:include, Paperclip::CallbackCompatability) + end + end + + def processor name #:nodoc: + name = name.to_s.camelize + processor = Paperclip.const_get(name) + unless processor.ancestors.include?(Paperclip::Processor) + raise PaperclipError.new("Processor #{name} was not found") + end + processor + end + end + + class PaperclipError < StandardError #:nodoc: + end + + class PaperclipCommandLineError < StandardError #:nodoc: + end + + class NotIdentifiedByImageMagickError < PaperclipError #:nodoc: + end + + module ClassMethods + # +has_attached_file+ gives the class it is called on an attribute that maps to a file. This + # is typically a file stored somewhere on the filesystem and has been uploaded by a user. + # The attribute returns a Paperclip::Attachment object which handles the management of + # that file. The intent is to make the attachment as much like a normal attribute. The + # thumbnails will be created when the new file is assigned, but they will *not* be saved + # until +save+ is called on the record. Likewise, if the attribute is set to +nil+ is + # called on it, the attachment will *not* be deleted until +save+ is called. See the + # Paperclip::Attachment documentation for more specifics. There are a number of options + # you can set to change the behavior of a Paperclip attachment: + # * +url+: The full URL of where the attachment is publically accessible. This can just + # as easily point to a directory served directly through Apache as it can to an action + # that can control permissions. You can specify the full domain and path, but usually + # just an absolute path is sufficient. The leading slash *must* be included manually for + # absolute paths. The default value is + # "/system/:attachment/:id/:style/:basename.:extension". See + # Paperclip::Attachment#interpolate for more information on variable interpolaton. + # :url => "/:class/:attachment/:id/:style_:basename.:extension" + # :url => "http://some.other.host/stuff/:class/:id_:extension" + # * +default_url+: The URL that will be returned if there is no attachment assigned. + # This field is interpolated just as the url is. The default value is + # "/:attachment/:style/missing.png" + # has_attached_file :avatar, :default_url => "/images/default_:style_avatar.png" + # User.new.avatar_url(:small) # => "/images/default_small_avatar.png" + # * +styles+: A hash of thumbnail styles and their geometries. You can find more about + # geometry strings at the ImageMagick website + # (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip + # also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally + # inside the dimensions and then crop the rest off (weighted at the center). The + # default value is to generate no thumbnails. + # * +default_style+: The thumbnail style that will be used by default URLs. + # Defaults to +original+. + # has_attached_file :avatar, :styles => { :normal => "100x100#" }, + # :default_style => :normal + # user.avatar.url # => "/avatars/23/normal_me.png" + # * +whiny_thumbnails+: Will raise an error if Paperclip cannot post_process an uploaded file due + # to a command line error. This will override the global setting for this attachment. + # Defaults to true. + # * +convert_options+: When creating thumbnails, use this free-form options + # field to pass in various convert command options. Typical options are "-strip" to + # remove all Exif data from the image (save space for thumbnails and avatars) or + # "-depth 8" to specify the bit depth of the resulting conversion. See ImageMagick + # convert documentation for more options: (http://www.imagemagick.org/script/convert.php) + # Note that this option takes a hash of options, each of which correspond to the style + # of thumbnail being generated. You can also specify :all as a key, which will apply + # to all of the thumbnails being generated. If you specify options for the :original, + # it would be best if you did not specify destructive options, as the intent of keeping + # the original around is to regenerate all the thumbnails when requirements change. + # has_attached_file :avatar, :styles => { :large => "300x300", :negative => "100x100" } + # :convert_options => { + # :all => "-strip", + # :negative => "-negate" + # } + # * +storage+: Chooses the storage backend where the files will be stored. The current + # choices are :filesystem and :s3. The default is :filesystem. Make sure you read the + # documentation for Paperclip::Storage::Filesystem and Paperclip::Storage::S3 + # for backend-specific options. + def has_attached_file name, options = {} + include InstanceMethods + + write_inheritable_attribute(:attachment_definitions, {}) if attachment_definitions.nil? + attachment_definitions[name] = {:validations => {}}.merge(options) + + after_save :save_attached_files + before_destroy :destroy_attached_files + + define_callbacks :before_post_process, :after_post_process + define_callbacks :"before_#{name}_post_process", :"after_#{name}_post_process" + + define_method name do |*args| + a = attachment_for(name) + (args.length > 0) ? a.to_s(args.first) : a + end + + define_method "#{name}=" do |file| + attachment_for(name).assign(file) + end + + define_method "#{name}?" do + attachment_for(name).file? + end + + validates_each(name) do |record, attr, value| + attachment = record.attachment_for(name) + attachment.send(:flush_errors) unless attachment.valid? + end + end + + # Places ActiveRecord-style validations on the size of the file assigned. The + # possible options are: + # * +in+: a Range of bytes (i.e. +1..1.megabyte+), + # * +less_than+: equivalent to :in => 0..options[:less_than] + # * +greater_than+: equivalent to :in => options[:greater_than]..Infinity + # * +message+: error message to display, use :min and :max as replacements + def validates_attachment_size name, options = {} + min = options[:greater_than] || (options[:in] && options[:in].first) || 0 + max = options[:less_than] || (options[:in] && options[:in].last) || (1.0/0) + range = (min..max) + message = options[:message] || "file size must be between :min and :max bytes." + + attachment_definitions[name][:validations][:size] = lambda do |attachment, instance| + if attachment.file? && !range.include?(attachment.size.to_i) + message.gsub(/:min/, min.to_s).gsub(/:max/, max.to_s) + end + end + end + + # Adds errors if thumbnail creation fails. The same as specifying :whiny_thumbnails => true. + def validates_attachment_thumbnails name, options = {} + attachment_definitions[name][:whiny_thumbnails] = true + end + + # Places ActiveRecord-style validations on the presence of a file. + def validates_attachment_presence name, options = {} + message = options[:message] || "must be set." + attachment_definitions[name][:validations][:presence] = lambda do |attachment, instance| + message unless attachment.file? + end + end + + # Places ActiveRecord-style validations on the content type of the file + # assigned. The possible options are: + # * +content_type+: Allowed content types. Can be a single content type + # or an array. Each type can be a String or a Regexp. It should be + # noted that Internet Explorer upload files with content_types that you + # may not expect. For example, JPEG images are given image/pjpeg and + # PNGs are image/x-png, so keep that in mind when determining how you + # match. Allows all by default. + # * +message+: The message to display when the uploaded file has an invalid + # content type. + # NOTE: If you do not specify an [attachment]_content_type field on your + # model, content_type validation will work _ONLY upon assignment_ and + # re-validation after the instance has been reloaded will always succeed. + def validates_attachment_content_type name, options = {} + attachment_definitions[name][:validations][:content_type] = lambda do |attachment, instance| + valid_types = [options[:content_type]].flatten + + unless attachment.original_filename.blank? + unless valid_types.blank? + content_type = attachment.instance_read(:content_type) + unless valid_types.any?{|t| content_type.nil? || t === content_type } + options[:message] || "is not one of the allowed file types." + end + end + end + end + end + + # Returns the attachment definitions defined by each call to + # has_attached_file. + def attachment_definitions + read_inheritable_attribute(:attachment_definitions) + end + end + + module InstanceMethods #:nodoc: + def attachment_for name + @_paperclip_attachments ||= {} + @_paperclip_attachments[name] ||= Attachment.new(name, self, self.class.attachment_definitions[name]) + end + + def each_attachment + self.class.attachment_definitions.each do |name, definition| + yield(name, attachment_for(name)) + end + end + + def save_attached_files + logger.info("[paperclip] Saving attachments.") + each_attachment do |name, attachment| + attachment.send(:save) + end + end + + def destroy_attached_files + logger.info("[paperclip] Deleting attachments.") + each_attachment do |name, attachment| + attachment.send(:queue_existing_for_delete) + attachment.send(:flush_deletes) + end + end + end + +end + +# Set it all up. +if Object.const_defined?("ActiveRecord") + ActiveRecord::Base.send(:include, Paperclip) + File.send(:include, Paperclip::Upfile) +end diff --git a/vendor/plugins/paperclip/lib/paperclip/attachment.rb b/vendor/plugins/paperclip/lib/paperclip/attachment.rb new file mode 100644 index 0000000..897c67e --- /dev/null +++ b/vendor/plugins/paperclip/lib/paperclip/attachment.rb @@ -0,0 +1,403 @@ +module Paperclip + # The Attachment class manages the files for a given attachment. It saves + # when the model saves, deletes when the model is destroyed, and processes + # the file upon assignment. + class Attachment + + def self.default_options + @default_options ||= { + :url => "/system/:attachment/:id/:style/:basename.:extension", + :path => ":rails_root/public/system/:attachment/:id/:style/:basename.:extension", + :styles => {}, + :default_url => "/:attachment/:style/missing.png", + :default_style => :original, + :validations => {}, + :storage => :filesystem + } + end + + attr_reader :name, :instance, :styles, :default_style, :convert_options, :queued_for_write + + # Creates an Attachment object. +name+ is the name of the attachment, + # +instance+ is the ActiveRecord object instance it's attached to, and + # +options+ is the same as the hash passed to +has_attached_file+. + def initialize name, instance, options = {} + @name = name + @instance = instance + + options = self.class.default_options.merge(options) + + @url = options[:url] + @url = @url.call(self) if @url.is_a?(Proc) + @path = options[:path] + @path = @path.call(self) if @path.is_a?(Proc) + @styles = options[:styles] + @styles = @styles.call(self) if @styles.is_a?(Proc) + @default_url = options[:default_url] + @validations = options[:validations] + @default_style = options[:default_style] + @storage = options[:storage] + @whiny = options[:whiny_thumbnails] + @convert_options = options[:convert_options] || {} + @processors = options[:processors] || [:thumbnail] + @options = options + @queued_for_delete = [] + @queued_for_write = {} + @errors = {} + @validation_errors = nil + @dirty = false + + normalize_style_definition + initialize_storage + end + + # What gets called when you call instance.attachment = File. It clears + # errors, assigns attributes, processes the file, and runs validations. It + # also queues up the previous file for deletion, to be flushed away on + # #save of its host. In addition to form uploads, you can also assign + # another Paperclip attachment: + # new_user.avatar = old_user.avatar + # If the file that is assigned is not valid, the processing (i.e. + # thumbnailing, etc) will NOT be run. + def assign uploaded_file + %w(file_name).each do |field| + unless @instance.class.column_names.include?("#{name}_#{field}") + raise PaperclipError.new("#{@instance.class} model does not have required column '#{name}_#{field}'") + end + end + + if uploaded_file.is_a?(Paperclip::Attachment) + uploaded_file = uploaded_file.to_file(:original) + close_uploaded_file = uploaded_file.respond_to?(:close) + end + + return nil unless valid_assignment?(uploaded_file) + + uploaded_file.binmode if uploaded_file.respond_to? :binmode + self.clear + + return nil if uploaded_file.nil? + + @queued_for_write[:original] = uploaded_file.to_tempfile + instance_write(:file_name, uploaded_file.original_filename.strip.gsub(/[^\w\d\.\-]+/, '_')) + instance_write(:content_type, uploaded_file.content_type.to_s.strip) + instance_write(:file_size, uploaded_file.size.to_i) + instance_write(:updated_at, Time.now) + + @dirty = true + + post_process if valid? + + # Reset the file size if the original file was reprocessed. + instance_write(:file_size, @queued_for_write[:original].size.to_i) + ensure + uploaded_file.close if close_uploaded_file + validate + end + + # Returns the public URL of the attachment, with a given style. Note that + # this does not necessarily need to point to a file that your web server + # can access and can point to an action in your app, if you need fine + # grained security. This is not recommended if you don't need the + # security, however, for performance reasons. set + # include_updated_timestamp to false if you want to stop the attachment + # update time appended to the url + def url style = default_style, include_updated_timestamp = true + url = original_filename.nil? ? interpolate(@default_url, style) : interpolate(@url, style) + include_updated_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url + end + + # Returns the path of the attachment as defined by the :path option. If the + # file is stored in the filesystem the path refers to the path of the file + # on disk. If the file is stored in S3, the path is the "key" part of the + # URL, and the :bucket option refers to the S3 bucket. + def path style = nil #:nodoc: + original_filename.nil? ? nil : interpolate(@path, style) + end + + # Alias to +url+ + def to_s style = nil + url(style) + end + + # Returns true if there are no errors on this attachment. + def valid? + validate + errors.empty? + end + + # Returns an array containing the errors on this attachment. + def errors + @errors + end + + # Returns true if there are changes that need to be saved. + def dirty? + @dirty + end + + # Saves the file, if there are no errors. If there are, it flushes them to + # the instance's errors and returns false, cancelling the save. + def save + if valid? + flush_deletes + flush_writes + @dirty = false + true + else + flush_errors + false + end + end + + # Clears out the attachment. Has the same effect as previously assigning + # nil to the attachment. Does NOT save. If you wish to clear AND save, + # use #destroy. + def clear + queue_existing_for_delete + @errors = {} + @validation_errors = nil + end + + # Destroys the attachment. Has the same effect as previously assigning + # nil to the attachment *and saving*. This is permanent. If you wish to + # wipe out the existing attachment but not save, use #clear. + def destroy + clear + save + end + + # Returns the name of the file as originally assigned, and lives in the + # _file_name attribute of the model. + def original_filename + instance_read(:file_name) + end + + # Returns the size of the file as originally assigned, and lives in the + # _file_size attribute of the model. + def size + instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size) + end + + # Returns the content_type of the file as originally assigned, and lives + # in the _content_type attribute of the model. + def content_type + instance_read(:content_type) + end + + # Returns the last modified time of the file as originally assigned, and + # lives in the _updated_at attribute of the model. + def updated_at + time = instance_read(:updated_at) + time && time.to_i + end + + # A hash of procs that are run during the interpolation of a path or url. + # A variable of the format :name will be replaced with the return value of + # the proc named ":name". Each lambda takes the attachment and the current + # style as arguments. This hash can be added to with your own proc if + # necessary. + def self.interpolations + @interpolations ||= { + :rails_root => lambda{|attachment,style| RAILS_ROOT }, + :rails_env => lambda{|attachment,style| RAILS_ENV }, + :class => lambda do |attachment,style| + attachment.instance.class.name.underscore.pluralize + end, + :basename => lambda do |attachment,style| + attachment.original_filename.gsub(/#{File.extname(attachment.original_filename)}$/, "") + end, + :extension => lambda do |attachment,style| + ((style = attachment.styles[style]) && style[:format]) || + File.extname(attachment.original_filename).gsub(/^\.+/, "") + end, + :id => lambda{|attachment,style| attachment.instance.id }, + :id_partition => lambda do |attachment, style| + ("%09d" % attachment.instance.id).scan(/\d{3}/).join("/") + end, + :attachment => lambda{|attachment,style| attachment.name.to_s.downcase.pluralize }, + :style => lambda{|attachment,style| style || attachment.default_style }, + } + end + + # This method really shouldn't be called that often. It's expected use is + # in the paperclip:refresh rake task and that's it. It will regenerate all + # thumbnails forcefully, by reobtaining the original file and going through + # the post-process again. + def reprocess! + new_original = Tempfile.new("paperclip-reprocess") + new_original.binmode + if old_original = to_file(:original) + new_original.write( old_original.read ) + new_original.rewind + + @queued_for_write = { :original => new_original } + post_process + + old_original.close if old_original.respond_to?(:close) + + save + else + true + end + end + + # Returns true if a file has been assigned. + def file? + !original_filename.blank? + end + + # Writes the attachment-specific attribute on the instance. For example, + # instance_write(:file_name, "me.jpg") will write "me.jpg" to the instance's + # "avatar_file_name" field (assuming the attachment is called avatar). + def instance_write(attr, value) + setter = :"#{name}_#{attr}=" + responds = instance.respond_to?(setter) + self.instance_variable_set("@_#{setter.to_s.chop}", value) + instance.send(setter, value) if responds || attr.to_s == "file_name" + end + + # Reads the attachment-specific attribute on the instance. See instance_write + # for more details. + def instance_read(attr) + getter = :"#{name}_#{attr}" + responds = instance.respond_to?(getter) + cached = self.instance_variable_get("@_#{getter}") + return cached if cached + instance.send(getter) if responds || attr.to_s == "file_name" + end + + private + + def logger #:nodoc: + instance.logger + end + + def log message #:nodoc: + logger.info("[paperclip] #{message}") if logging? + end + + def logging? #:nodoc: + Paperclip.options[:log] + end + + def valid_assignment? file #:nodoc: + file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type)) + end + + def validate #:nodoc: + unless @validation_errors + @validation_errors = @validations.inject({}) do |errors, validation| + name, block = validation + errors[name] = block.call(self, instance) if block + errors + end + @validation_errors.reject!{|k,v| v == nil } + @errors.merge!(@validation_errors) + end + @validation_errors + end + + def normalize_style_definition #:nodoc: + @styles.each do |name, args| + unless args.is_a? Hash + dimensions, format = *args + @styles[name] = { + :processors => @processors, + :geometry => dimensions, + :format => format, + :whiny => @whiny, + :convert_options => extra_options_for(name) + } + else + @styles[name] = { + :processors => @processors, + :whiny => @whiny, + :convert_options => extra_options_for(name) + }.merge(@styles[name]) + end + end + end + + def solidify_style_definitions #:nodoc: + @styles.each do |name, args| + @styles[name][:geometry] = @styles[name][:geometry].call(instance) if @styles[name][:geometry].respond_to?(:call) + @styles[name][:processors] = @styles[name][:processors].call(instance) if @styles[name][:processors].respond_to?(:call) + end + end + + def initialize_storage #:nodoc: + @storage_module = Paperclip::Storage.const_get(@storage.to_s.capitalize) + self.extend(@storage_module) + end + + def extra_options_for(style) #:nodoc: + all_options = convert_options[:all] + all_options = all_options.call(instance) if all_options.respond_to?(:call) + style_options = convert_options[style] + style_options = style_options.call(instance) if style_options.respond_to?(:call) + + [ style_options, all_options ].compact.join(" ") + end + + def post_process #:nodoc: + return if @queued_for_write[:original].nil? + solidify_style_definitions + return if fire_events(:before) + post_process_styles + return if fire_events(:after) + end + + def fire_events(which) + return true if callback(:"#{which}_post_process") == false + return true if callback(:"#{which}_#{name}_post_process") == false + end + + def callback which #:nodoc: + instance.run_callbacks(which, @queued_for_write){|result, obj| result == false } + end + + def post_process_styles + @styles.each do |name, args| + begin + raise RuntimeError.new("Style #{name} has no processors defined.") if args[:processors].blank? + @queued_for_write[name] = args[:processors].inject(@queued_for_write[:original]) do |file, processor| + Paperclip.processor(processor).make(file, args, self) + end + rescue PaperclipError => e + log("An error was received while processing: #{e.inspect}") + (@errors[:processing] ||= []) << e.message if @whiny + end + end + end + + def interpolate pattern, style = default_style #:nodoc: + interpolations = self.class.interpolations.sort{|a,b| a.first.to_s <=> b.first.to_s } + interpolations.reverse.inject( pattern.dup ) do |result, interpolation| + tag, blk = interpolation + result.gsub(/:#{tag}/) do |match| + blk.call( self, style ) + end + end + end + + def queue_existing_for_delete #:nodoc: + return unless file? + @queued_for_delete += [:original, *@styles.keys].uniq.map do |style| + path(style) if exists?(style) + end.compact + instance_write(:file_name, nil) + instance_write(:content_type, nil) + instance_write(:file_size, nil) + instance_write(:updated_at, nil) + end + + def flush_errors #:nodoc: + @errors.each do |error, message| + [message].flatten.each {|m| instance.errors.add(name, m) } + end + end + + end +end + diff --git a/vendor/plugins/paperclip/lib/paperclip/callback_compatability.rb b/vendor/plugins/paperclip/lib/paperclip/callback_compatability.rb new file mode 100644 index 0000000..10b08fc --- /dev/null +++ b/vendor/plugins/paperclip/lib/paperclip/callback_compatability.rb @@ -0,0 +1,33 @@ +module Paperclip + # This module is intended as a compatability shim for the differences in + # callbacks between Rails 2.0 and Rails 2.1. + module CallbackCompatability + def self.included(base) + base.extend(ClassMethods) + base.send(:include, InstanceMethods) + end + + module ClassMethods + # The implementation of this method is taken from the Rails 1.2.6 source, + # from rails/activerecord/lib/active_record/callbacks.rb, line 192. + def define_callbacks(*args) + args.each do |method| + self.class_eval <<-"end_eval" + def self.#{method}(*callbacks, &block) + callbacks << block if block_given? + write_inheritable_array(#{method.to_sym.inspect}, callbacks) + end + end_eval + end + end + end + + module InstanceMethods + # The callbacks in < 2.1 don't worry about the extra options or the + # block, so just run what we have available. + def run_callbacks(meth, opts = nil, &blk) + callback(meth) + end + end + end +end diff --git a/vendor/plugins/paperclip/lib/paperclip/geometry.rb b/vendor/plugins/paperclip/lib/paperclip/geometry.rb new file mode 100644 index 0000000..7fbe038 --- /dev/null +++ b/vendor/plugins/paperclip/lib/paperclip/geometry.rb @@ -0,0 +1,115 @@ +module Paperclip + + # Defines the geometry of an image. + class Geometry + attr_accessor :height, :width, :modifier + + # Gives a Geometry representing the given height and width + def initialize width = nil, height = nil, modifier = nil + @height = height.to_f + @width = width.to_f + @modifier = modifier + end + + # Uses ImageMagick to determing the dimensions of a file, passed in as either a + # File or path. + def self.from_file file + file = file.path if file.respond_to? "path" + geometry = begin + Paperclip.run("identify", %Q[-format "%wx%h" "#{file}"[0]]) + rescue PaperclipCommandLineError + "" + end + parse(geometry) || + raise(NotIdentifiedByImageMagickError.new("#{file} is not recognized by the 'identify' command.")) + end + + # Parses a "WxH" formatted string, where W is the width and H is the height. + def self.parse string + if match = (string && string.match(/\b(\d*)x?(\d*)\b([\>\<\#\@\%^!])?/)) + Geometry.new(*match[1,3]) + end + end + + # True if the dimensions represent a square + def square? + height == width + end + + # True if the dimensions represent a horizontal rectangle + def horizontal? + height < width + end + + # True if the dimensions represent a vertical rectangle + def vertical? + height > width + end + + # The aspect ratio of the dimensions. + def aspect + width / height + end + + # Returns the larger of the two dimensions + def larger + [height, width].max + end + + # Returns the smaller of the two dimensions + def smaller + [height, width].min + end + + # Returns the width and height in a format suitable to be passed to Geometry.parse + def to_s + s = "" + s << width.to_i.to_s if width > 0 + s << "x#{height.to_i}" if height > 0 + s << modifier.to_s + s + end + + # Same as to_s + def inspect + to_s + end + + # Returns the scaling and cropping geometries (in string-based ImageMagick format) + # neccessary to transform this Geometry into the Geometry given. If crop is true, + # then it is assumed the destination Geometry will be the exact final resolution. + # In this case, the source Geometry is scaled so that an image containing the + # destination Geometry would be completely filled by the source image, and any + # overhanging image would be cropped. Useful for square thumbnail images. The cropping + # is weighted at the center of the Geometry. + def transformation_to dst, crop = false + if crop + ratio = Geometry.new( dst.width / self.width, dst.height / self.height ) + scale_geometry, scale = scaling(dst, ratio) + crop_geometry = cropping(dst, ratio, scale) + else + scale_geometry = dst.to_s + end + + [ scale_geometry, crop_geometry ] + end + + private + + def scaling dst, ratio + if ratio.horizontal? || ratio.square? + [ "%dx" % dst.width, ratio.width ] + else + [ "x%d" % dst.height, ratio.height ] + end + end + + def cropping dst, ratio, scale + if ratio.horizontal? || ratio.square? + "%dx%d+%d+%d" % [ dst.width, dst.height, 0, (self.height * scale - dst.height) / 2 ] + else + "%dx%d+%d+%d" % [ dst.width, dst.height, (self.width * scale - dst.width) / 2, 0 ] + end + end + end +end diff --git a/vendor/plugins/paperclip/lib/paperclip/iostream.rb b/vendor/plugins/paperclip/lib/paperclip/iostream.rb new file mode 100644 index 0000000..74e2014 --- /dev/null +++ b/vendor/plugins/paperclip/lib/paperclip/iostream.rb @@ -0,0 +1,58 @@ +# Provides method that can be included on File-type objects (IO, StringIO, Tempfile, etc) to allow stream copying +# and Tempfile conversion. +module IOStream + + # Returns a Tempfile containing the contents of the readable object. + def to_tempfile + tempfile = Tempfile.new("stream") + tempfile.binmode + self.stream_to(tempfile) + end + + # Copies one read-able object from one place to another in blocks, obviating the need to load + # the whole thing into memory. Defaults to 8k blocks. If this module is included in both + # StringIO and Tempfile, then either can have its data copied anywhere else without typing + # worries or memory overhead worries. Returns a File if a String is passed in as the destination + # and returns the IO or Tempfile as passed in if one is sent as the destination. + def stream_to path_or_file, in_blocks_of = 8192 + dstio = case path_or_file + when String then File.new(path_or_file, "wb+") + when IO then path_or_file + when Tempfile then path_or_file + end + buffer = "" + self.rewind + while self.read(in_blocks_of, buffer) do + dstio.write(buffer) + end + dstio.rewind + dstio + end +end + +class IO #:nodoc: + include IOStream +end + +%w( Tempfile StringIO ).each do |klass| + if Object.const_defined? klass + Object.const_get(klass).class_eval do + include IOStream + end + end +end + +# Corrects a bug in Windows when asking for Tempfile size. +if defined? Tempfile + class Tempfile + def size + if @tmpfile + @tmpfile.fsync + @tmpfile.flush + @tmpfile.stat.size + else + 0 + end + end + end +end diff --git a/vendor/plugins/paperclip/lib/paperclip/matchers.rb b/vendor/plugins/paperclip/lib/paperclip/matchers.rb new file mode 100644 index 0000000..ca24b5e --- /dev/null +++ b/vendor/plugins/paperclip/lib/paperclip/matchers.rb @@ -0,0 +1,4 @@ +require 'paperclip/matchers/have_attached_file_matcher' +require 'paperclip/matchers/validate_attachment_presence_matcher' +require 'paperclip/matchers/validate_attachment_content_type_matcher' +require 'paperclip/matchers/validate_attachment_size_matcher' diff --git a/vendor/plugins/paperclip/lib/paperclip/matchers/have_attached_file_matcher.rb b/vendor/plugins/paperclip/lib/paperclip/matchers/have_attached_file_matcher.rb new file mode 100644 index 0000000..da0dd8b --- /dev/null +++ b/vendor/plugins/paperclip/lib/paperclip/matchers/have_attached_file_matcher.rb @@ -0,0 +1,49 @@ +module Paperclip + module Shoulda + module Matchers + def have_attached_file name + HaveAttachedFileMatcher.new(name) + end + + class HaveAttachedFileMatcher + def initialize attachment_name + @attachment_name = attachment_name + end + + def matches? subject + @subject = subject + responds? && has_column? && included? + end + + def failure_message + "Should have an attachment named #{@attachment_name}" + end + + def negative_failure_message + "Should not have an attachment named #{@attachment_name}" + end + + def description + "have an attachment named #{@attachment_name}" + end + + protected + + def responds? + methods = @subject.instance_methods + methods.include?("#{@attachment_name}") && + methods.include?("#{@attachment_name}=") && + methods.include?("#{@attachment_name}?") + end + + def has_column? + @subject.column_names.include?("#{@attachment_name}_file_name") + end + + def included? + @subject.ancestors.include?(Paperclip::InstanceMethods) + end + end + end + end +end diff --git a/vendor/plugins/paperclip/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb b/vendor/plugins/paperclip/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb new file mode 100644 index 0000000..b4e97fd --- /dev/null +++ b/vendor/plugins/paperclip/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb @@ -0,0 +1,66 @@ +module Paperclip + module Shoulda + module Matchers + def validate_attachment_content_type name + ValidateAttachmentContentTypeMatcher.new(name) + end + + class ValidateAttachmentContentTypeMatcher + def initialize attachment_name + @attachment_name = attachment_name + end + + def allowing *types + @allowed_types = types.flatten + self + end + + def rejecting *types + @rejected_types = types.flatten + self + end + + def matches? subject + @subject = subject + @allowed_types && @rejected_types && + allowed_types_allowed? && rejected_types_rejected? + end + + def failure_message + "Content types #{@allowed_types.join(", ")} should be accepted" + + " and #{@rejected_types.join(", ")} rejected by #{@attachment_name}" + end + + def negative_failure_message + "Content types #{@allowed_types.join(", ")} should be rejected" + + " and #{@rejected_types.join(", ")} accepted by #{@attachment_name}" + end + + def description + "validate the content types allowed on attachment #{@attachment_name}" + end + + protected + + def allow_types?(types) + types.all? do |type| + file = StringIO.new(".") + file.content_type = type + attachment = @subject.new.attachment_for(@attachment_name) + attachment.assign(file) + attachment.errors[:content_type].nil? + end + end + + def allowed_types_allowed? + allow_types?(@allowed_types) + end + + def rejected_types_rejected? + not allow_types?(@rejected_types) + end + end + end + end +end + diff --git a/vendor/plugins/paperclip/lib/paperclip/matchers/validate_attachment_presence_matcher.rb b/vendor/plugins/paperclip/lib/paperclip/matchers/validate_attachment_presence_matcher.rb new file mode 100644 index 0000000..61dc0ea --- /dev/null +++ b/vendor/plugins/paperclip/lib/paperclip/matchers/validate_attachment_presence_matcher.rb @@ -0,0 +1,48 @@ +module Paperclip + module Shoulda + module Matchers + def validate_attachment_presence name + ValidateAttachmentPresenceMatcher.new(name) + end + + class ValidateAttachmentPresenceMatcher + def initialize attachment_name + @attachment_name = attachment_name + end + + def matches? subject + @subject = subject + error_when_not_valid? && no_error_when_valid? + end + + def failure_message + "Attachment #{@attachment_name} should be required" + end + + def negative_failure_message + "Attachment #{@attachment_name} should not be required" + end + + def description + "require presence of attachment #{@attachment_name}" + end + + protected + + def error_when_not_valid? + @attachment = @subject.new.send(@attachment_name) + @attachment.assign(nil) + not @attachment.errors[:presence].nil? + end + + def no_error_when_valid? + @file = StringIO.new(".") + @attachment = @subject.new.send(@attachment_name) + @attachment.assign(@file) + @attachment.errors[:presence].nil? + end + end + end + end +end + diff --git a/vendor/plugins/paperclip/lib/paperclip/matchers/validate_attachment_size_matcher.rb b/vendor/plugins/paperclip/lib/paperclip/matchers/validate_attachment_size_matcher.rb new file mode 100644 index 0000000..f84c479 --- /dev/null +++ b/vendor/plugins/paperclip/lib/paperclip/matchers/validate_attachment_size_matcher.rb @@ -0,0 +1,83 @@ +module Paperclip + module Shoulda + module Matchers + def validate_attachment_size name + ValidateAttachmentSizeMatcher.new(name) + end + + class ValidateAttachmentSizeMatcher + def initialize attachment_name + @attachment_name = attachment_name + @low, @high = 0, (1.0/0) + end + + def less_than size + @high = size + self + end + + def greater_than size + @low = size + self + end + + def in range + @low, @high = range.first, range.last + self + end + + def matches? subject + @subject = subject + lower_than_low? && higher_than_low? && lower_than_high? && higher_than_high? + end + + def failure_message + "Attachment #{@attachment_name} must be between #{@low} and #{@high} bytes" + end + + def negative_failure_message + "Attachment #{@attachment_name} cannot be between #{@low} and #{@high} bytes" + end + + def description + "validate the size of attachment #{@attachment_name}" + end + + protected + + def override_method object, method, &replacement + (class << object; self; end).class_eval do + define_method(method, &replacement) + end + end + + def passes_validation_with_size(new_size) + file = StringIO.new(".") + override_method(file, :size){ new_size } + attachment = @subject.new.attachment_for(@attachment_name) + attachment.assign(file) + attachment.errors[:size].nil? + end + + def lower_than_low? + not passes_validation_with_size(@low - 1) + end + + def higher_than_low? + passes_validation_with_size(@low + 1) + end + + def lower_than_high? + return true if @high == (1.0/0) + passes_validation_with_size(@high - 1) + end + + def higher_than_high? + return true if @high == (1.0/0) + not passes_validation_with_size(@high + 1) + end + end + end + end +end + diff --git a/vendor/plugins/paperclip/lib/paperclip/processor.rb b/vendor/plugins/paperclip/lib/paperclip/processor.rb new file mode 100644 index 0000000..9082bfd --- /dev/null +++ b/vendor/plugins/paperclip/lib/paperclip/processor.rb @@ -0,0 +1,48 @@ +module Paperclip + # Paperclip processors allow you to modify attached files when they are + # attached in any way you are able. Paperclip itself uses command-line + # programs for its included Thumbnail processor, but custom processors + # are not required to follow suit. + # + # Processors are required to be defined inside the Paperclip module and + # are also required to be a subclass of Paperclip::Processor. There are + # only two methods you must implement to properly be a subclass: + # #initialize and #make. Initialize's arguments are the file that will + # be operated on (which is an instance of File), and a hash of options + # that were defined in has_attached_file's style hash. + # + # All #make needs to do is return an instance of File (Tempfile is + # acceptable) which contains the results of the processing. + # + # See Paperclip.run for more information about using command-line + # utilities from within Processors. + class Processor + attr_accessor :file, :options, :attachment + + def initialize file, options = {}, attachment = nil + @file = file + @options = options + @attachment = attachment + end + + def make + end + + def self.make file, options = {}, attachment = nil + new(file, options, attachment).make + end + end + + # Due to how ImageMagick handles its image format conversion and how Tempfile + # handles its naming scheme, it is necessary to override how Tempfile makes + # its names so as to allow for file extensions. Idea taken from the comments + # on this blog post: + # http://marsorange.com/archives/of-mogrify-ruby-tempfile-dynamic-class-definitions + class Tempfile < ::Tempfile + # Replaces Tempfile's +make_tmpname+ with one that honors file extensions. + def make_tmpname(basename, n) + extension = File.extname(basename) + sprintf("%s,%d,%d%s", File.basename(basename, extension), $$, n, extension) + end + end +end diff --git a/vendor/plugins/paperclip/lib/paperclip/storage.rb b/vendor/plugins/paperclip/lib/paperclip/storage.rb new file mode 100644 index 0000000..58913ad --- /dev/null +++ b/vendor/plugins/paperclip/lib/paperclip/storage.rb @@ -0,0 +1,236 @@ +module Paperclip + module Storage + + # The default place to store attachments is in the filesystem. Files on the local + # filesystem can be very easily served by Apache without requiring a hit to your app. + # They also can be processed more easily after they've been saved, as they're just + # normal files. There is one Filesystem-specific option for has_attached_file. + # * +path+: The location of the repository of attachments on disk. This can (and, in + # almost all cases, should) be coordinated with the value of the +url+ option to + # allow files to be saved into a place where Apache can serve them without + # hitting your app. Defaults to + # ":rails_root/public/:attachment/:id/:style/:basename.:extension" + # By default this places the files in the app's public directory which can be served + # directly. If you are using capistrano for deployment, a good idea would be to + # make a symlink to the capistrano-created system directory from inside your app's + # public directory. + # See Paperclip::Attachment#interpolate for more information on variable interpolaton. + # :path => "/var/app/attachments/:class/:id/:style/:basename.:extension" + module Filesystem + def self.extended base + end + + def exists?(style = default_style) + if original_filename + File.exist?(path(style)) + else + false + end + end + + # Returns representation of the data of the file assigned to the given + # style, in the format most representative of the current storage. + def to_file style = default_style + @queued_for_write[style] || (File.new(path(style), 'rb') if exists?(style)) + end + alias_method :to_io, :to_file + + def flush_writes #:nodoc: + @queued_for_write.each do |style, file| + file.close + FileUtils.mkdir_p(File.dirname(path(style))) + logger.info("[paperclip] saving #{path(style)}") + FileUtils.mv(file.path, path(style)) + FileUtils.chmod(0644, path(style)) + end + @queued_for_write = {} + end + + def flush_deletes #:nodoc: + @queued_for_delete.each do |path| + begin + logger.info("[paperclip] deleting #{path}") + FileUtils.rm(path) if File.exist?(path) + rescue Errno::ENOENT => e + # ignore file-not-found, let everything else pass + end + begin + while(true) + path = File.dirname(path) + FileUtils.rmdir(path) + end + rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR + # Stop trying to remove parent directories + rescue SystemCallError => e + logger.info("[paperclip] There was an unexpected error while deleting directories: #{e.class}") + # Ignore it + end + end + @queued_for_delete = [] + end + end + + # Amazon's S3 file hosting service is a scalable, easy place to store files for + # distribution. You can find out more about it at http://aws.amazon.com/s3 + # There are a few S3-specific options for has_attached_file: + # * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point + # to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon + # gives you. You can 'environment-space' this just like you do to your + # database.yml file, so different environments can use different accounts: + # development: + # access_key_id: 123... + # secret_access_key: 123... + # test: + # access_key_id: abc... + # secret_access_key: abc... + # production: + # access_key_id: 456... + # secret_access_key: 456... + # This is not required, however, and the file may simply look like this: + # access_key_id: 456... + # secret_access_key: 456... + # In which case, those access keys will be used in all environments. You can also + # put your bucket name in this file, instead of adding it to the code directly. + # This is useful when you want the same account but a different bucket for + # development versus production. + # * +s3_permissions+: This is a String that should be one of the "canned" access + # policies that S3 provides (more information can be found here: + # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies) + # The default for Paperclip is "public-read". + # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either + # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are 'public-read' (the + # default), and 'https' when your :s3_permissions are anything else. + # * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate} + # * +bucket+: This is the name of the S3 bucket that will store your files. Remember + # that the bucket must be unique across all of Amazon S3. If the bucket does not exist + # Paperclip will attempt to create it. The bucket name will not be interpolated. + # You can define the bucket as a Proc if you want to determine it's name at runtime. + # Paperclip will call that Proc with attachment as the only argument. + # * +s3_host_alias+: The fully-qualified domain name (FQDN) that is the alias to the + # S3 domain of your bucket. Used with the :s3_alias_url url interpolation. See the + # link in the +url+ entry for more information about S3 domains and buckets. + # * +url+: There are three options for the S3 url. You can choose to have the bucket's name + # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket). + # Lastly, you can specify a CNAME (which requires the CNAME to be specified as + # :s3_alias_url. You can read more about CNAMEs and S3 at + # http://docs.amazonwebservices.com/AmazonS3/latest/index.html?VirtualHosting.html + # Normally, this won't matter in the slightest and you can leave the default (which is + # path-style, or :s3_path_url). But in some cases paths don't work and you need to use + # the domain-style (:s3_domain_url). Anything else here will be treated like path-style. + # NOTE: If you use a CNAME for use with CloudFront, you can NOT specify https as your + # :s3_protocol; This is *not supported* by S3/CloudFront. Finally, when using the host + # alias, the :bucket parameter is ignored, as the hostname is used as the bucket name + # by S3. + # * +path+: This is the key under the bucket in which the file will be stored. The + # URL will be constructed from the bucket and the path. This is what you will want + # to interpolate. Keys should be unique, like filenames, and despite the fact that + # S3 (strictly speaking) does not support directories, you can still use a / to + # separate parts of your file name. + module S3 + def self.extended base + require 'right_aws' + base.instance_eval do + @s3_credentials = parse_credentials(@options[:s3_credentials]) + @bucket = @options[:bucket] || @s3_credentials[:bucket] + @bucket = @bucket.call(self) if @bucket.is_a?(Proc) + @s3_options = @options[:s3_options] || {} + @s3_permissions = @options[:s3_permissions] || 'public-read' + @s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https') + @s3_headers = @options[:s3_headers] || {} + @s3_host_alias = @options[:s3_host_alias] + @url = ":s3_path_url" unless @url.to_s.match(/^:s3.*url$/) + end + base.class.interpolations[:s3_alias_url] = lambda do |attachment, style| + "#{attachment.s3_protocol}://#{attachment.s3_host_alias}/#{attachment.path(style).gsub(%r{^/}, "")}" + end + base.class.interpolations[:s3_path_url] = lambda do |attachment, style| + "#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}" + end + base.class.interpolations[:s3_domain_url] = lambda do |attachment, style| + "#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}" + end + end + + def s3 + @s3 ||= RightAws::S3.new(@s3_credentials[:access_key_id], + @s3_credentials[:secret_access_key], + @s3_options) + end + + def s3_bucket + @s3_bucket ||= s3.bucket(@bucket, true, @s3_permissions) + end + + def bucket_name + @bucket + end + + def s3_host_alias + @s3_host_alias + end + + def parse_credentials creds + creds = find_credentials(creds).stringify_keys + (creds[ENV['RAILS_ENV']] || creds).symbolize_keys + end + + def exists?(style = default_style) + s3_bucket.key(path(style)) ? true : false + end + + def s3_protocol + @s3_protocol + end + + # Returns representation of the data of the file assigned to the given + # style, in the format most representative of the current storage. + def to_file style = default_style + @queued_for_write[style] || s3_bucket.key(path(style)) + end + alias_method :to_io, :to_file + + def flush_writes #:nodoc: + @queued_for_write.each do |style, file| + begin + logger.info("[paperclip] saving #{path(style)}") + key = s3_bucket.key(path(style)) + key.data = file + key.put(nil, @s3_permissions, {'Content-type' => instance_read(:content_type)}.merge(@s3_headers)) + rescue RightAws::AwsError => e + raise + end + end + @queued_for_write = {} + end + + def flush_deletes #:nodoc: + @queued_for_delete.each do |path| + begin + logger.info("[paperclip] deleting #{path}") + if file = s3_bucket.key(path) + file.delete + end + rescue RightAws::AwsError + # Ignore this. + end + end + @queued_for_delete = [] + end + + def find_credentials creds + case creds + when File + YAML.load_file(creds.path) + when String + YAML.load_file(creds) + when Hash + creds + else + raise ArgumentError, "Credentials are not a path, file, or hash." + end + end + private :find_credentials + + end + end +end diff --git a/vendor/plugins/paperclip/lib/paperclip/thumbnail.rb b/vendor/plugins/paperclip/lib/paperclip/thumbnail.rb new file mode 100644 index 0000000..2178a9c --- /dev/null +++ b/vendor/plugins/paperclip/lib/paperclip/thumbnail.rb @@ -0,0 +1,70 @@ +module Paperclip + # Handles thumbnailing images that are uploaded. + class Thumbnail < Processor + + attr_accessor :current_geometry, :target_geometry, :format, :whiny, :convert_options + + # Creates a Thumbnail object set to work on the +file+ given. It + # will attempt to transform the image into one defined by +target_geometry+ + # which is a "WxH"-style string. +format+ will be inferred from the +file+ + # unless specified. Thumbnail creation will raise no errors unless + # +whiny+ is true (which it is, by default. If +convert_options+ is + # set, the options will be appended to the convert command upon image conversion + def initialize file, options = {}, attachment = nil + super + geometry = options[:geometry] + @file = file + @crop = geometry[-1,1] == '#' + @target_geometry = Geometry.parse geometry + @current_geometry = Geometry.from_file @file + @convert_options = options[:convert_options] + @whiny = options[:whiny].nil? ? true : options[:whiny] + @format = options[:format] + + @current_format = File.extname(@file.path) + @basename = File.basename(@file.path, @current_format) + end + + # Returns true if the +target_geometry+ is meant to crop. + def crop? + @crop + end + + # Returns true if the image is meant to make use of additional convert options. + def convert_options? + not @convert_options.blank? + end + + # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile + # that contains the new image. + def make + src = @file + dst = Tempfile.new([@basename, @format].compact.join(".")) + dst.binmode + + command = <<-end_command + "#{ File.expand_path(src.path) }[0]" + #{ transformation_command } + "#{ File.expand_path(dst.path) }" + end_command + + begin + success = Paperclip.run("convert", command.gsub(/\s+/, " ")) + rescue PaperclipCommandLineError + raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny + end + + dst + end + + # Returns the command ImageMagick's +convert+ needs to transform the image + # into the thumbnail. + def transformation_command + scale, crop = @current_geometry.transformation_to(@target_geometry, crop?) + trans = "-resize \"#{scale}\"" + trans << " -crop \"#{crop}\" +repage" if crop + trans << " #{convert_options}" if convert_options? + trans + end + end +end diff --git a/vendor/plugins/paperclip/lib/paperclip/upfile.rb b/vendor/plugins/paperclip/lib/paperclip/upfile.rb new file mode 100644 index 0000000..f0c8a44 --- /dev/null +++ b/vendor/plugins/paperclip/lib/paperclip/upfile.rb @@ -0,0 +1,48 @@ +module Paperclip + # The Upfile module is a convenience module for adding uploaded-file-type methods + # to the +File+ class. Useful for testing. + # user.avatar = File.new("test/test_avatar.jpg") + module Upfile + + # Infer the MIME-type of the file from the extension. + def content_type + type = (self.path.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase + case type + when %r"jpe?g" then "image/jpeg" + when %r"tiff?" then "image/tiff" + when %r"png", "gif", "bmp" then "image/#{type}" + when "txt" then "text/plain" + when %r"html?" then "text/html" + when "csv", "xml", "css", "js" then "text/#{type}" + else "application/x-#{type}" + end + end + + # Returns the file's normal name. + def original_filename + File.basename(self.path) + end + + # Returns the size of the file. + def size + File.size(self) + end + end +end + +if defined? StringIO + class StringIO + attr_accessor :original_filename, :content_type + def original_filename + @original_filename ||= "stringio.txt" + end + def content_type + @content_type ||= "text/plain" + end + end +end + +class File #:nodoc: + include Paperclip::Upfile +end + -- cgit v1.3