jmcnevin-paperclip 2.4.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/LICENSE +26 -0
  2. data/README.md +414 -0
  3. data/Rakefile +86 -0
  4. data/generators/paperclip/USAGE +5 -0
  5. data/generators/paperclip/paperclip_generator.rb +27 -0
  6. data/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  7. data/init.rb +4 -0
  8. data/lib/generators/paperclip/USAGE +8 -0
  9. data/lib/generators/paperclip/paperclip_generator.rb +33 -0
  10. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  11. data/lib/paperclip.rb +480 -0
  12. data/lib/paperclip/attachment.rb +520 -0
  13. data/lib/paperclip/callback_compatibility.rb +61 -0
  14. data/lib/paperclip/geometry.rb +155 -0
  15. data/lib/paperclip/interpolations.rb +171 -0
  16. data/lib/paperclip/iostream.rb +45 -0
  17. data/lib/paperclip/matchers.rb +33 -0
  18. data/lib/paperclip/matchers/have_attached_file_matcher.rb +57 -0
  19. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +81 -0
  20. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +54 -0
  21. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +95 -0
  22. data/lib/paperclip/missing_attachment_styles.rb +87 -0
  23. data/lib/paperclip/options.rb +78 -0
  24. data/lib/paperclip/processor.rb +58 -0
  25. data/lib/paperclip/railtie.rb +26 -0
  26. data/lib/paperclip/storage.rb +3 -0
  27. data/lib/paperclip/storage/filesystem.rb +81 -0
  28. data/lib/paperclip/storage/fog.rb +163 -0
  29. data/lib/paperclip/storage/s3.rb +270 -0
  30. data/lib/paperclip/style.rb +95 -0
  31. data/lib/paperclip/thumbnail.rb +105 -0
  32. data/lib/paperclip/upfile.rb +62 -0
  33. data/lib/paperclip/version.rb +3 -0
  34. data/lib/tasks/paperclip.rake +101 -0
  35. data/rails/init.rb +2 -0
  36. data/shoulda_macros/paperclip.rb +124 -0
  37. data/test/attachment_test.rb +1161 -0
  38. data/test/database.yml +4 -0
  39. data/test/fixtures/12k.png +0 -0
  40. data/test/fixtures/50x50.png +0 -0
  41. data/test/fixtures/5k.png +0 -0
  42. data/test/fixtures/animated.gif +0 -0
  43. data/test/fixtures/bad.png +1 -0
  44. data/test/fixtures/double spaces in name.png +0 -0
  45. data/test/fixtures/fog.yml +8 -0
  46. data/test/fixtures/s3.yml +8 -0
  47. data/test/fixtures/spaced file.png +0 -0
  48. data/test/fixtures/text.txt +1 -0
  49. data/test/fixtures/twopage.pdf +0 -0
  50. data/test/fixtures/uppercase.PNG +0 -0
  51. data/test/fog_test.rb +192 -0
  52. data/test/geometry_test.rb +206 -0
  53. data/test/helper.rb +158 -0
  54. data/test/integration_test.rb +781 -0
  55. data/test/interpolations_test.rb +202 -0
  56. data/test/iostream_test.rb +71 -0
  57. data/test/matchers/have_attached_file_matcher_test.rb +24 -0
  58. data/test/matchers/validate_attachment_content_type_matcher_test.rb +87 -0
  59. data/test/matchers/validate_attachment_presence_matcher_test.rb +26 -0
  60. data/test/matchers/validate_attachment_size_matcher_test.rb +51 -0
  61. data/test/options_test.rb +75 -0
  62. data/test/paperclip_missing_attachment_styles_test.rb +80 -0
  63. data/test/paperclip_test.rb +340 -0
  64. data/test/processor_test.rb +10 -0
  65. data/test/storage/filesystem_test.rb +56 -0
  66. data/test/storage/s3_live_test.rb +88 -0
  67. data/test/storage/s3_test.rb +689 -0
  68. data/test/style_test.rb +180 -0
  69. data/test/thumbnail_test.rb +383 -0
  70. data/test/upfile_test.rb +53 -0
  71. metadata +294 -0
@@ -0,0 +1,520 @@
1
+ # encoding: utf-8
2
+ require 'uri'
3
+
4
+ module Paperclip
5
+ # The Attachment class manages the files for a given attachment. It saves
6
+ # when the model saves, deletes when the model is destroyed, and processes
7
+ # the file upon assignment.
8
+ class Attachment
9
+ include IOStream
10
+
11
+ def self.default_options
12
+ @default_options ||= {
13
+ :url => "/system/:attachment/:id/:style/:filename",
14
+ :path => ":rails_root/public:url",
15
+ :styles => {},
16
+ :only_process => [],
17
+ :processors => [:thumbnail],
18
+ :convert_options => {},
19
+ :source_file_options => {},
20
+ :default_url => "/:attachment/:style/missing.png",
21
+ :default_style => :original,
22
+ :storage => :filesystem,
23
+ :use_timestamp => true,
24
+ :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails],
25
+ :use_default_time_zone => true,
26
+ :hash_digest => "SHA1",
27
+ :hash_data => ":class/:attachment/:id/:style/:updated_at",
28
+ :preserve_files => false
29
+ }
30
+ end
31
+
32
+ attr_reader :name, :instance, :default_style, :convert_options, :queued_for_write, :whiny, :options, :interpolator
33
+ attr_accessor :post_processing
34
+
35
+ # Creates an Attachment object. +name+ is the name of the attachment,
36
+ # +instance+ is the ActiveRecord object instance it's attached to, and
37
+ # +options+ is the same as the hash passed to +has_attached_file+.
38
+ #
39
+ # Options include:
40
+ #
41
+ # +url+ - a relative URL of the attachment. This is interpolated using +interpolator+
42
+ # +path+ - where on the filesystem to store the attachment. This is interpolated using +interpolator+
43
+ # +styles+ - a hash of options for processing the attachment. See +has_attached_file+ for the details
44
+ # +only_process+ - style args to be run through the post-processor. This defaults to the empty list
45
+ # +default_url+ - a URL for the missing image
46
+ # +default_style+ - the style to use when don't specify an argument to e.g. #url, #path
47
+ # +storage+ - the storage mechanism. Defaults to :filesystem
48
+ # +use_timestamp+ - whether to append an anti-caching timestamp to image URLs. Defaults to true
49
+ # +whiny+, +whiny_thumbnails+ - whether to raise when thumbnailing fails
50
+ # +use_default_time_zone+ - related to +use_timestamp+. Defaults to true
51
+ # +hash_digest+ - a string representing a class that will be used to hash URLs for obfuscation
52
+ # +hash_data+ - the relative URL for the hash data. This is interpolated using +interpolator+
53
+ # +hash_secret+ - a secret passed to the +hash_digest+
54
+ # +convert_options+ - flags passed to the +convert+ command for processing
55
+ # +source_file_options+ - flags passed to the +convert+ command that controls how the file is read
56
+ # +processors+ - classes that transform the attachment. Defaults to [:thumbnail]
57
+ # +preserve_files+ - whether to keep files on the filesystem when deleting to clearing the attachment. Defaults to false
58
+ # +interpolator+ - the object used to interpolate filenames and URLs. Defaults to Paperclip::Interpolations
59
+ def initialize name, instance, options = {}
60
+ @name = name
61
+ @instance = instance
62
+
63
+ options = self.class.default_options.merge(options)
64
+
65
+ @options = Paperclip::Options.new(self, options)
66
+ @post_processing = true
67
+ @queued_for_delete = []
68
+ @queued_for_write = {}
69
+ @errors = {}
70
+ @dirty = false
71
+ @interpolator = (options[:interpolator] || Paperclip::Interpolations)
72
+ @dimensions = {}
73
+
74
+ initialize_storage
75
+ end
76
+
77
+ # [:url, :path, :only_process, :normalized_styles, :default_url, :default_style,
78
+ # :storage, :use_timestamp, :whiny, :use_default_time_zone, :hash_digest, :hash_secret,
79
+ # :convert_options, :preserve_files].each do |field|
80
+ # define_method field do
81
+ # @options.send(field)
82
+ # end
83
+ # end
84
+
85
+ # What gets called when you call instance.attachment = File. It clears
86
+ # errors, assigns attributes, and processes the file. It
87
+ # also queues up the previous file for deletion, to be flushed away on
88
+ # #save of its host. In addition to form uploads, you can also assign
89
+ # another Paperclip attachment:
90
+ # new_user.avatar = old_user.avatar
91
+ def assign uploaded_file
92
+ ensure_required_accessors!
93
+
94
+ if uploaded_file.is_a?(Paperclip::Attachment)
95
+ uploaded_filename = uploaded_file.original_filename
96
+ uploaded_file = uploaded_file.to_file(:original)
97
+ close_uploaded_file = uploaded_file.respond_to?(:close)
98
+ else
99
+ instance_write(:uploaded_file, uploaded_file) if uploaded_file
100
+ end
101
+
102
+ return nil unless valid_assignment?(uploaded_file)
103
+
104
+ uploaded_file.binmode if uploaded_file.respond_to? :binmode
105
+ self.clear
106
+
107
+ return nil if uploaded_file.nil?
108
+
109
+ uploaded_filename ||= uploaded_file.original_filename
110
+ @queued_for_write[:original] = to_tempfile(uploaded_file)
111
+ instance_write(:file_name, uploaded_filename.strip)
112
+ instance_write(:content_type, uploaded_file.content_type.to_s.strip)
113
+ instance_write(:file_size, uploaded_file.size.to_i)
114
+ instance_write(:fingerprint, generate_fingerprint(uploaded_file))
115
+ instance_write(:updated_at, Time.now)
116
+
117
+ @dirty = true
118
+
119
+ post_process(*@options.only_process) if post_processing
120
+
121
+ # Reset the file size if the original file was reprocessed.
122
+ instance_write(:file_size, @queued_for_write[:original].size.to_i)
123
+ instance_write(:fingerprint, generate_fingerprint(@queued_for_write[:original]))
124
+
125
+ if image? and
126
+ @instance.class.column_names.include?("#{name}_width") and
127
+ @instance.class.column_names.include?("#{name}_height")
128
+
129
+ begin
130
+ geometry = Paperclip::Geometry.from_file(@queued_for_write[:original])
131
+ instance_write(:width, geometry.width.to_i)
132
+ instance_write(:height, geometry.height.to_i)
133
+ rescue NotIdentifiedByImageMagickError => e
134
+ log("Couldn't get dimensions for #{name}: #{e}")
135
+ end
136
+ else
137
+ instance_write(:width, nil)
138
+ instance_write(:height, nil)
139
+ end
140
+ ensure
141
+ uploaded_file.close if close_uploaded_file
142
+ end
143
+
144
+ # Returns the public URL of the attachment, with a given style. Note that
145
+ # this does not necessarily need to point to a file that your web server
146
+ # can access and can point to an action in your app, if you need fine
147
+ # grained security. This is not recommended if you don't need the
148
+ # security, however, for performance reasons. Set use_timestamp to false
149
+ # if you want to stop the attachment update time appended to the url
150
+ def url(style_name = default_style, options = {})
151
+ options = handle_url_options(options)
152
+ url = interpolate(most_appropriate_url, style_name)
153
+
154
+ url = url_timestamp(url) if options[:timestamp]
155
+ url = escape_url(url) if options[:escape]
156
+ url
157
+ end
158
+
159
+ # Returns the path of the attachment as defined by the :path option. If the
160
+ # file is stored in the filesystem the path refers to the path of the file
161
+ # on disk. If the file is stored in S3, the path is the "key" part of the
162
+ # URL, and the :bucket option refers to the S3 bucket.
163
+ def path(style_name = default_style)
164
+ path = original_filename.nil? ? nil : interpolate(@options.path, style_name)
165
+ path.respond_to?(:unescape) ? path.unescape : path
166
+ end
167
+
168
+ # Alias to +url+
169
+ def to_s style_name = default_style
170
+ url(style_name)
171
+ end
172
+
173
+ def default_style
174
+ @options.default_style
175
+ end
176
+
177
+ def styles
178
+ @options.styles
179
+ end
180
+
181
+ # Returns an array containing the errors on this attachment.
182
+ def errors
183
+ @errors
184
+ end
185
+
186
+ # Returns true if there are changes that need to be saved.
187
+ def dirty?
188
+ @dirty
189
+ end
190
+
191
+ # Saves the file, if there are no errors. If there are, it flushes them to
192
+ # the instance's errors and returns false, cancelling the save.
193
+ def save
194
+ flush_deletes
195
+ flush_writes
196
+ @dirty = false
197
+ true
198
+ end
199
+
200
+ # Clears out the attachment. Has the same effect as previously assigning
201
+ # nil to the attachment. Does NOT save. If you wish to clear AND save,
202
+ # use #destroy.
203
+ def clear
204
+ queue_existing_for_delete
205
+ @queued_for_write = {}
206
+ @errors = {}
207
+ end
208
+
209
+ # Destroys the attachment. Has the same effect as previously assigning
210
+ # nil to the attachment *and saving*. This is permanent. If you wish to
211
+ # wipe out the existing attachment but not save, use #clear.
212
+ def destroy
213
+ unless @options.preserve_files
214
+ clear
215
+ save
216
+ end
217
+ end
218
+
219
+ # Returns the uploaded file if present.
220
+ def uploaded_file
221
+ instance_read(:uploaded_file)
222
+ end
223
+
224
+ # Returns the name of the file as originally assigned, and lives in the
225
+ # <attachment>_file_name attribute of the model.
226
+ def original_filename
227
+ instance_read(:file_name)
228
+ end
229
+
230
+ # Returns the size of the file as originally assigned, and lives in the
231
+ # <attachment>_file_size attribute of the model.
232
+ def size
233
+ instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size)
234
+ end
235
+
236
+ # Returns the hash of the file as originally assigned, and lives in the
237
+ # <attachment>_fingerprint attribute of the model.
238
+ def fingerprint
239
+ instance_read(:fingerprint) || (@queued_for_write[:original] && generate_fingerprint(@queued_for_write[:original]))
240
+ end
241
+
242
+ # Returns the content_type of the file as originally assigned, and lives
243
+ # in the <attachment>_content_type attribute of the model.
244
+ def content_type
245
+ instance_read(:content_type)
246
+ end
247
+
248
+ # Returns the last modified time of the file as originally assigned, and
249
+ # lives in the <attachment>_updated_at attribute of the model.
250
+ def updated_at
251
+ time = instance_read(:updated_at)
252
+ time && time.to_f.to_i
253
+ end
254
+
255
+ # The time zone to use for timestamp interpolation. Using the default
256
+ # time zone ensures that results are consistent across all threads.
257
+ def time_zone
258
+ @options.use_default_time_zone ? Time.zone_default : Time.zone
259
+ end
260
+
261
+ # Returns a unique hash suitable for obfuscating the URL of an otherwise
262
+ # publicly viewable attachment.
263
+ def hash(style_name = default_style)
264
+ raise ArgumentError, "Unable to generate hash without :hash_secret" unless @options.hash_secret
265
+ require 'openssl' unless defined?(OpenSSL)
266
+ data = interpolate(@options.hash_data, style_name)
267
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@options.hash_digest).new, @options.hash_secret, data)
268
+ end
269
+
270
+ def generate_fingerprint(source)
271
+ if source.respond_to?(:path) && source.path && !source.path.blank?
272
+ Digest::MD5.file(source.path).to_s
273
+ else
274
+ data = source.read
275
+ source.rewind if source.respond_to?(:rewind)
276
+ Digest::MD5.hexdigest(data)
277
+ end
278
+ end
279
+
280
+ # If <attachment> is an image and <attachment>_width attribute is present, returns the original width
281
+ # of the image when no argument is specified or the calculated new width of the image when passed a
282
+ # valid style. Returns nil otherwise
283
+ def width style = default_style
284
+ dimensions(style)[0]
285
+ end
286
+
287
+ # If <attachment> is an image and <attachment>_height attribute is present, returns the original width
288
+ # of the image when no argument is specified or the calculated new height of the image when passed a
289
+ # valid style. Returns nil otherwise
290
+ def height style = default_style
291
+ dimensions(style)[1]
292
+ end
293
+
294
+ # Paths and URLs can have a number of variables interpolated into them
295
+ # to vary the storage location based on name, id, style, class, etc.
296
+ # This method is a deprecated access into supplying and retrieving these
297
+ # interpolations. Future access should use either Paperclip.interpolates
298
+ # or extend the Paperclip::Interpolations module directly.
299
+ def self.interpolations
300
+ warn('[DEPRECATION] Paperclip::Attachment.interpolations is deprecated ' +
301
+ 'and will be removed from future versions. ' +
302
+ 'Use Paperclip.interpolates instead')
303
+ Paperclip::Interpolations
304
+ end
305
+
306
+ # This method really shouldn't be called that often. It's expected use is
307
+ # in the paperclip:refresh rake task and that's it. It will regenerate all
308
+ # thumbnails forcefully, by reobtaining the original file and going through
309
+ # the post-process again.
310
+ def reprocess!(*style_args)
311
+ new_original = Tempfile.new("paperclip-reprocess")
312
+ new_original.binmode
313
+ if old_original = to_file(:original)
314
+ new_original.write( old_original.respond_to?(:get) ? old_original.get : old_original.read )
315
+ new_original.rewind
316
+
317
+ @queued_for_write = { :original => new_original }
318
+ instance_write(:updated_at, Time.now)
319
+ post_process(*style_args)
320
+
321
+ old_original.close if old_original.respond_to?(:close)
322
+ old_original.unlink if old_original.respond_to?(:unlink)
323
+
324
+ save
325
+ else
326
+ true
327
+ end
328
+ rescue Errno::EACCES => e
329
+ warn "#{e} - skipping file"
330
+ false
331
+ end
332
+
333
+ # Returns true if a file has been assigned.
334
+ def file?
335
+ !original_filename.blank?
336
+ end
337
+
338
+ alias :present? :file?
339
+
340
+ # Determines whether or not the attachment is an image based on the content_type
341
+ def image?
342
+ !content_type.nil? and !!content_type.match(%r{\Aimage/})
343
+ end
344
+
345
+ # Writes the attachment-specific attribute on the instance. For example,
346
+ # instance_write(:file_name, "me.jpg") will write "me.jpg" to the instance's
347
+ # "avatar_file_name" field (assuming the attachment is called avatar).
348
+ def instance_write(attr, value)
349
+ setter = :"#{name}_#{attr}="
350
+ responds = instance.respond_to?(setter)
351
+ self.instance_variable_set("@_#{setter.to_s.chop}", value)
352
+ instance.send(setter, value) if responds || attr.to_s == "file_name"
353
+ end
354
+
355
+ # Reads the attachment-specific attribute on the instance. See instance_write
356
+ # for more details.
357
+ def instance_read(attr)
358
+ getter = :"#{name}_#{attr}"
359
+ responds = instance.respond_to?(getter)
360
+ cached = self.instance_variable_get("@_#{getter}")
361
+ return cached if cached
362
+ instance.send(getter) if responds || attr.to_s == "file_name"
363
+ end
364
+
365
+ private
366
+
367
+ def handle_url_options(options)
368
+ timestamp = extract_timestamp(options)
369
+ options = {} if options == true || options == false
370
+ options[:timestamp] = timestamp
371
+ options[:escape] = true if options[:escape].nil?
372
+ options
373
+ end
374
+
375
+ def extract_timestamp(options)
376
+ possibilities = [((options == true || options == false) ? options : nil),
377
+ (options.respond_to?(:[]) ? options[:timestamp] : nil),
378
+ @options.use_timestamp]
379
+ possibilities.find{|n| !n.nil? }
380
+ end
381
+
382
+ def default_url
383
+ return @options.default_url.call(self) if @options.default_url.is_a?(Proc)
384
+ @options.default_url
385
+ end
386
+
387
+ def most_appropriate_url
388
+ if original_filename.nil?
389
+ default_url
390
+ else
391
+ @options.url
392
+ end
393
+ end
394
+
395
+ def url_timestamp(url)
396
+ return url unless updated_at
397
+ delimiter_char = url.include?("?") ? "&" : "?"
398
+ "#{url}#{delimiter_char}#{updated_at.to_s}"
399
+ end
400
+
401
+ def escape_url(url)
402
+ url.respond_to?(:escape) ? url.escape : URI.escape(url)
403
+ end
404
+
405
+ def ensure_required_accessors! #:nodoc:
406
+ %w(file_name).each do |field|
407
+ unless @instance.respond_to?("#{name}_#{field}") && @instance.respond_to?("#{name}_#{field}=")
408
+ raise PaperclipError.new("#{@instance.class} model missing required attr_accessor for '#{name}_#{field}'")
409
+ end
410
+ end
411
+ end
412
+
413
+ def log message #:nodoc:
414
+ Paperclip.log(message)
415
+ end
416
+
417
+ def valid_assignment? file #:nodoc:
418
+ file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type))
419
+ end
420
+
421
+ def initialize_storage #:nodoc:
422
+ storage_class_name = @options.storage.to_s.downcase.camelize
423
+ begin
424
+ storage_module = Paperclip::Storage.const_get(storage_class_name)
425
+ rescue NameError
426
+ raise StorageMethodNotFound, "Cannot load storage module '#{storage_class_name}'"
427
+ end
428
+ self.extend(storage_module)
429
+ end
430
+
431
+ def extra_options_for(style) #:nodoc:
432
+ all_options = @options.convert_options[:all]
433
+ all_options = all_options.call(instance) if all_options.respond_to?(:call)
434
+ style_options = @options.convert_options[style]
435
+ style_options = style_options.call(instance) if style_options.respond_to?(:call)
436
+
437
+ [ style_options, all_options ].compact.join(" ")
438
+ end
439
+
440
+ def extra_source_file_options_for(style) #:nodoc:
441
+ all_options = @options.source_file_options[:all]
442
+ all_options = all_options.call(instance) if all_options.respond_to?(:call)
443
+ style_options = @options.source_file_options[style]
444
+ style_options = style_options.call(instance) if style_options.respond_to?(:call)
445
+
446
+ [ style_options, all_options ].compact.join(" ")
447
+ end
448
+
449
+ def post_process(*style_args) #:nodoc:
450
+ return if @queued_for_write[:original].nil?
451
+ instance.run_paperclip_callbacks(:post_process) do
452
+ instance.run_paperclip_callbacks(:"#{name}_post_process") do
453
+ post_process_styles(*style_args)
454
+ end
455
+ end
456
+ end
457
+
458
+ def post_process_styles(*style_args) #:nodoc:
459
+ @options.styles.each do |name, style|
460
+ begin
461
+ if style_args.empty? || style_args.include?(name)
462
+ raise RuntimeError.new("Style #{name} has no processors defined.") if style.processors.blank?
463
+ @queued_for_write[name] = style.processors.inject(@queued_for_write[:original]) do |file, processor|
464
+ Paperclip.processor(processor).make(file, style.processor_options, self)
465
+ end
466
+ end
467
+ rescue PaperclipError => e
468
+ log("An error was received while processing: #{e.inspect}")
469
+ (@errors[:processing] ||= []) << e.message if @options.whiny
470
+ end
471
+ end
472
+ end
473
+
474
+ def interpolate(pattern, style_name = default_style) #:nodoc:
475
+ interpolator.interpolate(pattern, self, style_name)
476
+ end
477
+
478
+ def dimensions style = default_style
479
+ return [nil,nil] unless image?
480
+ return @dimensions[style] unless @dimensions[style].nil?
481
+
482
+ w, h = instance_read(:width), instance_read(:height)
483
+
484
+ @dimensions[style] = if (styles[style].nil? || styles[style][:geometry].nil?)
485
+ [w,h]
486
+ else
487
+ Geometry.parse(styles[style][:geometry]).new_dimensions_for(w, h)
488
+ end
489
+ end
490
+
491
+ def queue_existing_for_delete #:nodoc:
492
+ return if @options.preserve_files || !file?
493
+ @queued_for_delete += [:original, *@options.styles.keys].uniq.map do |style|
494
+ path(style) if exists?(style)
495
+ end.compact
496
+ instance_write(:file_name, nil)
497
+ instance_write(:content_type, nil)
498
+ instance_write(:file_size, nil)
499
+ instance_write(:updated_at, nil)
500
+ instance_write(:width, nil)
501
+ instance_write(:height, nil)
502
+ @dimensions = {}
503
+ end
504
+
505
+ def flush_errors #:nodoc:
506
+ @errors.each do |error, message|
507
+ [message].flatten.each {|m| instance.errors.add(name, m) }
508
+ end
509
+ end
510
+
511
+ # called by storage after the writes are flushed and before @queued_for_writes is cleared
512
+ def after_flush_writes
513
+ @queued_for_write.each do |style, file|
514
+ file.close unless file.closed?
515
+ file.unlink if file.respond_to?(:unlink) && file.path.present? && File.exist?(file.path)
516
+ end
517
+ end
518
+
519
+ end
520
+ end