summaryrefslogtreecommitdiff
path: root/vendor/plugins/paperclip/lib
diff options
context:
space:
mode:
authorhukl <contact@smyck.org>2009-04-24 11:43:08 +0200
committerhukl <contact@smyck.org>2009-04-25 14:55:27 +0200
commitcf1b60e0cfa7d1a8f4a80d686649cc12e73a634e (patch)
treeb68bd845d290ce968892c4532bcff52083925834 /vendor/plugins/paperclip/lib
parentb2b78c06074046bd73cc3408a29386a976f0469c (diff)
Integrated basic Asset upload functionality. You can upload files now and use their url in pages.
Diffstat (limited to 'vendor/plugins/paperclip/lib')
-rw-r--r--vendor/plugins/paperclip/lib/paperclip.rb318
-rw-r--r--vendor/plugins/paperclip/lib/paperclip/attachment.rb403
-rw-r--r--vendor/plugins/paperclip/lib/paperclip/callback_compatability.rb33
-rw-r--r--vendor/plugins/paperclip/lib/paperclip/geometry.rb115
-rw-r--r--vendor/plugins/paperclip/lib/paperclip/iostream.rb58
-rw-r--r--vendor/plugins/paperclip/lib/paperclip/matchers.rb4
-rw-r--r--vendor/plugins/paperclip/lib/paperclip/matchers/have_attached_file_matcher.rb49
-rw-r--r--vendor/plugins/paperclip/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb66
-rw-r--r--vendor/plugins/paperclip/lib/paperclip/matchers/validate_attachment_presence_matcher.rb48
-rw-r--r--vendor/plugins/paperclip/lib/paperclip/matchers/validate_attachment_size_matcher.rb83
-rw-r--r--vendor/plugins/paperclip/lib/paperclip/processor.rb48
-rw-r--r--vendor/plugins/paperclip/lib/paperclip/storage.rb236
-rw-r--r--vendor/plugins/paperclip/lib/paperclip/thumbnail.rb70
-rw-r--r--vendor/plugins/paperclip/lib/paperclip/upfile.rb48
14 files changed, 1579 insertions, 0 deletions
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 @@
1# Paperclip allows file attachments that are stored in the filesystem. All graphical
2# transformations are done using the Graphics/ImageMagick command line utilities and
3# are stored in Tempfiles until the record is saved. Paperclip does not require a
4# separate model for storing the attachment's information, instead adding a few simple
5# columns to your table.
6#
7# Author:: Jon Yurek
8# Copyright:: Copyright (c) 2008 thoughtbot, inc.
9# License:: MIT License (http://www.opensource.org/licenses/mit-license.php)
10#
11# Paperclip defines an attachment as any file, though it makes special considerations
12# for image files. You can declare that a model has an attached file with the
13# +has_attached_file+ method:
14#
15# class User < ActiveRecord::Base
16# has_attached_file :avatar, :styles => { :thumb => "100x100" }
17# end
18#
19# user = User.new
20# user.avatar = params[:user][:avatar]
21# user.avatar.url
22# # => "/users/avatars/4/original_me.jpg"
23# user.avatar.url(:thumb)
24# # => "/users/avatars/4/thumb_me.jpg"
25#
26# See the +has_attached_file+ documentation for more details.
27
28require 'tempfile'
29require 'paperclip/upfile'
30require 'paperclip/iostream'
31require 'paperclip/geometry'
32require 'paperclip/processor'
33require 'paperclip/thumbnail'
34require 'paperclip/storage'
35require 'paperclip/attachment'
36if defined? RAILS_ROOT
37 Dir.glob(File.join(File.expand_path(RAILS_ROOT), "lib", "paperclip_processors", "*.rb")).each do |processor|
38 require processor
39 end
40end
41
42# The base module that gets included in ActiveRecord::Base. See the
43# documentation for Paperclip::ClassMethods for more useful information.
44module Paperclip
45
46 VERSION = "2.2.8"
47
48 class << self
49 # Provides configurability to Paperclip. There are a number of options available, such as:
50 # * whiny_thumbnails: Will raise an error if Paperclip cannot process thumbnails of
51 # an uploaded image. Defaults to true.
52 # * log: Logs progress to the Rails log. Uses ActiveRecord's logger, so honors
53 # log levels, etc. Defaults to true.
54 # * command_path: Defines the path at which to find the command line
55 # programs if they are not visible to Rails the system's search path. Defaults to
56 # nil, which uses the first executable found in the user's search path.
57 # * image_magick_path: Deprecated alias of command_path.
58 def options
59 @options ||= {
60 :whiny_thumbnails => true,
61 :image_magick_path => nil,
62 :command_path => nil,
63 :log => true,
64 :swallow_stderr => true
65 }
66 end
67
68 def path_for_command command #:nodoc:
69 if options[:image_magick_path]
70 warn("[DEPRECATION] :image_magick_path is deprecated and will be removed. Use :command_path instead")
71 end
72 path = [options[:command_path] || options[:image_magick_path], command].compact
73 File.join(*path)
74 end
75
76 def interpolates key, &block
77 Paperclip::Attachment.interpolations[key] = block
78 end
79
80 # The run method takes a command to execute and a string of parameters
81 # that get passed to it. The command is prefixed with the :command_path
82 # option from Paperclip.options. If you have many commands to run and
83 # they are in different paths, the suggested course of action is to
84 # symlink them so they are all in the same directory.
85 #
86 # If the command returns with a result code that is not one of the
87 # expected_outcodes, a PaperclipCommandLineError will be raised. Generally
88 # a code of 0 is expected, but a list of codes may be passed if necessary.
89 def run cmd, params = "", expected_outcodes = 0
90 command = %Q<#{%Q[#{path_for_command(cmd)} #{params}].gsub(/\s+/, " ")}>
91 command = "#{command} 2>#{bit_bucket}" if Paperclip.options[:swallow_stderr]
92 output = `#{command}`
93 unless [expected_outcodes].flatten.include?($?.exitstatus)
94 raise PaperclipCommandLineError, "Error while running #{cmd}"
95 end
96 output
97 end
98
99 def bit_bucket #:nodoc:
100 File.exists?("/dev/null") ? "/dev/null" : "NUL"
101 end
102
103 def included base #:nodoc:
104 base.extend ClassMethods
105 unless base.respond_to?(:define_callbacks)
106 base.send(:include, Paperclip::CallbackCompatability)
107 end
108 end
109
110 def processor name #:nodoc:
111 name = name.to_s.camelize
112 processor = Paperclip.const_get(name)
113 unless processor.ancestors.include?(Paperclip::Processor)
114 raise PaperclipError.new("Processor #{name} was not found")
115 end
116 processor
117 end
118 end
119
120 class PaperclipError < StandardError #:nodoc:
121 end
122
123 class PaperclipCommandLineError < StandardError #:nodoc:
124 end
125
126 class NotIdentifiedByImageMagickError < PaperclipError #:nodoc:
127 end
128
129 module ClassMethods
130 # +has_attached_file+ gives the class it is called on an attribute that maps to a file. This
131 # is typically a file stored somewhere on the filesystem and has been uploaded by a user.
132 # The attribute returns a Paperclip::Attachment object which handles the management of
133 # that file. The intent is to make the attachment as much like a normal attribute. The
134 # thumbnails will be created when the new file is assigned, but they will *not* be saved
135 # until +save+ is called on the record. Likewise, if the attribute is set to +nil+ is
136 # called on it, the attachment will *not* be deleted until +save+ is called. See the
137 # Paperclip::Attachment documentation for more specifics. There are a number of options
138 # you can set to change the behavior of a Paperclip attachment:
139 # * +url+: The full URL of where the attachment is publically accessible. This can just
140 # as easily point to a directory served directly through Apache as it can to an action
141 # that can control permissions. You can specify the full domain and path, but usually
142 # just an absolute path is sufficient. The leading slash *must* be included manually for
143 # absolute paths. The default value is
144 # "/system/:attachment/:id/:style/:basename.:extension". See
145 # Paperclip::Attachment#interpolate for more information on variable interpolaton.
146 # :url => "/:class/:attachment/:id/:style_:basename.:extension"
147 # :url => "http://some.other.host/stuff/:class/:id_:extension"
148 # * +default_url+: The URL that will be returned if there is no attachment assigned.
149 # This field is interpolated just as the url is. The default value is
150 # "/:attachment/:style/missing.png"
151 # has_attached_file :avatar, :default_url => "/images/default_:style_avatar.png"
152 # User.new.avatar_url(:small) # => "/images/default_small_avatar.png"
153 # * +styles+: A hash of thumbnail styles and their geometries. You can find more about
154 # geometry strings at the ImageMagick website
155 # (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
156 # also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally
157 # inside the dimensions and then crop the rest off (weighted at the center). The
158 # default value is to generate no thumbnails.
159 # * +default_style+: The thumbnail style that will be used by default URLs.
160 # Defaults to +original+.
161 # has_attached_file :avatar, :styles => { :normal => "100x100#" },
162 # :default_style => :normal
163 # user.avatar.url # => "/avatars/23/normal_me.png"
164 # * +whiny_thumbnails+: Will raise an error if Paperclip cannot post_process an uploaded file due
165 # to a command line error. This will override the global setting for this attachment.
166 # Defaults to true.
167 # * +convert_options+: When creating thumbnails, use this free-form options
168 # field to pass in various convert command options. Typical options are "-strip" to
169 # remove all Exif data from the image (save space for thumbnails and avatars) or
170 # "-depth 8" to specify the bit depth of the resulting conversion. See ImageMagick
171 # convert documentation for more options: (http://www.imagemagick.org/script/convert.php)
172 # Note that this option takes a hash of options, each of which correspond to the style
173 # of thumbnail being generated. You can also specify :all as a key, which will apply
174 # to all of the thumbnails being generated. If you specify options for the :original,
175 # it would be best if you did not specify destructive options, as the intent of keeping
176 # the original around is to regenerate all the thumbnails when requirements change.
177 # has_attached_file :avatar, :styles => { :large => "300x300", :negative => "100x100" }
178 # :convert_options => {
179 # :all => "-strip",
180 # :negative => "-negate"
181 # }
182 # * +storage+: Chooses the storage backend where the files will be stored. The current
183 # choices are :filesystem and :s3. The default is :filesystem. Make sure you read the
184 # documentation for Paperclip::Storage::Filesystem and Paperclip::Storage::S3
185 # for backend-specific options.
186 def has_attached_file name, options = {}
187 include InstanceMethods
188
189 write_inheritable_attribute(:attachment_definitions, {}) if attachment_definitions.nil?
190 attachment_definitions[name] = {:validations => {}}.merge(options)
191
192 after_save :save_attached_files
193 before_destroy :destroy_attached_files
194
195 define_callbacks :before_post_process, :after_post_process
196 define_callbacks :"before_#{name}_post_process", :"after_#{name}_post_process"
197
198 define_method name do |*args|
199 a = attachment_for(name)
200 (args.length > 0) ? a.to_s(args.first) : a
201 end
202
203 define_method "#{name}=" do |file|
204 attachment_for(name).assign(file)
205 end
206
207 define_method "#{name}?" do
208 attachment_for(name).file?
209 end
210
211 validates_each(name) do |record, attr, value|
212 attachment = record.attachment_for(name)
213 attachment.send(:flush_errors) unless attachment.valid?
214 end
215 end
216
217 # Places ActiveRecord-style validations on the size of the file assigned. The
218 # possible options are:
219 # * +in+: a Range of bytes (i.e. +1..1.megabyte+),
220 # * +less_than+: equivalent to :in => 0..options[:less_than]
221 # * +greater_than+: equivalent to :in => options[:greater_than]..Infinity
222 # * +message+: error message to display, use :min and :max as replacements
223 def validates_attachment_size name, options = {}
224 min = options[:greater_than] || (options[:in] && options[:in].first) || 0
225 max = options[:less_than] || (options[:in] && options[:in].last) || (1.0/0)
226 range = (min..max)
227 message = options[:message] || "file size must be between :min and :max bytes."
228
229 attachment_definitions[name][:validations][:size] = lambda do |attachment, instance|
230 if attachment.file? && !range.include?(attachment.size.to_i)
231 message.gsub(/:min/, min.to_s).gsub(/:max/, max.to_s)
232 end
233 end
234 end
235
236 # Adds errors if thumbnail creation fails. The same as specifying :whiny_thumbnails => true.
237 def validates_attachment_thumbnails name, options = {}
238 attachment_definitions[name][:whiny_thumbnails] = true
239 end
240
241 # Places ActiveRecord-style validations on the presence of a file.
242 def validates_attachment_presence name, options = {}
243 message = options[:message] || "must be set."
244 attachment_definitions[name][:validations][:presence] = lambda do |attachment, instance|
245 message unless attachment.file?
246 end
247 end
248
249 # Places ActiveRecord-style validations on the content type of the file
250 # assigned. The possible options are:
251 # * +content_type+: Allowed content types. Can be a single content type
252 # or an array. Each type can be a String or a Regexp. It should be
253 # noted that Internet Explorer upload files with content_types that you
254 # may not expect. For example, JPEG images are given image/pjpeg and
255 # PNGs are image/x-png, so keep that in mind when determining how you
256 # match. Allows all by default.
257 # * +message+: The message to display when the uploaded file has an invalid
258 # content type.
259 # NOTE: If you do not specify an [attachment]_content_type field on your
260 # model, content_type validation will work _ONLY upon assignment_ and
261 # re-validation after the instance has been reloaded will always succeed.
262 def validates_attachment_content_type name, options = {}
263 attachment_definitions[name][:validations][:content_type] = lambda do |attachment, instance|
264 valid_types = [options[:content_type]].flatten
265
266 unless attachment.original_filename.blank?
267 unless valid_types.blank?
268 content_type = attachment.instance_read(:content_type)
269 unless valid_types.any?{|t| content_type.nil? || t === content_type }
270 options[:message] || "is not one of the allowed file types."
271 end
272 end
273 end
274 end
275 end
276
277 # Returns the attachment definitions defined by each call to
278 # has_attached_file.
279 def attachment_definitions
280 read_inheritable_attribute(:attachment_definitions)
281 end
282 end
283
284 module InstanceMethods #:nodoc:
285 def attachment_for name
286 @_paperclip_attachments ||= {}
287 @_paperclip_attachments[name] ||= Attachment.new(name, self, self.class.attachment_definitions[name])
288 end
289
290 def each_attachment
291 self.class.attachment_definitions.each do |name, definition|
292 yield(name, attachment_for(name))
293 end
294 end
295
296 def save_attached_files
297 logger.info("[paperclip] Saving attachments.")
298 each_attachment do |name, attachment|
299 attachment.send(:save)
300 end
301 end
302
303 def destroy_attached_files
304 logger.info("[paperclip] Deleting attachments.")
305 each_attachment do |name, attachment|
306 attachment.send(:queue_existing_for_delete)
307 attachment.send(:flush_deletes)
308 end
309 end
310 end
311
312end
313
314# Set it all up.
315if Object.const_defined?("ActiveRecord")
316 ActiveRecord::Base.send(:include, Paperclip)
317 File.send(:include, Paperclip::Upfile)
318end
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 @@
1module Paperclip
2 # The Attachment class manages the files for a given attachment. It saves
3 # when the model saves, deletes when the model is destroyed, and processes
4 # the file upon assignment.
5 class Attachment
6
7 def self.default_options
8 @default_options ||= {
9 :url => "/system/:attachment/:id/:style/:basename.:extension",
10 :path => ":rails_root/public/system/:attachment/:id/:style/:basename.:extension",
11 :styles => {},
12 :default_url => "/:attachment/:style/missing.png",
13 :default_style => :original,
14 :validations => {},
15 :storage => :filesystem
16 }
17 end
18
19 attr_reader :name, :instance, :styles, :default_style, :convert_options, :queued_for_write
20
21 # Creates an Attachment object. +name+ is the name of the attachment,
22 # +instance+ is the ActiveRecord object instance it's attached to, and
23 # +options+ is the same as the hash passed to +has_attached_file+.
24 def initialize name, instance, options = {}
25 @name = name
26 @instance = instance
27
28 options = self.class.default_options.merge(options)
29
30 @url = options[:url]
31 @url = @url.call(self) if @url.is_a?(Proc)
32 @path = options[:path]
33 @path = @path.call(self) if @path.is_a?(Proc)
34 @styles = options[:styles]
35 @styles = @styles.call(self) if @styles.is_a?(Proc)
36 @default_url = options[:default_url]
37 @validations = options[:validations]
38 @default_style = options[:default_style]
39 @storage = options[:storage]
40 @whiny = options[:whiny_thumbnails]
41 @convert_options = options[:convert_options] || {}
42 @processors = options[:processors] || [:thumbnail]
43 @options = options
44 @queued_for_delete = []
45 @queued_for_write = {}
46 @errors = {}
47 @validation_errors = nil
48 @dirty = false
49
50 normalize_style_definition
51 initialize_storage
52 end
53
54 # What gets called when you call instance.attachment = File. It clears
55 # errors, assigns attributes, processes the file, and runs validations. It
56 # also queues up the previous file for deletion, to be flushed away on
57 # #save of its host. In addition to form uploads, you can also assign
58 # another Paperclip attachment:
59 # new_user.avatar = old_user.avatar
60 # If the file that is assigned is not valid, the processing (i.e.
61 # thumbnailing, etc) will NOT be run.
62 def assign uploaded_file
63 %w(file_name).each do |field|
64 unless @instance.class.column_names.include?("#{name}_#{field}")
65 raise PaperclipError.new("#{@instance.class} model does not have required column '#{name}_#{field}'")
66 end
67 end
68
69 if uploaded_file.is_a?(Paperclip::Attachment)
70 uploaded_file = uploaded_file.to_file(:original)
71 close_uploaded_file = uploaded_file.respond_to?(:close)
72 end
73
74 return nil unless valid_assignment?(uploaded_file)
75
76 uploaded_file.binmode if uploaded_file.respond_to? :binmode
77 self.clear
78
79 return nil if uploaded_file.nil?
80
81 @queued_for_write[:original] = uploaded_file.to_tempfile
82 instance_write(:file_name, uploaded_file.original_filename.strip.gsub(/[^\w\d\.\-]+/, '_'))
83 instance_write(:content_type, uploaded_file.content_type.to_s.strip)
84 instance_write(:file_size, uploaded_file.size.to_i)
85 instance_write(:updated_at, Time.now)
86
87 @dirty = true
88
89 post_process if valid?
90
91 # Reset the file size if the original file was reprocessed.
92 instance_write(:file_size, @queued_for_write[:original].size.to_i)
93 ensure
94 uploaded_file.close if close_uploaded_file
95 validate
96 end
97
98 # Returns the public URL of the attachment, with a given style. Note that
99 # this does not necessarily need to point to a file that your web server
100 # can access and can point to an action in your app, if you need fine
101 # grained security. This is not recommended if you don't need the
102 # security, however, for performance reasons. set
103 # include_updated_timestamp to false if you want to stop the attachment
104 # update time appended to the url
105 def url style = default_style, include_updated_timestamp = true
106 url = original_filename.nil? ? interpolate(@default_url, style) : interpolate(@url, style)
107 include_updated_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url
108 end
109
110 # Returns the path of the attachment as defined by the :path option. If the
111 # file is stored in the filesystem the path refers to the path of the file
112 # on disk. If the file is stored in S3, the path is the "key" part of the
113 # URL, and the :bucket option refers to the S3 bucket.
114 def path style = nil #:nodoc:
115 original_filename.nil? ? nil : interpolate(@path, style)
116 end
117
118 # Alias to +url+
119 def to_s style = nil
120 url(style)
121 end
122
123 # Returns true if there are no errors on this attachment.
124 def valid?
125 validate
126 errors.empty?
127 end
128
129 # Returns an array containing the errors on this attachment.
130 def errors
131 @errors
132 end
133
134 # Returns true if there are changes that need to be saved.
135 def dirty?
136 @dirty
137 end
138
139 # Saves the file, if there are no errors. If there are, it flushes them to
140 # the instance's errors and returns false, cancelling the save.
141 def save
142 if valid?
143 flush_deletes
144 flush_writes
145 @dirty = false
146 true
147 else
148 flush_errors
149 false
150 end
151 end
152
153 # Clears out the attachment. Has the same effect as previously assigning
154 # nil to the attachment. Does NOT save. If you wish to clear AND save,
155 # use #destroy.
156 def clear
157 queue_existing_for_delete
158 @errors = {}
159 @validation_errors = nil
160 end
161
162 # Destroys the attachment. Has the same effect as previously assigning
163 # nil to the attachment *and saving*. This is permanent. If you wish to
164 # wipe out the existing attachment but not save, use #clear.
165 def destroy
166 clear
167 save
168 end
169
170 # Returns the name of the file as originally assigned, and lives in the
171 # <attachment>_file_name attribute of the model.
172 def original_filename
173 instance_read(:file_name)
174 end
175
176 # Returns the size of the file as originally assigned, and lives in the
177 # <attachment>_file_size attribute of the model.
178 def size
179 instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size)
180 end
181
182 # Returns the content_type of the file as originally assigned, and lives
183 # in the <attachment>_content_type attribute of the model.
184 def content_type
185 instance_read(:content_type)
186 end
187
188 # Returns the last modified time of the file as originally assigned, and
189 # lives in the <attachment>_updated_at attribute of the model.
190 def updated_at
191 time = instance_read(:updated_at)
192 time && time.to_i
193 end
194
195 # A hash of procs that are run during the interpolation of a path or url.
196 # A variable of the format :name will be replaced with the return value of
197 # the proc named ":name". Each lambda takes the attachment and the current
198 # style as arguments. This hash can be added to with your own proc if
199 # necessary.
200 def self.interpolations
201 @interpolations ||= {
202 :rails_root => lambda{|attachment,style| RAILS_ROOT },
203 :rails_env => lambda{|attachment,style| RAILS_ENV },
204 :class => lambda do |attachment,style|
205 attachment.instance.class.name.underscore.pluralize
206 end,
207 :basename => lambda do |attachment,style|
208 attachment.original_filename.gsub(/#{File.extname(attachment.original_filename)}$/, "")
209 end,
210 :extension => lambda do |attachment,style|
211 ((style = attachment.styles[style]) && style[:format]) ||
212 File.extname(attachment.original_filename).gsub(/^\.+/, "")
213 end,
214 :id => lambda{|attachment,style| attachment.instance.id },
215 :id_partition => lambda do |attachment, style|
216 ("%09d" % attachment.instance.id).scan(/\d{3}/).join("/")
217 end,
218 :attachment => lambda{|attachment,style| attachment.name.to_s.downcase.pluralize },
219 :style => lambda{|attachment,style| style || attachment.default_style },
220 }
221 end
222
223 # This method really shouldn't be called that often. It's expected use is
224 # in the paperclip:refresh rake task and that's it. It will regenerate all
225 # thumbnails forcefully, by reobtaining the original file and going through
226 # the post-process again.
227 def reprocess!
228 new_original = Tempfile.new("paperclip-reprocess")
229 new_original.binmode
230 if old_original = to_file(:original)
231 new_original.write( old_original.read )
232 new_original.rewind
233
234 @queued_for_write = { :original => new_original }
235 post_process
236
237 old_original.close if old_original.respond_to?(:close)
238
239 save
240 else
241 true
242 end
243 end
244
245 # Returns true if a file has been assigned.
246 def file?
247 !original_filename.blank?
248 end
249
250 # Writes the attachment-specific attribute on the instance. For example,
251 # instance_write(:file_name, "me.jpg") will write "me.jpg" to the instance's
252 # "avatar_file_name" field (assuming the attachment is called avatar).
253 def instance_write(attr, value)
254 setter = :"#{name}_#{attr}="
255 responds = instance.respond_to?(setter)
256 self.instance_variable_set("@_#{setter.to_s.chop}", value)
257 instance.send(setter, value) if responds || attr.to_s == "file_name"
258 end
259
260 # Reads the attachment-specific attribute on the instance. See instance_write
261 # for more details.
262 def instance_read(attr)
263 getter = :"#{name}_#{attr}"
264 responds = instance.respond_to?(getter)
265 cached = self.instance_variable_get("@_#{getter}")
266 return cached if cached
267 instance.send(getter) if responds || attr.to_s == "file_name"
268 end
269
270 private
271
272 def logger #:nodoc:
273 instance.logger
274 end
275
276 def log message #:nodoc:
277 logger.info("[paperclip] #{message}") if logging?
278 end
279
280 def logging? #:nodoc:
281 Paperclip.options[:log]
282 end
283
284 def valid_assignment? file #:nodoc:
285 file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type))
286 end
287
288 def validate #:nodoc:
289 unless @validation_errors
290 @validation_errors = @validations.inject({}) do |errors, validation|
291 name, block = validation
292 errors[name] = block.call(self, instance) if block
293 errors
294 end
295 @validation_errors.reject!{|k,v| v == nil }
296 @errors.merge!(@validation_errors)
297 end
298 @validation_errors
299 end
300
301 def normalize_style_definition #:nodoc:
302 @styles.each do |name, args|
303 unless args.is_a? Hash
304 dimensions, format = *args
305 @styles[name] = {
306 :processors => @processors,
307 :geometry => dimensions,
308 :format => format,
309 :whiny => @whiny,
310 :convert_options => extra_options_for(name)
311 }
312 else
313 @styles[name] = {
314 :processors => @processors,
315 :whiny => @whiny,
316 :convert_options => extra_options_for(name)
317 }.merge(@styles[name])
318 end
319 end
320 end
321
322 def solidify_style_definitions #:nodoc:
323 @styles.each do |name, args|
324 @styles[name][:geometry] = @styles[name][:geometry].call(instance) if @styles[name][:geometry].respond_to?(:call)
325 @styles[name][:processors] = @styles[name][:processors].call(instance) if @styles[name][:processors].respond_to?(:call)
326 end
327 end
328
329 def initialize_storage #:nodoc:
330 @storage_module = Paperclip::Storage.const_get(@storage.to_s.capitalize)
331 self.extend(@storage_module)
332 end
333
334 def extra_options_for(style) #:nodoc:
335 all_options = convert_options[:all]
336 all_options = all_options.call(instance) if all_options.respond_to?(:call)
337 style_options = convert_options[style]
338 style_options = style_options.call(instance) if style_options.respond_to?(:call)
339
340 [ style_options, all_options ].compact.join(" ")
341 end
342
343 def post_process #:nodoc:
344 return if @queued_for_write[:original].nil?
345 solidify_style_definitions
346 return if fire_events(:before)
347 post_process_styles
348 return if fire_events(:after)
349 end
350
351 def fire_events(which)
352 return true if callback(:"#{which}_post_process") == false
353 return true if callback(:"#{which}_#{name}_post_process") == false
354 end
355
356 def callback which #:nodoc:
357 instance.run_callbacks(which, @queued_for_write){|result, obj| result == false }
358 end
359
360 def post_process_styles
361 @styles.each do |name, args|
362 begin
363 raise RuntimeError.new("Style #{name} has no processors defined.") if args[:processors].blank?
364 @queued_for_write[name] = args[:processors].inject(@queued_for_write[:original]) do |file, processor|
365 Paperclip.processor(processor).make(file, args, self)
366 end
367 rescue PaperclipError => e
368 log("An error was received while processing: #{e.inspect}")
369 (@errors[:processing] ||= []) << e.message if @whiny
370 end
371 end
372 end
373
374 def interpolate pattern, style = default_style #:nodoc:
375 interpolations = self.class.interpolations.sort{|a,b| a.first.to_s <=> b.first.to_s }
376 interpolations.reverse.inject( pattern.dup ) do |result, interpolation|
377 tag, blk = interpolation
378 result.gsub(/:#{tag}/) do |match|
379 blk.call( self, style )
380 end
381 end
382 end
383
384 def queue_existing_for_delete #:nodoc:
385 return unless file?
386 @queued_for_delete += [:original, *@styles.keys].uniq.map do |style|
387 path(style) if exists?(style)
388 end.compact
389 instance_write(:file_name, nil)
390 instance_write(:content_type, nil)
391 instance_write(:file_size, nil)
392 instance_write(:updated_at, nil)
393 end
394
395 def flush_errors #:nodoc:
396 @errors.each do |error, message|
397 [message].flatten.each {|m| instance.errors.add(name, m) }
398 end
399 end
400
401 end
402end
403
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 @@
1module Paperclip
2 # This module is intended as a compatability shim for the differences in
3 # callbacks between Rails 2.0 and Rails 2.1.
4 module CallbackCompatability
5 def self.included(base)
6 base.extend(ClassMethods)
7 base.send(:include, InstanceMethods)
8 end
9
10 module ClassMethods
11 # The implementation of this method is taken from the Rails 1.2.6 source,
12 # from rails/activerecord/lib/active_record/callbacks.rb, line 192.
13 def define_callbacks(*args)
14 args.each do |method|
15 self.class_eval <<-"end_eval"
16 def self.#{method}(*callbacks, &block)
17 callbacks << block if block_given?
18 write_inheritable_array(#{method.to_sym.inspect}, callbacks)
19 end
20 end_eval
21 end
22 end
23 end
24
25 module InstanceMethods
26 # The callbacks in < 2.1 don't worry about the extra options or the
27 # block, so just run what we have available.
28 def run_callbacks(meth, opts = nil, &blk)
29 callback(meth)
30 end
31 end
32 end
33end
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 @@
1module Paperclip
2
3 # Defines the geometry of an image.
4 class Geometry
5 attr_accessor :height, :width, :modifier
6
7 # Gives a Geometry representing the given height and width
8 def initialize width = nil, height = nil, modifier = nil
9 @height = height.to_f
10 @width = width.to_f
11 @modifier = modifier
12 end
13
14 # Uses ImageMagick to determing the dimensions of a file, passed in as either a
15 # File or path.
16 def self.from_file file
17 file = file.path if file.respond_to? "path"
18 geometry = begin
19 Paperclip.run("identify", %Q[-format "%wx%h" "#{file}"[0]])
20 rescue PaperclipCommandLineError
21 ""
22 end
23 parse(geometry) ||
24 raise(NotIdentifiedByImageMagickError.new("#{file} is not recognized by the 'identify' command."))
25 end
26
27 # Parses a "WxH" formatted string, where W is the width and H is the height.
28 def self.parse string
29 if match = (string && string.match(/\b(\d*)x?(\d*)\b([\>\<\#\@\%^!])?/))
30 Geometry.new(*match[1,3])
31 end
32 end
33
34 # True if the dimensions represent a square
35 def square?
36 height == width
37 end
38
39 # True if the dimensions represent a horizontal rectangle
40 def horizontal?
41 height < width
42 end
43
44 # True if the dimensions represent a vertical rectangle
45 def vertical?
46 height > width
47 end
48
49 # The aspect ratio of the dimensions.
50 def aspect
51 width / height
52 end
53
54 # Returns the larger of the two dimensions
55 def larger
56 [height, width].max
57 end
58
59 # Returns the smaller of the two dimensions
60 def smaller
61 [height, width].min
62 end
63
64 # Returns the width and height in a format suitable to be passed to Geometry.parse
65 def to_s
66 s = ""
67 s << width.to_i.to_s if width > 0
68 s << "x#{height.to_i}" if height > 0
69 s << modifier.to_s
70 s
71 end
72
73 # Same as to_s
74 def inspect
75 to_s
76 end
77
78 # Returns the scaling and cropping geometries (in string-based ImageMagick format)
79 # neccessary to transform this Geometry into the Geometry given. If crop is true,
80 # then it is assumed the destination Geometry will be the exact final resolution.
81 # In this case, the source Geometry is scaled so that an image containing the
82 # destination Geometry would be completely filled by the source image, and any
83 # overhanging image would be cropped. Useful for square thumbnail images. The cropping
84 # is weighted at the center of the Geometry.
85 def transformation_to dst, crop = false
86 if crop
87 ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
88 scale_geometry, scale = scaling(dst, ratio)
89 crop_geometry = cropping(dst, ratio, scale)
90 else
91 scale_geometry = dst.to_s
92 end
93
94 [ scale_geometry, crop_geometry ]
95 end
96
97 private
98
99 def scaling dst, ratio
100 if ratio.horizontal? || ratio.square?
101 [ "%dx" % dst.width, ratio.width ]
102 else
103 [ "x%d" % dst.height, ratio.height ]
104 end
105 end
106
107 def cropping dst, ratio, scale
108 if ratio.horizontal? || ratio.square?
109 "%dx%d+%d+%d" % [ dst.width, dst.height, 0, (self.height * scale - dst.height) / 2 ]
110 else
111 "%dx%d+%d+%d" % [ dst.width, dst.height, (self.width * scale - dst.width) / 2, 0 ]
112 end
113 end
114 end
115end
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 @@
1# Provides method that can be included on File-type objects (IO, StringIO, Tempfile, etc) to allow stream copying
2# and Tempfile conversion.
3module IOStream
4
5 # Returns a Tempfile containing the contents of the readable object.
6 def to_tempfile
7 tempfile = Tempfile.new("stream")
8 tempfile.binmode
9 self.stream_to(tempfile)
10 end
11
12 # Copies one read-able object from one place to another in blocks, obviating the need to load
13 # the whole thing into memory. Defaults to 8k blocks. If this module is included in both
14 # StringIO and Tempfile, then either can have its data copied anywhere else without typing
15 # worries or memory overhead worries. Returns a File if a String is passed in as the destination
16 # and returns the IO or Tempfile as passed in if one is sent as the destination.
17 def stream_to path_or_file, in_blocks_of = 8192
18 dstio = case path_or_file
19 when String then File.new(path_or_file, "wb+")
20 when IO then path_or_file
21 when Tempfile then path_or_file
22 end
23 buffer = ""
24 self.rewind
25 while self.read(in_blocks_of, buffer) do
26 dstio.write(buffer)
27 end
28 dstio.rewind
29 dstio
30 end
31end
32
33class IO #:nodoc:
34 include IOStream
35end
36
37%w( Tempfile StringIO ).each do |klass|
38 if Object.const_defined? klass
39 Object.const_get(klass).class_eval do
40 include IOStream
41 end
42 end
43end
44
45# Corrects a bug in Windows when asking for Tempfile size.
46if defined? Tempfile
47 class Tempfile
48 def size
49 if @tmpfile
50 @tmpfile.fsync
51 @tmpfile.flush
52 @tmpfile.stat.size
53 else
54 0
55 end
56 end
57 end
58end
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 @@
1require 'paperclip/matchers/have_attached_file_matcher'
2require 'paperclip/matchers/validate_attachment_presence_matcher'
3require 'paperclip/matchers/validate_attachment_content_type_matcher'
4require '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 @@
1module Paperclip
2 module Shoulda
3 module Matchers
4 def have_attached_file name
5 HaveAttachedFileMatcher.new(name)
6 end
7
8 class HaveAttachedFileMatcher
9 def initialize attachment_name
10 @attachment_name = attachment_name
11 end
12
13 def matches? subject
14 @subject = subject
15 responds? && has_column? && included?
16 end
17
18 def failure_message
19 "Should have an attachment named #{@attachment_name}"
20 end
21
22 def negative_failure_message
23 "Should not have an attachment named #{@attachment_name}"
24 end
25
26 def description
27 "have an attachment named #{@attachment_name}"
28 end
29
30 protected
31
32 def responds?
33 methods = @subject.instance_methods
34 methods.include?("#{@attachment_name}") &&
35 methods.include?("#{@attachment_name}=") &&
36 methods.include?("#{@attachment_name}?")
37 end
38
39 def has_column?
40 @subject.column_names.include?("#{@attachment_name}_file_name")
41 end
42
43 def included?
44 @subject.ancestors.include?(Paperclip::InstanceMethods)
45 end
46 end
47 end
48 end
49end
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 @@
1module Paperclip
2 module Shoulda
3 module Matchers
4 def validate_attachment_content_type name
5 ValidateAttachmentContentTypeMatcher.new(name)
6 end
7
8 class ValidateAttachmentContentTypeMatcher
9 def initialize attachment_name
10 @attachment_name = attachment_name
11 end
12
13 def allowing *types
14 @allowed_types = types.flatten
15 self
16 end
17
18 def rejecting *types
19 @rejected_types = types.flatten
20 self
21 end
22
23 def matches? subject
24 @subject = subject
25 @allowed_types && @rejected_types &&
26 allowed_types_allowed? && rejected_types_rejected?
27 end
28
29 def failure_message
30 "Content types #{@allowed_types.join(", ")} should be accepted" +
31 " and #{@rejected_types.join(", ")} rejected by #{@attachment_name}"
32 end
33
34 def negative_failure_message
35 "Content types #{@allowed_types.join(", ")} should be rejected" +
36 " and #{@rejected_types.join(", ")} accepted by #{@attachment_name}"
37 end
38
39 def description
40 "validate the content types allowed on attachment #{@attachment_name}"
41 end
42
43 protected
44
45 def allow_types?(types)
46 types.all? do |type|
47 file = StringIO.new(".")
48 file.content_type = type
49 attachment = @subject.new.attachment_for(@attachment_name)
50 attachment.assign(file)
51 attachment.errors[:content_type].nil?
52 end
53 end
54
55 def allowed_types_allowed?
56 allow_types?(@allowed_types)
57 end
58
59 def rejected_types_rejected?
60 not allow_types?(@rejected_types)
61 end
62 end
63 end
64 end
65end
66
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 @@
1module Paperclip
2 module Shoulda
3 module Matchers
4 def validate_attachment_presence name
5 ValidateAttachmentPresenceMatcher.new(name)
6 end
7
8 class ValidateAttachmentPresenceMatcher
9 def initialize attachment_name
10 @attachment_name = attachment_name
11 end
12
13 def matches? subject
14 @subject = subject
15 error_when_not_valid? && no_error_when_valid?
16 end
17
18 def failure_message
19 "Attachment #{@attachment_name} should be required"
20 end
21
22 def negative_failure_message
23 "Attachment #{@attachment_name} should not be required"
24 end
25
26 def description
27 "require presence of attachment #{@attachment_name}"
28 end
29
30 protected
31
32 def error_when_not_valid?
33 @attachment = @subject.new.send(@attachment_name)
34 @attachment.assign(nil)
35 not @attachment.errors[:presence].nil?
36 end
37
38 def no_error_when_valid?
39 @file = StringIO.new(".")
40 @attachment = @subject.new.send(@attachment_name)
41 @attachment.assign(@file)
42 @attachment.errors[:presence].nil?
43 end
44 end
45 end
46 end
47end
48
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 @@
1module Paperclip
2 module Shoulda
3 module Matchers
4 def validate_attachment_size name
5 ValidateAttachmentSizeMatcher.new(name)
6 end
7
8 class ValidateAttachmentSizeMatcher
9 def initialize attachment_name
10 @attachment_name = attachment_name
11 @low, @high = 0, (1.0/0)
12 end
13
14 def less_than size
15 @high = size
16 self
17 end
18
19 def greater_than size
20 @low = size
21 self
22 end
23
24 def in range
25 @low, @high = range.first, range.last
26 self
27 end
28
29 def matches? subject
30 @subject = subject
31 lower_than_low? && higher_than_low? && lower_than_high? && higher_than_high?
32 end
33
34 def failure_message
35 "Attachment #{@attachment_name} must be between #{@low} and #{@high} bytes"
36 end
37
38 def negative_failure_message
39 "Attachment #{@attachment_name} cannot be between #{@low} and #{@high} bytes"
40 end
41
42 def description
43 "validate the size of attachment #{@attachment_name}"
44 end
45
46 protected
47
48 def override_method object, method, &replacement
49 (class << object; self; end).class_eval do
50 define_method(method, &replacement)
51 end
52 end
53
54 def passes_validation_with_size(new_size)
55 file = StringIO.new(".")
56 override_method(file, :size){ new_size }
57 attachment = @subject.new.attachment_for(@attachment_name)
58 attachment.assign(file)
59 attachment.errors[:size].nil?
60 end
61
62 def lower_than_low?
63 not passes_validation_with_size(@low - 1)
64 end
65
66 def higher_than_low?
67 passes_validation_with_size(@low + 1)
68 end
69
70 def lower_than_high?
71 return true if @high == (1.0/0)
72 passes_validation_with_size(@high - 1)
73 end
74
75 def higher_than_high?
76 return true if @high == (1.0/0)
77 not passes_validation_with_size(@high + 1)
78 end
79 end
80 end
81 end
82end
83
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 @@
1module Paperclip
2 # Paperclip processors allow you to modify attached files when they are
3 # attached in any way you are able. Paperclip itself uses command-line
4 # programs for its included Thumbnail processor, but custom processors
5 # are not required to follow suit.
6 #
7 # Processors are required to be defined inside the Paperclip module and
8 # are also required to be a subclass of Paperclip::Processor. There are
9 # only two methods you must implement to properly be a subclass:
10 # #initialize and #make. Initialize's arguments are the file that will
11 # be operated on (which is an instance of File), and a hash of options
12 # that were defined in has_attached_file's style hash.
13 #
14 # All #make needs to do is return an instance of File (Tempfile is
15 # acceptable) which contains the results of the processing.
16 #
17 # See Paperclip.run for more information about using command-line
18 # utilities from within Processors.
19 class Processor
20 attr_accessor :file, :options, :attachment
21
22 def initialize file, options = {}, attachment = nil
23 @file = file
24 @options = options
25 @attachment = attachment
26 end
27
28 def make
29 end
30
31 def self.make file, options = {}, attachment = nil
32 new(file, options, attachment).make
33 end
34 end
35
36 # Due to how ImageMagick handles its image format conversion and how Tempfile
37 # handles its naming scheme, it is necessary to override how Tempfile makes
38 # its names so as to allow for file extensions. Idea taken from the comments
39 # on this blog post:
40 # http://marsorange.com/archives/of-mogrify-ruby-tempfile-dynamic-class-definitions
41 class Tempfile < ::Tempfile
42 # Replaces Tempfile's +make_tmpname+ with one that honors file extensions.
43 def make_tmpname(basename, n)
44 extension = File.extname(basename)
45 sprintf("%s,%d,%d%s", File.basename(basename, extension), $$, n, extension)
46 end
47 end
48end
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 @@
1module Paperclip
2 module Storage
3
4 # The default place to store attachments is in the filesystem. Files on the local
5 # filesystem can be very easily served by Apache without requiring a hit to your app.
6 # They also can be processed more easily after they've been saved, as they're just
7 # normal files. There is one Filesystem-specific option for has_attached_file.
8 # * +path+: The location of the repository of attachments on disk. This can (and, in
9 # almost all cases, should) be coordinated with the value of the +url+ option to
10 # allow files to be saved into a place where Apache can serve them without
11 # hitting your app. Defaults to
12 # ":rails_root/public/:attachment/:id/:style/:basename.:extension"
13 # By default this places the files in the app's public directory which can be served
14 # directly. If you are using capistrano for deployment, a good idea would be to
15 # make a symlink to the capistrano-created system directory from inside your app's
16 # public directory.
17 # See Paperclip::Attachment#interpolate for more information on variable interpolaton.
18 # :path => "/var/app/attachments/:class/:id/:style/:basename.:extension"
19 module Filesystem
20 def self.extended base
21 end
22
23 def exists?(style = default_style)
24 if original_filename
25 File.exist?(path(style))
26 else
27 false
28 end
29 end
30
31 # Returns representation of the data of the file assigned to the given
32 # style, in the format most representative of the current storage.
33 def to_file style = default_style
34 @queued_for_write[style] || (File.new(path(style), 'rb') if exists?(style))
35 end
36 alias_method :to_io, :to_file
37
38 def flush_writes #:nodoc:
39 @queued_for_write.each do |style, file|
40 file.close
41 FileUtils.mkdir_p(File.dirname(path(style)))
42 logger.info("[paperclip] saving #{path(style)}")
43 FileUtils.mv(file.path, path(style))
44 FileUtils.chmod(0644, path(style))
45 end
46 @queued_for_write = {}
47 end
48
49 def flush_deletes #:nodoc:
50 @queued_for_delete.each do |path|
51 begin
52 logger.info("[paperclip] deleting #{path}")
53 FileUtils.rm(path) if File.exist?(path)
54 rescue Errno::ENOENT => e
55 # ignore file-not-found, let everything else pass
56 end
57 begin
58 while(true)
59 path = File.dirname(path)
60 FileUtils.rmdir(path)
61 end
62 rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR
63 # Stop trying to remove parent directories
64 rescue SystemCallError => e
65 logger.info("[paperclip] There was an unexpected error while deleting directories: #{e.class}")
66 # Ignore it
67 end
68 end
69 @queued_for_delete = []
70 end
71 end
72
73 # Amazon's S3 file hosting service is a scalable, easy place to store files for
74 # distribution. You can find out more about it at http://aws.amazon.com/s3
75 # There are a few S3-specific options for has_attached_file:
76 # * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
77 # to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
78 # gives you. You can 'environment-space' this just like you do to your
79 # database.yml file, so different environments can use different accounts:
80 # development:
81 # access_key_id: 123...
82 # secret_access_key: 123...
83 # test:
84 # access_key_id: abc...
85 # secret_access_key: abc...
86 # production:
87 # access_key_id: 456...
88 # secret_access_key: 456...
89 # This is not required, however, and the file may simply look like this:
90 # access_key_id: 456...
91 # secret_access_key: 456...
92 # In which case, those access keys will be used in all environments. You can also
93 # put your bucket name in this file, instead of adding it to the code directly.
94 # This is useful when you want the same account but a different bucket for
95 # development versus production.
96 # * +s3_permissions+: This is a String that should be one of the "canned" access
97 # policies that S3 provides (more information can be found here:
98 # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
99 # The default for Paperclip is "public-read".
100 # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
101 # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are 'public-read' (the
102 # default), and 'https' when your :s3_permissions are anything else.
103 # * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate}
104 # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
105 # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
106 # Paperclip will attempt to create it. The bucket name will not be interpolated.
107 # You can define the bucket as a Proc if you want to determine it's name at runtime.
108 # Paperclip will call that Proc with attachment as the only argument.
109 # * +s3_host_alias+: The fully-qualified domain name (FQDN) that is the alias to the
110 # S3 domain of your bucket. Used with the :s3_alias_url url interpolation. See the
111 # link in the +url+ entry for more information about S3 domains and buckets.
112 # * +url+: There are three options for the S3 url. You can choose to have the bucket's name
113 # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
114 # Lastly, you can specify a CNAME (which requires the CNAME to be specified as
115 # :s3_alias_url. You can read more about CNAMEs and S3 at
116 # http://docs.amazonwebservices.com/AmazonS3/latest/index.html?VirtualHosting.html
117 # Normally, this won't matter in the slightest and you can leave the default (which is
118 # path-style, or :s3_path_url). But in some cases paths don't work and you need to use
119 # the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
120 # NOTE: If you use a CNAME for use with CloudFront, you can NOT specify https as your
121 # :s3_protocol; This is *not supported* by S3/CloudFront. Finally, when using the host
122 # alias, the :bucket parameter is ignored, as the hostname is used as the bucket name
123 # by S3.
124 # * +path+: This is the key under the bucket in which the file will be stored. The
125 # URL will be constructed from the bucket and the path. This is what you will want
126 # to interpolate. Keys should be unique, like filenames, and despite the fact that
127 # S3 (strictly speaking) does not support directories, you can still use a / to
128 # separate parts of your file name.
129 module S3
130 def self.extended base
131 require 'right_aws'
132 base.instance_eval do
133 @s3_credentials = parse_credentials(@options[:s3_credentials])
134 @bucket = @options[:bucket] || @s3_credentials[:bucket]
135 @bucket = @bucket.call(self) if @bucket.is_a?(Proc)
136 @s3_options = @options[:s3_options] || {}
137 @s3_permissions = @options[:s3_permissions] || 'public-read'
138 @s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https')
139 @s3_headers = @options[:s3_headers] || {}
140 @s3_host_alias = @options[:s3_host_alias]
141 @url = ":s3_path_url" unless @url.to_s.match(/^:s3.*url$/)
142 end
143 base.class.interpolations[:s3_alias_url] = lambda do |attachment, style|
144 "#{attachment.s3_protocol}://#{attachment.s3_host_alias}/#{attachment.path(style).gsub(%r{^/}, "")}"
145 end
146 base.class.interpolations[:s3_path_url] = lambda do |attachment, style|
147 "#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
148 end
149 base.class.interpolations[:s3_domain_url] = lambda do |attachment, style|
150 "#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}"
151 end
152 end
153
154 def s3
155 @s3 ||= RightAws::S3.new(@s3_credentials[:access_key_id],
156 @s3_credentials[:secret_access_key],
157 @s3_options)
158 end
159
160 def s3_bucket
161 @s3_bucket ||= s3.bucket(@bucket, true, @s3_permissions)
162 end
163
164 def bucket_name
165 @bucket
166 end
167
168 def s3_host_alias
169 @s3_host_alias
170 end
171
172 def parse_credentials creds
173 creds = find_credentials(creds).stringify_keys
174 (creds[ENV['RAILS_ENV']] || creds).symbolize_keys
175 end
176
177 def exists?(style = default_style)
178 s3_bucket.key(path(style)) ? true : false
179 end
180
181 def s3_protocol
182 @s3_protocol
183 end
184
185 # Returns representation of the data of the file assigned to the given
186 # style, in the format most representative of the current storage.
187 def to_file style = default_style
188 @queued_for_write[style] || s3_bucket.key(path(style))
189 end
190 alias_method :to_io, :to_file
191
192 def flush_writes #:nodoc:
193 @queued_for_write.each do |style, file|
194 begin
195 logger.info("[paperclip] saving #{path(style)}")
196 key = s3_bucket.key(path(style))
197 key.data = file
198 key.put(nil, @s3_permissions, {'Content-type' => instance_read(:content_type)}.merge(@s3_headers))
199 rescue RightAws::AwsError => e
200 raise
201 end
202 end
203 @queued_for_write = {}
204 end
205
206 def flush_deletes #:nodoc:
207 @queued_for_delete.each do |path|
208 begin
209 logger.info("[paperclip] deleting #{path}")
210 if file = s3_bucket.key(path)
211 file.delete
212 end
213 rescue RightAws::AwsError
214 # Ignore this.
215 end
216 end
217 @queued_for_delete = []
218 end
219
220 def find_credentials creds
221 case creds
222 when File
223 YAML.load_file(creds.path)
224 when String
225 YAML.load_file(creds)
226 when Hash
227 creds
228 else
229 raise ArgumentError, "Credentials are not a path, file, or hash."
230 end
231 end
232 private :find_credentials
233
234 end
235 end
236end
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 @@
1module Paperclip
2 # Handles thumbnailing images that are uploaded.
3 class Thumbnail < Processor
4
5 attr_accessor :current_geometry, :target_geometry, :format, :whiny, :convert_options
6
7 # Creates a Thumbnail object set to work on the +file+ given. It
8 # will attempt to transform the image into one defined by +target_geometry+
9 # which is a "WxH"-style string. +format+ will be inferred from the +file+
10 # unless specified. Thumbnail creation will raise no errors unless
11 # +whiny+ is true (which it is, by default. If +convert_options+ is
12 # set, the options will be appended to the convert command upon image conversion
13 def initialize file, options = {}, attachment = nil
14 super
15 geometry = options[:geometry]
16 @file = file
17 @crop = geometry[-1,1] == '#'
18 @target_geometry = Geometry.parse geometry
19 @current_geometry = Geometry.from_file @file
20 @convert_options = options[:convert_options]
21 @whiny = options[:whiny].nil? ? true : options[:whiny]
22 @format = options[:format]
23
24 @current_format = File.extname(@file.path)
25 @basename = File.basename(@file.path, @current_format)
26 end
27
28 # Returns true if the +target_geometry+ is meant to crop.
29 def crop?
30 @crop
31 end
32
33 # Returns true if the image is meant to make use of additional convert options.
34 def convert_options?
35 not @convert_options.blank?
36 end
37
38 # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
39 # that contains the new image.
40 def make
41 src = @file
42 dst = Tempfile.new([@basename, @format].compact.join("."))
43 dst.binmode
44
45 command = <<-end_command
46 "#{ File.expand_path(src.path) }[0]"
47 #{ transformation_command }
48 "#{ File.expand_path(dst.path) }"
49 end_command
50
51 begin
52 success = Paperclip.run("convert", command.gsub(/\s+/, " "))
53 rescue PaperclipCommandLineError
54 raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny
55 end
56
57 dst
58 end
59
60 # Returns the command ImageMagick's +convert+ needs to transform the image
61 # into the thumbnail.
62 def transformation_command
63 scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
64 trans = "-resize \"#{scale}\""
65 trans << " -crop \"#{crop}\" +repage" if crop
66 trans << " #{convert_options}" if convert_options?
67 trans
68 end
69 end
70end
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 @@
1module Paperclip
2 # The Upfile module is a convenience module for adding uploaded-file-type methods
3 # to the +File+ class. Useful for testing.
4 # user.avatar = File.new("test/test_avatar.jpg")
5 module Upfile
6
7 # Infer the MIME-type of the file from the extension.
8 def content_type
9 type = (self.path.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase
10 case type
11 when %r"jpe?g" then "image/jpeg"
12 when %r"tiff?" then "image/tiff"
13 when %r"png", "gif", "bmp" then "image/#{type}"
14 when "txt" then "text/plain"
15 when %r"html?" then "text/html"
16 when "csv", "xml", "css", "js" then "text/#{type}"
17 else "application/x-#{type}"
18 end
19 end
20
21 # Returns the file's normal name.
22 def original_filename
23 File.basename(self.path)
24 end
25
26 # Returns the size of the file.
27 def size
28 File.size(self)
29 end
30 end
31end
32
33if defined? StringIO
34 class StringIO
35 attr_accessor :original_filename, :content_type
36 def original_filename
37 @original_filename ||= "stringio.txt"
38 end
39 def content_type
40 @content_type ||= "text/plain"
41 end
42 end
43end
44
45class File #:nodoc:
46 include Paperclip::Upfile
47end
48