dm-paperclip 2.4.1 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/Gemfile +29 -0
  2. data/Gemfile.lock +100 -0
  3. data/README.md +145 -0
  4. data/Rakefile +37 -71
  5. data/VERSION +1 -0
  6. data/dm-paperclip.gemspec +103 -0
  7. data/lib/dm-paperclip.rb +88 -74
  8. data/lib/dm-paperclip/attachment.rb +139 -102
  9. data/lib/dm-paperclip/callbacks.rb +55 -0
  10. data/lib/dm-paperclip/command_line.rb +86 -0
  11. data/lib/dm-paperclip/ext/blank.rb +24 -0
  12. data/lib/dm-paperclip/ext/class.rb +50 -0
  13. data/lib/dm-paperclip/ext/compatibility.rb +11 -0
  14. data/lib/dm-paperclip/ext/try_dup.rb +12 -0
  15. data/lib/dm-paperclip/geometry.rb +3 -5
  16. data/lib/dm-paperclip/interpolations.rb +57 -32
  17. data/lib/dm-paperclip/iostream.rb +12 -26
  18. data/lib/dm-paperclip/processor.rb +14 -4
  19. data/lib/dm-paperclip/storage.rb +2 -257
  20. data/lib/dm-paperclip/storage/filesystem.rb +73 -0
  21. data/lib/dm-paperclip/storage/s3.rb +209 -0
  22. data/lib/dm-paperclip/storage/s3/aws_library.rb +41 -0
  23. data/lib/dm-paperclip/storage/s3/aws_s3_library.rb +60 -0
  24. data/lib/dm-paperclip/style.rb +90 -0
  25. data/lib/dm-paperclip/thumbnail.rb +33 -24
  26. data/lib/dm-paperclip/upfile.rb +13 -5
  27. data/lib/dm-paperclip/validations.rb +40 -37
  28. data/lib/dm-paperclip/version.rb +4 -0
  29. data/test/attachment_test.rb +510 -67
  30. data/test/command_line_test.rb +138 -0
  31. data/test/fixtures/s3.yml +8 -0
  32. data/test/fixtures/twopage.pdf +0 -0
  33. data/test/fixtures/uppercase.PNG +0 -0
  34. data/test/geometry_test.rb +54 -19
  35. data/test/helper.rb +91 -28
  36. data/test/integration_test.rb +252 -79
  37. data/test/interpolations_test.rb +150 -0
  38. data/test/iostream_test.rb +8 -15
  39. data/test/paperclip_test.rb +222 -69
  40. data/test/processor_test.rb +10 -0
  41. data/test/storage_test.rb +102 -23
  42. data/test/style_test.rb +141 -0
  43. data/test/thumbnail_test.rb +106 -18
  44. data/test/upfile_test.rb +36 -0
  45. metadata +136 -121
  46. data/README.rdoc +0 -116
  47. data/init.rb +0 -1
  48. data/lib/dm-paperclip/callback_compatability.rb +0 -33
@@ -26,26 +26,33 @@
26
26
  # See the +has_attached_file+ documentation for more details.
27
27
 
28
28
  require 'erb'
29
+ require 'digest'
29
30
  require 'tempfile'
30
31
 
31
- require 'extlib'
32
32
  require 'dm-core'
33
+ require 'extlib'
33
34
 
35
+ require 'dm-paperclip/ext/compatibility'
36
+ require 'dm-paperclip/ext/class'
37
+ require 'dm-paperclip/ext/blank'
38
+ require 'dm-paperclip/ext/try_dup'
39
+ require 'dm-paperclip/version'
34
40
  require 'dm-paperclip/upfile'
35
41
  require 'dm-paperclip/iostream'
36
42
  require 'dm-paperclip/geometry'
37
43
  require 'dm-paperclip/processor'
38
44
  require 'dm-paperclip/thumbnail'
39
- require 'dm-paperclip/storage'
40
45
  require 'dm-paperclip/interpolations'
46
+ require 'dm-paperclip/style'
41
47
  require 'dm-paperclip/attachment'
48
+ require 'dm-paperclip/storage'
49
+ require 'dm-paperclip/command_line'
50
+ require 'dm-paperclip/callbacks'
42
51
 
43
52
  # The base module that gets included in ActiveRecord::Base. See the
44
53
  # documentation for Paperclip::ClassMethods for more useful information.
45
54
  module Paperclip
46
55
 
47
- VERSION = "2.4.1"
48
-
49
56
  # To configure Paperclip, put this code in an initializer, Rake task, or wherever:
50
57
  #
51
58
  # Paperclip.configure do |config|
@@ -104,12 +111,12 @@ module Paperclip
104
111
  class << self
105
112
 
106
113
  # Provides configurability to Paperclip. There are a number of options available, such as:
107
- # * whiny: Will raise an error if Paperclip cannot process thumbnails of
114
+ # * whiny: Will raise an error if Paperclip cannot process thumbnails of
108
115
  # an uploaded image. Defaults to true.
109
116
  # * log: Logs progress to the Rails log. Uses ActiveRecord's logger, so honors
110
117
  # log levels, etc. Defaults to true.
111
118
  # * command_path: Defines the path at which to find the command line
112
- # programs if they are not visible to Rails the system's search path. Defaults to
119
+ # programs if they are not visible to Rails the system's search path. Defaults to
113
120
  # nil, which uses the first executable found in the user's search path.
114
121
  # * image_magick_path: Deprecated alias of command_path.
115
122
  def options
@@ -118,7 +125,7 @@ module Paperclip
118
125
  :image_magick_path => nil,
119
126
  :command_path => nil,
120
127
  :log => true,
121
- :log_command => false,
128
+ :log_command => true,
122
129
  :swallow_stderr => true
123
130
  }
124
131
  end
@@ -135,7 +142,7 @@ module Paperclip
135
142
  Paperclip::Interpolations[key] = block
136
143
  end
137
144
 
138
- # The run method takes a command to execute and a string of parameters
145
+ # The run method takes a command to execute and an array of parameters
139
146
  # that get passed to it. The command is prefixed with the :command_path
140
147
  # option from Paperclip.options. If you have many commands to run and
141
148
  # they are in different paths, the suggested course of action is to
@@ -144,23 +151,19 @@ module Paperclip
144
151
  # If the command returns with a result code that is not one of the
145
152
  # expected_outcodes, a PaperclipCommandLineError will be raised. Generally
146
153
  # a code of 0 is expected, but a list of codes may be passed if necessary.
154
+ # These codes should be passed as a hash as the last argument, like so:
155
+ #
156
+ # Paperclip.run("echo", "something", :expected_outcodes => [0,1,2,3])
147
157
  #
148
- # This method can log the command being run when
158
+ # This method can log the command being run when
149
159
  # Paperclip.options[:log_command] is set to true (defaults to false). This
150
160
  # will only log if logging in general is set to true as well.
151
- def run cmd, params = "", expected_outcodes = 0
152
- command = %Q<#{%Q[#{path_for_command(cmd)} #{params}].gsub(/\s+/, " ")}>
153
- command = "#{command} 2>#{bit_bucket}" if Paperclip.options[:swallow_stderr]
154
- Paperclip.log(command) if Paperclip.options[:log_command]
155
- output = `#{command}`
156
- unless [expected_outcodes].flatten.include?($?.exitstatus)
157
- raise PaperclipCommandLineError, "Error while running #{cmd}"
161
+ def run cmd, *params
162
+ if options[:image_magick_path]
163
+ Paperclip.log("[DEPRECATION] :image_magick_path is deprecated and will be removed. Use :command_path instead")
158
164
  end
159
- output
160
- end
161
-
162
- def bit_bucket #:nodoc:
163
- File.exists?("/dev/null") ? "/dev/null" : "NUL"
165
+ CommandLine.path = options[:command_path] || options[:image_magick_path]
166
+ CommandLine.new(cmd, *params).run
164
167
  end
165
168
 
166
169
  def included base #:nodoc:
@@ -171,7 +174,7 @@ module Paperclip
171
174
  end
172
175
 
173
176
  def processor name #:nodoc:
174
- name = name.to_s.camel_case
177
+ name = DataMapper::Inflector.classify(name.to_s)
175
178
  processor = Paperclip.const_get(name)
176
179
  unless processor.ancestors.include?(Paperclip::Processor)
177
180
  raise PaperclipError.new("[paperclip] Processor #{name} was not found")
@@ -179,6 +182,12 @@ module Paperclip
179
182
  processor
180
183
  end
181
184
 
185
+ def each_instance_with_attachment(klass, name)
186
+ Object.const_get(klass).all.each do |instance|
187
+ yield(instance) if instance.send(:"#{name}?")
188
+ end
189
+ end
190
+
182
191
  # Log a paperclip-specific line. Uses ActiveRecord::Base.logger
183
192
  # by default. Set Paperclip.options[:log] to false to turn off.
184
193
  def log message
@@ -197,31 +206,30 @@ module Paperclip
197
206
  class PaperclipError < StandardError #:nodoc:
198
207
  end
199
208
 
200
- class PaperclipCommandLineError < StandardError #:nodoc:
209
+ class PaperclipCommandLineError < PaperclipError #:nodoc:
210
+ attr_accessor :output
211
+ def initialize(msg = nil, output = nil)
212
+ super(msg)
213
+ @output = output
214
+ end
215
+ end
216
+
217
+ class StorageMethodNotFound < PaperclipError
218
+ end
219
+
220
+ class CommandNotFoundError < PaperclipError
201
221
  end
202
222
 
203
223
  class NotIdentifiedByImageMagickError < PaperclipError #:nodoc:
204
224
  end
205
-
225
+
206
226
  class InfiniteInterpolationError < PaperclipError #:nodoc:
207
227
  end
208
-
209
- module Resource
210
228
 
229
+ module Resource
211
230
  def self.included(base)
212
-
213
- base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
214
- class_variable_set(:@@attachment_definitions,nil) unless class_variable_defined?(:@@attachment_definitions)
215
- def self.attachment_definitions
216
- @@attachment_definitions
217
- end
218
-
219
- def self.attachment_definitions=(obj)
220
- @@attachment_definitions = obj
221
- end
222
- RUBY
223
-
224
231
  base.extend Paperclip::ClassMethods
232
+ base.extend Paperclip::Ext::Class::Hook
225
233
 
226
234
  # Done at this time to ensure that the user
227
235
  # had a chance to configure the app in an initializer
@@ -232,50 +240,50 @@ module Paperclip
232
240
  end
233
241
 
234
242
  Paperclip.require_processors
235
-
236
243
  end
237
-
238
244
  end
239
245
 
240
246
  module ClassMethods
241
247
  # +has_attached_file+ gives the class it is called on an attribute that maps to a file. This
242
- # is typically a file stored somewhere on the filesystem and has been uploaded by a user.
248
+ # is typically a file stored somewhere on the filesystem and has been uploaded by a user.
243
249
  # The attribute returns a Paperclip::Attachment object which handles the management of
244
- # that file. The intent is to make the attachment as much like a normal attribute. The
245
- # thumbnails will be created when the new file is assigned, but they will *not* be saved
246
- # until +save+ is called on the record. Likewise, if the attribute is set to +nil+ is
247
- # called on it, the attachment will *not* be deleted until +save+ is called. See the
248
- # Paperclip::Attachment documentation for more specifics. There are a number of options
250
+ # that file. The intent is to make the attachment as much like a normal attribute. The
251
+ # thumbnails will be created when the new file is assigned, but they will *not* be saved
252
+ # until +save+ is called on the record. Likewise, if the attribute is set to +nil+ is
253
+ # called on it, the attachment will *not* be deleted until +save+ is called. See the
254
+ # Paperclip::Attachment documentation for more specifics. There are a number of options
249
255
  # you can set to change the behavior of a Paperclip attachment:
250
256
  # * +url+: The full URL of where the attachment is publically accessible. This can just
251
257
  # as easily point to a directory served directly through Apache as it can to an action
252
258
  # that can control permissions. You can specify the full domain and path, but usually
253
- # just an absolute path is sufficient. The leading slash must be included manually for
254
- # absolute paths. The default value is "/:class/:attachment/:id/:style_:filename". See
259
+ # just an absolute path is sufficient. The leading slash *must* be included manually for
260
+ # absolute paths. The default value is
261
+ # "/system/:attachment/:id/:style/:filename". See
255
262
  # Paperclip::Attachment#interpolate for more information on variable interpolaton.
256
- # :url => "/:attachment/:id/:style_:basename:extension"
263
+ # :url => "/:class/:attachment/:id/:style_:filename"
257
264
  # :url => "http://some.other.host/stuff/:class/:id_:extension"
258
- # * +default_url+: The URL that will be returned if there is no attachment assigned.
259
- # This field is interpolated just as the url is. The default value is
260
- # "/:class/:attachment/missing_:style.png"
265
+ # * +default_url+: The URL that will be returned if there is no attachment assigned.
266
+ # This field is interpolated just as the url is. The default value is
267
+ # "/:attachment/:style/missing.png"
261
268
  # has_attached_file :avatar, :default_url => "/images/default_:style_avatar.png"
262
269
  # User.new.avatar_url(:small) # => "/images/default_small_avatar.png"
263
- # * +styles+: A hash of thumbnail styles and their geometries. You can find more about
264
- # geometry strings at the ImageMagick website
270
+ # * +styles+: A hash of thumbnail styles and their geometries. You can find more about
271
+ # geometry strings at the ImageMagick website
265
272
  # (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
266
- # also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally
267
- # inside the dimensions and then crop the rest off (weighted at the center). The
273
+ # also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally
274
+ # inside the dimensions and then crop the rest off (weighted at the center). The
268
275
  # default value is to generate no thumbnails.
269
- # * +default_style+: The thumbnail style that will be used by default URLs.
276
+ # * +default_style+: The thumbnail style that will be used by default URLs.
270
277
  # Defaults to +original+.
271
278
  # has_attached_file :avatar, :styles => { :normal => "100x100#" },
272
279
  # :default_style => :normal
273
280
  # user.avatar.url # => "/avatars/23/normal_me.png"
274
- # * +whiny_thumbnails+: Will raise an error if Paperclip cannot process thumbnails of an
275
- # uploaded image. This will ovrride the global setting for this attachment.
276
- # Defaults to true.
281
+ # * +whiny+: Will raise an error if Paperclip cannot post_process an uploaded file due
282
+ # to a command line error. This will override the global setting for this attachment.
283
+ # Defaults to true. This option used to be called :whiny_thumbanils, but this is
284
+ # deprecated.
277
285
  # * +convert_options+: When creating thumbnails, use this free-form options
278
- # field to pass in various convert command options. Typical options are "-strip" to
286
+ # array to pass in various convert command options. Typical options are "-strip" to
279
287
  # remove all Exif data from the image (save space for thumbnails and avatars) or
280
288
  # "-depth 8" to specify the bit depth of the resulting conversion. See ImageMagick
281
289
  # convert documentation for more options: (http://www.imagemagick.org/script/convert.php)
@@ -283,12 +291,18 @@ module Paperclip
283
291
  # of thumbnail being generated. You can also specify :all as a key, which will apply
284
292
  # to all of the thumbnails being generated. If you specify options for the :original,
285
293
  # it would be best if you did not specify destructive options, as the intent of keeping
286
- # the original around is to regenerate all the thumbnails then requirements change.
294
+ # the original around is to regenerate all the thumbnails when requirements change.
287
295
  # has_attached_file :avatar, :styles => { :large => "300x300", :negative => "100x100" }
288
296
  # :convert_options => {
289
297
  # :all => "-strip",
290
298
  # :negative => "-negate"
291
299
  # }
300
+ # NOTE: While not deprecated yet, it is not recommended to specify options this way.
301
+ # It is recommended that :convert_options option be included in the hash passed to each
302
+ # :styles for compatability with future versions.
303
+ # NOTE: Strings supplied to :convert_options are split on space in order to undergo
304
+ # shell quoting for safety. If your options require a space, please pre-split them
305
+ # and pass an array to :convert_options instead.
292
306
  # * +storage+: Chooses the storage backend where the files will be stored. The current
293
307
  # choices are :filesystem and :s3. The default is :filesystem. Make sure you read the
294
308
  # documentation for Paperclip::Storage::Filesystem and Paperclip::Storage::S3
@@ -296,10 +310,11 @@ module Paperclip
296
310
  def has_attached_file name, options = {}
297
311
  include InstanceMethods
298
312
 
299
- self.attachment_definitions = {} if self.attachment_definitions.nil?
300
- self.attachment_definitions[name] = {:validations => []}.merge(options)
301
-
313
+ Paperclip::Ext::Class.write_inheritable_attribute(self, :attachment_definitions, {}) if attachment_definitions.nil?
314
+ attachment_definitions[name] = {:validations => []}.merge(options)
315
+
302
316
  property_options = options.delete_if { |k,v| ![ :public, :protected, :private, :accessor, :reader, :writer ].include?(key) }
317
+ property_options[:required] = false
303
318
 
304
319
  property :"#{name}_file_name", String, property_options.merge(:length => 255)
305
320
  property :"#{name}_content_type", String, property_options.merge(:length => 255)
@@ -308,10 +323,9 @@ module Paperclip
308
323
 
309
324
  after :save, :save_attached_files
310
325
  before :destroy, :destroy_attached_files
311
-
312
- # not needed with extlib just do before :post_process, or after :post_process
313
- # define_callbacks :before_post_process, :after_post_process
314
- # define_callbacks :"before_#{name}_post_process", :"after_#{name}_post_process"
326
+
327
+ Paperclip::Callbacks.define(self, "post_process")
328
+ Paperclip::Callbacks.define(self, "#{name}_post_process")
315
329
 
316
330
  define_method name do |*args|
317
331
  a = attachment_for(name)
@@ -323,11 +337,11 @@ module Paperclip
323
337
  end
324
338
 
325
339
  define_method "#{name}?" do
326
- ! attachment_for(name).original_filename.blank?
340
+ attachment_for(name).file?
327
341
  end
328
342
 
329
343
  if Paperclip.config.use_dm_validations
330
- add_validator_to_context(opts_from_validator_args([name]), [name], Paperclip::Validate::CopyAttachmentErrors)
344
+ validators.add(Paperclip::Validate::CopyAttachmentErrors, name)
331
345
  end
332
346
 
333
347
  end
@@ -335,14 +349,14 @@ module Paperclip
335
349
  # Returns the attachment definitions defined by each call to
336
350
  # has_attached_file.
337
351
  def attachment_definitions
338
- read_inheritable_attribute(:attachment_definitions)
352
+ Paperclip::Ext::Class.read_inheritable_attribute(self, :attachment_definitions)
339
353
  end
340
354
  end
341
355
 
342
356
  module InstanceMethods #:nodoc:
343
357
  def attachment_for name
344
- @attachments ||= {}
345
- @attachments[name] ||= Attachment.new(name, self, self.class.attachment_definitions[name])
358
+ @_paperclip_attachments ||= {}
359
+ @_paperclip_attachments[name] ||= Attachment.new(name, self, self.class.attachment_definitions[name])
346
360
  end
347
361
 
348
362
  def each_attachment
@@ -1,23 +1,31 @@
1
+ # encoding: utf-8
1
2
  module Paperclip
2
3
  # The Attachment class manages the files for a given attachment. It saves
3
4
  # when the model saves, deletes when the model is destroyed, and processes
4
5
  # the file upon assignment.
5
6
  class Attachment
6
-
7
+ include IOStream
7
8
  def self.default_options
8
9
  @default_options ||= {
9
- :url => "/system/:attachment/:id/:style/:filename",
10
- :path => ":rails_root/public:url",
11
- :styles => {},
12
- :default_url => "/:attachment/:style/missing.png",
13
- :default_style => :original,
14
- :validations => [],
15
- :storage => :filesystem,
16
- :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails]
10
+ :url => "/system/:attachment/:id/:style/:filename",
11
+ :path => ":web_root/public:url",
12
+ :styles => {},
13
+ :processors => [:thumbnail],
14
+ :convert_options => {},
15
+ :default_url => "/:attachment/:style/missing.png",
16
+ :default_style => :original,
17
+ :validations => [],
18
+ :storage => :filesystem,
19
+ :use_timestamp => true,
20
+ :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails],
21
+ :use_default_time_zone => true,
22
+ :hash_digest => "SHA1",
23
+ :hash_data => ":class/:attachment/:id/:style/:updated_at"
17
24
  }
18
25
  end
19
26
 
20
- attr_reader :name, :instance, :styles, :default_style, :convert_options, :queued_for_write, :options
27
+ attr_reader :name, :instance, :default_style, :convert_options, :queued_for_write, :whiny, :options
28
+ attr_accessor :post_processing
21
29
 
22
30
  # Creates an Attachment object. +name+ is the name of the attachment,
23
31
  # +instance+ is the ActiveRecord object instance it's attached to, and
@@ -28,38 +36,55 @@ module Paperclip
28
36
 
29
37
  options = self.class.default_options.merge(options)
30
38
 
31
- @url = options[:url]
32
- @url = @url.call(self) if @url.is_a?(Proc)
33
- @path = options[:path]
34
- @path = @path.call(self) if @path.is_a?(Proc)
35
- @styles = options[:styles]
36
- @styles = @styles.call(self) if @styles.is_a?(Proc)
37
- @default_url = options[:default_url]
38
- @validations = options[:validations]
39
- @default_style = options[:default_style]
40
- @storage = options[:storage]
41
- @whiny = options[:whiny_thumbnails] || options[:whiny]
42
- @convert_options = options[:convert_options] || {}
43
- @processors = options[:processors] || [:thumbnail]
44
- @options = options
45
- @queued_for_delete = []
46
- @queued_for_write = {}
47
- @errors = {}
48
- @validation_errors = nil
49
- @dirty = false
39
+ @url = options[:url]
40
+ @url = @url.call(self) if @url.is_a?(Proc)
41
+ @path = options[:path]
42
+ @path = @path.call(self) if @path.is_a?(Proc)
43
+ @styles = options[:styles]
44
+ @normalized_styles = nil
45
+ @default_url = options[:default_url]
46
+ @validations = options[:validations]
47
+ @default_style = options[:default_style]
48
+ @storage = options[:storage]
49
+ @use_timestamp = options[:use_timestamp]
50
+ @whiny = options[:whiny_thumbnails] || options[:whiny]
51
+ @use_default_time_zone = options[:use_default_time_zone]
52
+ @hash_digest = options[:hash_digest]
53
+ @hash_data = options[:hash_data]
54
+ @hash_secret = options[:hash_secret]
55
+ @convert_options = options[:convert_options]
56
+ @processors = options[:processors]
57
+ @options = options
58
+ @post_processing = true
59
+ @queued_for_delete = []
60
+ @queued_for_write = {}
61
+ @errors = {}
62
+ @validation_errors = nil
63
+ @dirty = false
50
64
 
51
- normalize_style_definition
52
65
  initialize_storage
53
66
  end
54
67
 
68
+ def styles
69
+ unless @normalized_styles
70
+ @normalized_styles = {}
71
+ (@styles.respond_to?(:call) ? @styles.call(self) : @styles).each do |name, args|
72
+ @normalized_styles[name] = Paperclip::Style.new(name, args.dup, self)
73
+ end
74
+ end
75
+ @normalized_styles
76
+ end
77
+
78
+ def processors
79
+ @processors.respond_to?(:call) ? @processors.call(instance) : @processors
80
+ end
81
+
55
82
  # What gets called when you call instance.attachment = File. It clears
56
- # errors, assigns attributes, processes the file, and runs validations. It
83
+ # errors, assigns attributes, and processes the file. It
57
84
  # also queues up the previous file for deletion, to be flushed away on
58
85
  # #save of its host. In addition to form uploads, you can also assign
59
86
  # another Paperclip attachment:
60
87
  # new_user.avatar = old_user.avatar
61
- # If the file that is assigned is not valid, the processing (i.e.
62
- # thumbnailing, etc) will NOT be run.
63
88
  def assign uploaded_file
64
89
 
65
90
  ensure_required_accessors!
@@ -77,28 +102,29 @@ module Paperclip
77
102
  return nil if uploaded_file.nil?
78
103
 
79
104
  if uploaded_file.respond_to?(:[])
80
- uploaded_file = uploaded_file.to_mash
81
-
105
+ uploaded_file = DataMapper::Mash.new(uploaded_file)
106
+
82
107
  @queued_for_write[:original] = uploaded_file['tempfile']
83
108
  instance_write(:file_name, uploaded_file['filename'].strip.gsub(/[^\w\d\.\-]+/, '_'))
84
- instance_write(:content_type, uploaded_file['content_type'] ? uploaded_file['content_type'].strip : uploaded_file['tempfile'].content_type.to_s.strip)
109
+ instance_write(:content_type, ( uploaded_file['content_type'] && uploaded_file['content_type'].strip || # sometimes it is 'type' instead of 'content_type'
110
+ uploaded_file['type'] && uploaded_file['type'].strip ||
111
+ uploaded_file['tempfile'].content_type.to_s.strip))
85
112
  instance_write(:file_size, uploaded_file['size'] ? uploaded_file['size'].to_i : uploaded_file['tempfile'].size.to_i)
86
- instance_write(:updated_at, Time.now)
87
113
  else
88
- @queued_for_write[:original] = uploaded_file.to_tempfile
89
- instance_write(:file_name, uploaded_file.original_filename.strip.gsub(/[^\w\d\.\-]+/, '_'))
114
+ @queued_for_write[:original] = to_tempfile(uploaded_file)
115
+ instance_write(:file_name, uploaded_file.original_filename.strip)
90
116
  instance_write(:content_type, uploaded_file.content_type.to_s.strip)
91
117
  instance_write(:file_size, uploaded_file.size.to_i)
92
- instance_write(:updated_at, Time.now)
93
118
  end
94
119
 
95
120
  @dirty = true
96
121
 
97
- post_process if valid?
122
+ post_process if @post_processing && valid?
98
123
 
99
124
  # Reset the file size if the original file was reprocessed.
100
- instance_write(:file_size, @queued_for_write[:original].size.to_i)
101
-
125
+ instance_write(:file_size, @queued_for_write[:original].size.to_i)
126
+ instance_write(:fingerprint, generate_fingerprint(@queued_for_write[:original]))
127
+ instance_write(:updated_at, DateTime.now)
102
128
  ensure
103
129
  uploaded_file.close if close_uploaded_file
104
130
  validate
@@ -108,25 +134,24 @@ module Paperclip
108
134
  # this does not necessarily need to point to a file that your web server
109
135
  # can access and can point to an action in your app, if you need fine
110
136
  # grained security. This is not recommended if you don't need the
111
- # security, however, for performance reasons. set
112
- # include_updated_timestamp to false if you want to stop the attachment
113
- # update time appended to the url
114
- def url style = default_style, include_updated_timestamp = true
115
- the_url = original_filename.nil? ? interpolate(@default_url, style) : interpolate(@url, style)
116
- include_updated_timestamp && updated_at ? [the_url, updated_at.to_time.to_i].compact.join(the_url.include?("?") ? "&" : "?") : the_url
137
+ # security, however, for performance reasons. Set use_timestamp to false
138
+ # if you want to stop the attachment update time appended to the url
139
+ def url(style_name = default_style, use_timestamp = @use_timestamp)
140
+ url = original_filename.nil? ? interpolate(@default_url, style_name) : interpolate(@url, style_name)
141
+ use_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url
117
142
  end
118
143
 
119
144
  # Returns the path of the attachment as defined by the :path option. If the
120
145
  # file is stored in the filesystem the path refers to the path of the file
121
146
  # on disk. If the file is stored in S3, the path is the "key" part of the
122
147
  # URL, and the :bucket option refers to the S3 bucket.
123
- def path style = default_style
124
- original_filename.nil? ? nil : interpolate(@path, style)
148
+ def path style_name = default_style
149
+ original_filename.nil? ? nil : interpolate(@path, style_name)
125
150
  end
126
151
 
127
152
  # Alias to +url+
128
- def to_s style = nil
129
- url(style)
153
+ def to_s style_name = default_style
154
+ url(style_name)
130
155
  end
131
156
 
132
157
  # Returns true if there are no errors on this attachment.
@@ -188,6 +213,12 @@ module Paperclip
188
213
  instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size)
189
214
  end
190
215
 
216
+ # Returns the hash of the file as originally assigned, and lives in the
217
+ # <attachment>_fingerprint attribute of the model.
218
+ def fingerprint
219
+ instance_read(:fingerprint) || (@queued_for_write[:original] && generate_fingerprint(@queued_for_write[:original]))
220
+ end
221
+
191
222
  # Returns the content_type of the file as originally assigned, and lives
192
223
  # in the <attachment>_content_type attribute of the model.
193
224
  def content_type
@@ -197,7 +228,29 @@ module Paperclip
197
228
  # Returns the last modified time of the file as originally assigned, and
198
229
  # lives in the <attachment>_updated_at attribute of the model.
199
230
  def updated_at
200
- instance_read(:updated_at)
231
+ time = instance_read(:updated_at)
232
+ return nil unless time
233
+
234
+ if time.is_a?(DateTime)
235
+ time && ((time - ::DateTime.civil(1970)) * 86_400).to_i
236
+ else
237
+ time.to_i
238
+ end
239
+ end
240
+
241
+ # Returns a unique hash suitable for obfuscating the URL of an otherwise
242
+ # publicly viewable attachment.
243
+ def hash(style_name = default_style)
244
+ raise ArgumentError, "Unable to generate hash without :hash_secret" unless @hash_secret
245
+ require 'openssl' unless defined?(OpenSSL)
246
+ data = interpolate(@hash_data, style_name)
247
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@hash_digest).new, @hash_secret, data)
248
+ end
249
+
250
+ def generate_fingerprint(source)
251
+ data = source.read
252
+ source.rewind if source.respond_to?(:rewind)
253
+ Digest::MD5.hexdigest(data)
201
254
  end
202
255
 
203
256
  # Paths and URLs can have a number of variables interpolated into them
@@ -216,15 +269,15 @@ module Paperclip
216
269
  # in the paperclip:refresh rake task and that's it. It will regenerate all
217
270
  # thumbnails forcefully, by reobtaining the original file and going through
218
271
  # the post-process again.
219
- def reprocess!
272
+ def reprocess!(*style_args)
220
273
  new_original = Tempfile.new("paperclip-reprocess")
221
274
  new_original.binmode
222
275
  if old_original = to_file(:original)
223
- new_original.write( old_original.read )
276
+ new_original.write( old_original.respond_to?(:get) ? old_original.get : old_original.read )
224
277
  new_original.rewind
225
278
 
226
279
  @queued_for_write = { :original => new_original }
227
- post_process
280
+ post_process(*style_args)
228
281
 
229
282
  old_original.close if old_original.respond_to?(:close)
230
283
 
@@ -232,11 +285,14 @@ module Paperclip
232
285
  else
233
286
  true
234
287
  end
288
+ rescue Errno::EACCES => e
289
+ log "Skipping file: #{e.message}"
290
+ false
235
291
  end
236
292
 
237
293
  # Returns true if a file has been assigned.
238
294
  def file?
239
- !original_filename.blank?
295
+ !Paperclip::Ext.blank?(original_filename)
240
296
  end
241
297
 
242
298
  # Writes the attachment-specific attribute on the instance. For example,
@@ -301,7 +357,7 @@ module Paperclip
301
357
  def check_guard guard #:nodoc:
302
358
  if guard.respond_to? :call
303
359
  guard.call(instance)
304
- elsif ! guard.blank?
360
+ elsif ! Paperclip::Ext.blank?(guard)
305
361
  instance.send(guard.to_s)
306
362
  end
307
363
  end
@@ -318,8 +374,8 @@ module Paperclip
318
374
 
319
375
  def validate_content_type options #:nodoc:
320
376
  valid_types = [options[:content_type]].flatten
321
- unless original_filename.blank?
322
- unless valid_types.blank?
377
+ unless Paperclip::Ext.blank?(original_filename)
378
+ unless Paperclip::Ext.blank?(valid_types)
323
379
  content_type = instance_read(:content_type)
324
380
  unless valid_types.any?{|t| content_type.nil? || t === content_type }
325
381
  options[:message] || "is not one of the allowed file types."
@@ -328,37 +384,13 @@ module Paperclip
328
384
  end
329
385
  end
330
386
 
331
- def normalize_style_definition #:nodoc:
332
- @styles.each do |name, args|
333
- unless args.is_a? Hash
334
- dimensions, format = [args, nil].flatten[0..1]
335
- format = nil if format.blank?
336
- @styles[name] = {
337
- :processors => @processors,
338
- :geometry => dimensions,
339
- :format => format,
340
- :whiny => @whiny,
341
- :convert_options => extra_options_for(name)
342
- }
343
- else
344
- @styles[name] = {
345
- :processors => @processors,
346
- :whiny => @whiny,
347
- :convert_options => extra_options_for(name)
348
- }.merge(@styles[name])
349
- end
350
- end
351
- end
352
-
353
- def solidify_style_definitions #:nodoc:
354
- @styles.each do |name, args|
355
- @styles[name][:geometry] = @styles[name][:geometry].call(instance) if @styles[name][:geometry].respond_to?(:call)
356
- @styles[name][:processors] = @styles[name][:processors].call(instance) if @styles[name][:processors].respond_to?(:call)
357
- end
358
- end
359
-
360
387
  def initialize_storage #:nodoc:
361
- @storage_module = Paperclip::Storage.const_get(@storage.to_s.capitalize)
388
+ storage_class_name = @storage.to_s.capitalize
389
+ begin
390
+ @storage_module = Paperclip::Storage.const_get(storage_class_name)
391
+ rescue NameError
392
+ raise StorageMethodNotFound, "Cannot load storage module '#{storage_class_name}'"
393
+ end
362
394
  self.extend(@storage_module)
363
395
  end
364
396
 
@@ -371,18 +403,23 @@ module Paperclip
371
403
  [ style_options, all_options ].compact.join(" ")
372
404
  end
373
405
 
374
- def post_process #:nodoc:
406
+ def post_process(*style_args) #:nodoc:
375
407
  return if @queued_for_write[:original].nil?
376
- solidify_style_definitions
377
- post_process_styles
408
+ Paperclip::Callbacks.run(instance, 'post_process') do
409
+ Paperclip::Callbacks.run(instance, "#{name}_post_process") do
410
+ post_process_styles(*style_args)
411
+ end
412
+ end
378
413
  end
379
414
 
380
- def post_process_styles #:nodoc:
381
- @styles.each do |name, args|
415
+ def post_process_styles(*style_args) #:nodoc:
416
+ styles.each do |name, style|
382
417
  begin
383
- raise RuntimeError.new("Style #{name} has no processors defined.") if args[:processors].blank?
384
- @queued_for_write[name] = args[:processors].inject(@queued_for_write[:original]) do |file, processor|
385
- Paperclip.processor(processor).make(file, args, self)
418
+ if style_args.empty? || style_args.include?(name)
419
+ raise RuntimeError.new("Style #{name} has no processors defined.") if style.processors.empty?
420
+ @queued_for_write[name] = style.processors.inject(@queued_for_write[:original]) do |file, processor|
421
+ Paperclip.processor(processor).make(file, style.processor_options, self)
422
+ end
386
423
  end
387
424
  rescue PaperclipError => e
388
425
  log("An error was received while processing: #{e.inspect}")
@@ -391,13 +428,13 @@ module Paperclip
391
428
  end
392
429
  end
393
430
 
394
- def interpolate pattern, style = default_style #:nodoc:
395
- Paperclip::Interpolations.interpolate(pattern, self, style)
431
+ def interpolate pattern, style_name = default_style #:nodoc:
432
+ Paperclip::Interpolations.interpolate(pattern, self, style_name)
396
433
  end
397
434
 
398
435
  def queue_existing_for_delete #:nodoc:
399
436
  return unless file?
400
- @queued_for_delete += [:original, *@styles.keys].uniq.map do |style|
437
+ @queued_for_delete += [:original, *styles.keys].uniq.map do |style|
401
438
  path(style) if exists?(style)
402
439
  end.compact
403
440
  instance_write(:file_name, nil)