betelgeuse-paperclip 2.2.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,404 @@
1
+ module 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, nil].flatten[0..1]
305
+ format = nil if format.blank?
306
+ @styles[name] = {
307
+ :processors => @processors,
308
+ :geometry => dimensions,
309
+ :format => format,
310
+ :whiny => @whiny,
311
+ :convert_options => extra_options_for(name)
312
+ }
313
+ else
314
+ @styles[name] = {
315
+ :processors => @processors,
316
+ :whiny => @whiny,
317
+ :convert_options => extra_options_for(name)
318
+ }.merge(@styles[name])
319
+ end
320
+ end
321
+ end
322
+
323
+ def solidify_style_definitions #:nodoc:
324
+ @styles.each do |name, args|
325
+ @styles[name][:geometry] = @styles[name][:geometry].call(instance) if @styles[name][:geometry].respond_to?(:call)
326
+ @styles[name][:processors] = @styles[name][:processors].call(instance) if @styles[name][:processors].respond_to?(:call)
327
+ end
328
+ end
329
+
330
+ def initialize_storage #:nodoc:
331
+ @storage_module = Paperclip::Storage.const_get(@storage.to_s.capitalize)
332
+ self.extend(@storage_module)
333
+ end
334
+
335
+ def extra_options_for(style) #:nodoc:
336
+ all_options = convert_options[:all]
337
+ all_options = all_options.call(instance) if all_options.respond_to?(:call)
338
+ style_options = convert_options[style]
339
+ style_options = style_options.call(instance) if style_options.respond_to?(:call)
340
+
341
+ [ style_options, all_options ].compact.join(" ")
342
+ end
343
+
344
+ def post_process #:nodoc:
345
+ return if @queued_for_write[:original].nil?
346
+ solidify_style_definitions
347
+ return if fire_events(:before)
348
+ post_process_styles
349
+ return if fire_events(:after)
350
+ end
351
+
352
+ def fire_events(which)
353
+ return true if callback(:"#{which}_post_process") == false
354
+ return true if callback(:"#{which}_#{name}_post_process") == false
355
+ end
356
+
357
+ def callback which #:nodoc:
358
+ instance.run_callbacks(which, @queued_for_write){|result, obj| result == false }
359
+ end
360
+
361
+ def post_process_styles
362
+ @styles.each do |name, args|
363
+ begin
364
+ raise RuntimeError.new("Style #{name} has no processors defined.") if args[:processors].blank?
365
+ @queued_for_write[name] = args[:processors].inject(@queued_for_write[:original]) do |file, processor|
366
+ Paperclip.processor(processor).make(file, args, self)
367
+ end
368
+ rescue PaperclipError => e
369
+ log("An error was received while processing: #{e.inspect}")
370
+ (@errors[:processing] ||= []) << e.message if @whiny
371
+ end
372
+ end
373
+ end
374
+
375
+ def interpolate pattern, style = default_style #:nodoc:
376
+ interpolations = self.class.interpolations.sort{|a,b| a.first.to_s <=> b.first.to_s }
377
+ interpolations.reverse.inject( pattern.dup ) do |result, interpolation|
378
+ tag, blk = interpolation
379
+ result.gsub(/:#{tag}/) do |match|
380
+ blk.call( self, style )
381
+ end
382
+ end
383
+ end
384
+
385
+ def queue_existing_for_delete #:nodoc:
386
+ return unless file?
387
+ @queued_for_delete += [:original, *@styles.keys].uniq.map do |style|
388
+ path(style) if exists?(style)
389
+ end.compact
390
+ instance_write(:file_name, nil)
391
+ instance_write(:content_type, nil)
392
+ instance_write(:file_size, nil)
393
+ instance_write(:updated_at, nil)
394
+ end
395
+
396
+ def flush_errors #:nodoc:
397
+ @errors.each do |error, message|
398
+ [message].flatten.each {|m| instance.errors.add(name, m) }
399
+ end
400
+ end
401
+
402
+ end
403
+ end
404
+
@@ -0,0 +1,33 @@
1
+ module 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
33
+ end
@@ -0,0 +1,115 @@
1
+ module 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
115
+ end