reenhanced-paperclip 2.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. data/LICENSE +26 -0
  2. data/README.md +217 -0
  3. data/Rakefile +80 -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 +1 -0
  8. data/lib/generators/paperclip/USAGE +8 -0
  9. data/lib/generators/paperclip/paperclip_generator.rb +31 -0
  10. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  11. data/lib/paperclip.rb +376 -0
  12. data/lib/paperclip/attachment.rb +350 -0
  13. data/lib/paperclip/callback_compatability.rb +61 -0
  14. data/lib/paperclip/command_line.rb +80 -0
  15. data/lib/paperclip/geometry.rb +115 -0
  16. data/lib/paperclip/interpolations.rb +114 -0
  17. data/lib/paperclip/iostream.rb +45 -0
  18. data/lib/paperclip/matchers.rb +33 -0
  19. data/lib/paperclip/matchers/have_attached_file_matcher.rb +57 -0
  20. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +75 -0
  21. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +54 -0
  22. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +95 -0
  23. data/lib/paperclip/processor.rb +58 -0
  24. data/lib/paperclip/railtie.rb +24 -0
  25. data/lib/paperclip/storage.rb +2 -0
  26. data/lib/paperclip/storage/filesystem.rb +73 -0
  27. data/lib/paperclip/storage/s3.rb +192 -0
  28. data/lib/paperclip/style.rb +90 -0
  29. data/lib/paperclip/thumbnail.rb +79 -0
  30. data/lib/paperclip/upfile.rb +55 -0
  31. data/lib/paperclip/version.rb +3 -0
  32. data/lib/tasks/paperclip.rake +72 -0
  33. data/rails/init.rb +2 -0
  34. data/shoulda_macros/paperclip.rb +118 -0
  35. data/test/attachment_test.rb +844 -0
  36. data/test/command_line_test.rb +133 -0
  37. data/test/database.yml +4 -0
  38. data/test/fixtures/12k.png +0 -0
  39. data/test/fixtures/50x50.png +0 -0
  40. data/test/fixtures/5k.png +0 -0
  41. data/test/fixtures/bad.png +1 -0
  42. data/test/fixtures/s3.yml +8 -0
  43. data/test/fixtures/text.txt +0 -0
  44. data/test/fixtures/twopage.pdf +0 -0
  45. data/test/fixtures/uppercase.PNG +0 -0
  46. data/test/geometry_test.rb +177 -0
  47. data/test/helper.rb +146 -0
  48. data/test/integration_test.rb +498 -0
  49. data/test/interpolations_test.rb +127 -0
  50. data/test/iostream_test.rb +71 -0
  51. data/test/matchers/have_attached_file_matcher_test.rb +24 -0
  52. data/test/matchers/validate_attachment_content_type_matcher_test.rb +47 -0
  53. data/test/matchers/validate_attachment_presence_matcher_test.rb +26 -0
  54. data/test/matchers/validate_attachment_size_matcher_test.rb +51 -0
  55. data/test/paperclip_test.rb +271 -0
  56. data/test/processor_test.rb +10 -0
  57. data/test/storage_test.rb +386 -0
  58. data/test/style_test.rb +141 -0
  59. data/test/thumbnail_test.rb +227 -0
  60. data/test/upfile_test.rb +36 -0
  61. metadata +224 -0
data/lib/paperclip.rb ADDED
@@ -0,0 +1,376 @@
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-2009 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
+
28
+ require 'erb'
29
+ require 'digest'
30
+ require 'tempfile'
31
+ require 'paperclip/version'
32
+ require 'paperclip/upfile'
33
+ require 'paperclip/iostream'
34
+ require 'paperclip/geometry'
35
+ require 'paperclip/processor'
36
+ require 'paperclip/thumbnail'
37
+ require 'paperclip/interpolations'
38
+ require 'paperclip/style'
39
+ require 'paperclip/attachment'
40
+ require 'paperclip/storage'
41
+ require 'paperclip/callback_compatability'
42
+ require 'paperclip/command_line'
43
+ require 'paperclip/railtie'
44
+ if defined?(Rails.root) && Rails.root
45
+ Dir.glob(File.join(File.expand_path(Rails.root), "lib", "paperclip_processors", "*.rb")).each do |processor|
46
+ require processor
47
+ end
48
+ end
49
+
50
+ # The base module that gets included in ActiveRecord::Base. See the
51
+ # documentation for Paperclip::ClassMethods for more useful information.
52
+ module Paperclip
53
+
54
+ class << self
55
+ # Provides configurability to Paperclip. There are a number of options available, such as:
56
+ # * whiny: Will raise an error if Paperclip cannot process thumbnails of
57
+ # an uploaded image. Defaults to true.
58
+ # * log: Logs progress to the Rails log. Uses ActiveRecord's logger, so honors
59
+ # log levels, etc. Defaults to true.
60
+ # * command_path: Defines the path at which to find the command line
61
+ # programs if they are not visible to Rails the system's search path. Defaults to
62
+ # nil, which uses the first executable found in the user's search path.
63
+ # * image_magick_path: Deprecated alias of command_path.
64
+ def options
65
+ @options ||= {
66
+ :whiny => true,
67
+ :image_magick_path => nil,
68
+ :command_path => nil,
69
+ :log => true,
70
+ :log_command => true,
71
+ :swallow_stderr => true
72
+ }
73
+ end
74
+
75
+ def configure
76
+ yield(self) if block_given?
77
+ end
78
+
79
+ def interpolates key, &block
80
+ Paperclip::Interpolations[key] = block
81
+ end
82
+
83
+ # The run method takes a command to execute and an array of parameters
84
+ # that get passed to it. The command is prefixed with the :command_path
85
+ # option from Paperclip.options. If you have many commands to run and
86
+ # they are in different paths, the suggested course of action is to
87
+ # symlink them so they are all in the same directory.
88
+ #
89
+ # If the command returns with a result code that is not one of the
90
+ # expected_outcodes, a PaperclipCommandLineError will be raised. Generally
91
+ # a code of 0 is expected, but a list of codes may be passed if necessary.
92
+ # These codes should be passed as a hash as the last argument, like so:
93
+ #
94
+ # Paperclip.run("echo", "something", :expected_outcodes => [0,1,2,3])
95
+ #
96
+ # This method can log the command being run when
97
+ # Paperclip.options[:log_command] is set to true (defaults to false). This
98
+ # will only log if logging in general is set to true as well.
99
+ def run cmd, *params
100
+ if options[:image_magick_path]
101
+ Paperclip.log("[DEPRECATION] :image_magick_path is deprecated and will be removed. Use :command_path instead")
102
+ end
103
+ CommandLine.path = options[:command_path] || options[:image_magick_path]
104
+ CommandLine.new(cmd, *params).run
105
+ end
106
+
107
+ def processor name #:nodoc:
108
+ name = name.to_s.camelize
109
+ processor = Paperclip.const_get(name)
110
+ unless processor.ancestors.include?(Paperclip::Processor)
111
+ raise PaperclipError.new("Processor #{name} was not found")
112
+ end
113
+ processor
114
+ end
115
+
116
+ def each_instance_with_attachment(klass, name)
117
+ Object.const_get(klass).all.each do |instance|
118
+ yield(instance) if instance.send(:"#{name}?")
119
+ end
120
+ end
121
+
122
+ # Log a paperclip-specific line. Uses ActiveRecord::Base.logger
123
+ # by default. Set Paperclip.options[:log] to false to turn off.
124
+ def log message
125
+ logger.info("[paperclip] #{message}") if logging?
126
+ end
127
+
128
+ def logger #:nodoc:
129
+ ActiveRecord::Base.logger
130
+ end
131
+
132
+ def logging? #:nodoc:
133
+ options[:log]
134
+ end
135
+ end
136
+
137
+ class PaperclipError < StandardError #:nodoc:
138
+ end
139
+
140
+ class PaperclipCommandLineError < PaperclipError #:nodoc:
141
+ attr_accessor :output
142
+ def initialize(msg = nil, output = nil)
143
+ super(msg)
144
+ @output = output
145
+ end
146
+ end
147
+
148
+ class StorageMethodNotFound < PaperclipError
149
+ end
150
+
151
+ class CommandNotFoundError < PaperclipError
152
+ end
153
+
154
+ class NotIdentifiedByImageMagickError < PaperclipError #:nodoc:
155
+ end
156
+
157
+ class InfiniteInterpolationError < PaperclipError #:nodoc:
158
+ end
159
+
160
+ module Glue
161
+ def self.included base #:nodoc:
162
+ base.extend ClassMethods
163
+ if base.respond_to?("set_callback")
164
+ base.send :include, Paperclip::CallbackCompatability::Rails3
165
+ else
166
+ base.send :include, Paperclip::CallbackCompatability::Rails21
167
+ end
168
+ end
169
+ end
170
+
171
+ module ClassMethods
172
+ # +has_attached_file+ gives the class it is called on an attribute that maps to a file. This
173
+ # is typically a file stored somewhere on the filesystem and has been uploaded by a user.
174
+ # The attribute returns a Paperclip::Attachment object which handles the management of
175
+ # that file. The intent is to make the attachment as much like a normal attribute. The
176
+ # thumbnails will be created when the new file is assigned, but they will *not* be saved
177
+ # until +save+ is called on the record. Likewise, if the attribute is set to +nil+ is
178
+ # called on it, the attachment will *not* be deleted until +save+ is called. See the
179
+ # Paperclip::Attachment documentation for more specifics. There are a number of options
180
+ # you can set to change the behavior of a Paperclip attachment:
181
+ # * +url+: The full URL of where the attachment is publically accessible. This can just
182
+ # as easily point to a directory served directly through Apache as it can to an action
183
+ # that can control permissions. You can specify the full domain and path, but usually
184
+ # just an absolute path is sufficient. The leading slash *must* be included manually for
185
+ # absolute paths. The default value is
186
+ # "/system/:attachment/:id/:style/:filename". See
187
+ # Paperclip::Attachment#interpolate for more information on variable interpolaton.
188
+ # :url => "/:class/:attachment/:id/:style_:filename"
189
+ # :url => "http://some.other.host/stuff/:class/:id_:extension"
190
+ # * +default_url+: The URL that will be returned if there is no attachment assigned.
191
+ # This field is interpolated just as the url is. The default value is
192
+ # "/:attachment/:style/missing.png"
193
+ # has_attached_file :avatar, :default_url => "/images/default_:style_avatar.png"
194
+ # User.new.avatar_url(:small) # => "/images/default_small_avatar.png"
195
+ # * +styles+: A hash of thumbnail styles and their geometries. You can find more about
196
+ # geometry strings at the ImageMagick website
197
+ # (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
198
+ # also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally
199
+ # inside the dimensions and then crop the rest off (weighted at the center). The
200
+ # default value is to generate no thumbnails.
201
+ # * +default_style+: The thumbnail style that will be used by default URLs.
202
+ # Defaults to +original+.
203
+ # has_attached_file :avatar, :styles => { :normal => "100x100#" },
204
+ # :default_style => :normal
205
+ # user.avatar.url # => "/avatars/23/normal_me.png"
206
+ # * +whiny+: Will raise an error if Paperclip cannot post_process an uploaded file due
207
+ # to a command line error. This will override the global setting for this attachment.
208
+ # Defaults to true. This option used to be called :whiny_thumbanils, but this is
209
+ # deprecated.
210
+ # * +convert_options+: When creating thumbnails, use this free-form options
211
+ # array to pass in various convert command options. Typical options are "-strip" to
212
+ # remove all Exif data from the image (save space for thumbnails and avatars) or
213
+ # "-depth 8" to specify the bit depth of the resulting conversion. See ImageMagick
214
+ # convert documentation for more options: (http://www.imagemagick.org/script/convert.php)
215
+ # Note that this option takes a hash of options, each of which correspond to the style
216
+ # of thumbnail being generated. You can also specify :all as a key, which will apply
217
+ # to all of the thumbnails being generated. If you specify options for the :original,
218
+ # it would be best if you did not specify destructive options, as the intent of keeping
219
+ # the original around is to regenerate all the thumbnails when requirements change.
220
+ # has_attached_file :avatar, :styles => { :large => "300x300", :negative => "100x100" }
221
+ # :convert_options => {
222
+ # :all => "-strip",
223
+ # :negative => "-negate"
224
+ # }
225
+ # NOTE: While not deprecated yet, it is not recommended to specify options this way.
226
+ # It is recommended that :convert_options option be included in the hash passed to each
227
+ # :styles for compatability with future versions.
228
+ # NOTE: Strings supplied to :convert_options are split on space in order to undergo
229
+ # shell quoting for safety. If your options require a space, please pre-split them
230
+ # and pass an array to :convert_options instead.
231
+ # * +storage+: Chooses the storage backend where the files will be stored. The current
232
+ # choices are :filesystem and :s3. The default is :filesystem. Make sure you read the
233
+ # documentation for Paperclip::Storage::Filesystem and Paperclip::Storage::S3
234
+ # for backend-specific options.
235
+ def has_attached_file name, options = {}
236
+ include InstanceMethods
237
+
238
+ write_inheritable_attribute(:attachment_definitions, {}) if attachment_definitions.nil?
239
+ attachment_definitions[name] = {:validations => []}.merge(options)
240
+
241
+ after_save :save_attached_files
242
+ before_destroy :destroy_attached_files
243
+
244
+ define_paperclip_callbacks :post_process, :"#{name}_post_process"
245
+
246
+ define_method name do |*args|
247
+ a = attachment_for(name)
248
+ (args.length > 0) ? a.to_s(args.first) : a
249
+ end
250
+
251
+ define_method "#{name}=" do |file|
252
+ attachment_for(name).assign(file)
253
+ end
254
+
255
+ define_method "#{name}?" do
256
+ attachment_for(name).file?
257
+ end
258
+
259
+ validates_each(name) do |record, attr, value|
260
+ attachment = record.attachment_for(name)
261
+ attachment.send(:flush_errors)
262
+ end
263
+ end
264
+
265
+ # Places ActiveRecord-style validations on the size of the file assigned. The
266
+ # possible options are:
267
+ # * +in+: a Range of bytes (i.e. +1..1.megabyte+),
268
+ # * +less_than+: equivalent to :in => 0..options[:less_than]
269
+ # * +greater_than+: equivalent to :in => options[:greater_than]..Infinity
270
+ # * +message+: error message to display, use :min and :max as replacements
271
+ # * +if+: A lambda or name of a method on the instance. Validation will only
272
+ # be run is this lambda or method returns true.
273
+ # * +unless+: Same as +if+ but validates if lambda or method returns false.
274
+ def validates_attachment_size name, options = {}
275
+ min = options[:greater_than] || (options[:in] && options[:in].first) || 0
276
+ max = options[:less_than] || (options[:in] && options[:in].last) || (1.0/0)
277
+ range = (min..max)
278
+ message = options[:message] || "file size must be between :min and :max bytes."
279
+ message = message.gsub(/:min/, min.to_s).gsub(/:max/, max.to_s)
280
+
281
+ validates_inclusion_of :"#{name}_file_size",
282
+ :in => range,
283
+ :message => message,
284
+ :if => options[:if],
285
+ :unless => options[:unless],
286
+ :allow_nil => true
287
+ end
288
+
289
+ # Adds errors if thumbnail creation fails. The same as specifying :whiny_thumbnails => true.
290
+ def validates_attachment_thumbnails name, options = {}
291
+ warn('[DEPRECATION] validates_attachment_thumbnail is deprecated. ' +
292
+ 'This validation is on by default and will be removed from future versions. ' +
293
+ 'If you wish to turn it off, supply :whiny => false in your definition.')
294
+ attachment_definitions[name][:whiny_thumbnails] = true
295
+ end
296
+
297
+ # Places ActiveRecord-style validations on the presence of a file.
298
+ # Options:
299
+ # * +if+: A lambda or name of a method on the instance. Validation will only
300
+ # be run is this lambda or method returns true.
301
+ # * +unless+: Same as +if+ but validates if lambda or method returns false.
302
+ def validates_attachment_presence name, options = {}
303
+ message = options[:message] || "must be set."
304
+ validates_presence_of :"#{name}_file_name",
305
+ :message => message,
306
+ :if => options[:if],
307
+ :unless => options[:unless]
308
+ end
309
+
310
+ # Places ActiveRecord-style validations on the content type of the file
311
+ # assigned. The possible options are:
312
+ # * +content_type+: Allowed content types. Can be a single content type
313
+ # or an array. Each type can be a String or a Regexp. It should be
314
+ # noted that Internet Explorer upload files with content_types that you
315
+ # may not expect. For example, JPEG images are given image/pjpeg and
316
+ # PNGs are image/x-png, so keep that in mind when determining how you
317
+ # match. Allows all by default.
318
+ # * +message+: The message to display when the uploaded file has an invalid
319
+ # content type.
320
+ # * +if+: A lambda or name of a method on the instance. Validation will only
321
+ # be run is this lambda or method returns true.
322
+ # * +unless+: Same as +if+ but validates if lambda or method returns false.
323
+ # NOTE: If you do not specify an [attachment]_content_type field on your
324
+ # model, content_type validation will work _ONLY upon assignment_ and
325
+ # re-validation after the instance has been reloaded will always succeed.
326
+ def validates_attachment_content_type name, options = {}
327
+ validation_options = options.dup
328
+ allowed_types = [validation_options[:content_type]].flatten
329
+ validates_each(:"#{name}_content_type", validation_options) do |record, attr, value|
330
+ if !allowed_types.any?{|t| t === value } && !(value.nil? || value.blank?)
331
+ if record.errors.method(:add).arity == -2
332
+ message = options[:message] || "is not one of #{allowed_types.join(", ")}"
333
+ record.errors.add(:"#{name}_content_type", message)
334
+ else
335
+ record.errors.add(:"#{name}_content_type", :inclusion, :default => options[:message], :value => value)
336
+ end
337
+ end
338
+ end
339
+ end
340
+
341
+ # Returns the attachment definitions defined by each call to
342
+ # has_attached_file.
343
+ def attachment_definitions
344
+ read_inheritable_attribute(:attachment_definitions)
345
+ end
346
+ end
347
+
348
+ module InstanceMethods #:nodoc:
349
+ def attachment_for name
350
+ @_paperclip_attachments ||= {}
351
+ @_paperclip_attachments[name] ||= Attachment.new(name, self, self.class.attachment_definitions[name])
352
+ end
353
+
354
+ def each_attachment
355
+ self.class.attachment_definitions.each do |name, definition|
356
+ yield(name, attachment_for(name))
357
+ end
358
+ end
359
+
360
+ def save_attached_files
361
+ Paperclip.log("Saving attachments.")
362
+ each_attachment do |name, attachment|
363
+ attachment.send(:save)
364
+ end
365
+ end
366
+
367
+ def destroy_attached_files
368
+ Paperclip.log("Deleting attachments.")
369
+ each_attachment do |name, attachment|
370
+ attachment.send(:queue_existing_for_delete)
371
+ attachment.send(:flush_deletes)
372
+ end
373
+ end
374
+ end
375
+
376
+ end
@@ -0,0 +1,350 @@
1
+ # encoding: utf-8
2
+ module Paperclip
3
+ # The Attachment class manages the files for a given attachment. It saves
4
+ # when the model saves, deletes when the model is destroyed, and processes
5
+ # the file upon assignment.
6
+ class Attachment
7
+ include IOStream
8
+
9
+ def self.default_options
10
+ @default_options ||= {
11
+ :url => "/system/:attachment/:id/:style/:filename",
12
+ :path => ":rails_root/public:url",
13
+ :styles => {},
14
+ :processors => [:thumbnail],
15
+ :convert_options => {},
16
+ :default_url => "/:attachment/:style/missing.png",
17
+ :default_style => :original,
18
+ :storage => :filesystem,
19
+ :use_timestamp => true,
20
+ :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails]
21
+ }
22
+ end
23
+
24
+ attr_reader :name, :instance, :default_style, :convert_options, :queued_for_write, :whiny, :options
25
+
26
+ # Creates an Attachment object. +name+ is the name of the attachment,
27
+ # +instance+ is the ActiveRecord object instance it's attached to, and
28
+ # +options+ is the same as the hash passed to +has_attached_file+.
29
+ def initialize name, instance, options = {}
30
+ @name = name
31
+ @instance = instance
32
+
33
+ options = self.class.default_options.merge(options)
34
+
35
+ @url = options[:url]
36
+ @url = @url.call(self) if @url.is_a?(Proc)
37
+ @path = options[:path]
38
+ @path = @path.call(self) if @path.is_a?(Proc)
39
+ @styles = options[:styles]
40
+ @normalized_styles = nil
41
+ @default_url = options[:default_url]
42
+ @default_style = options[:default_style]
43
+ @storage = options[:storage]
44
+ @use_timestamp = options[:use_timestamp]
45
+ @whiny = options[:whiny_thumbnails] || options[:whiny]
46
+ @convert_options = options[:convert_options]
47
+ @processors = options[:processors]
48
+ @options = options
49
+ @queued_for_delete = []
50
+ @queued_for_write = {}
51
+ @errors = {}
52
+ @dirty = false
53
+
54
+ initialize_storage
55
+ end
56
+
57
+ def styles
58
+ unless @normalized_styles
59
+ @normalized_styles = {}
60
+ (@styles.respond_to?(:call) ? @styles.call(self) : @styles).each do |name, args|
61
+ @normalized_styles[name] = Paperclip::Style.new(name, args.dup, self)
62
+ end
63
+ end
64
+ @normalized_styles
65
+ end
66
+
67
+ def processors
68
+ @processors.respond_to?(:call) ? @processors.call(instance) : @processors
69
+ end
70
+
71
+ # What gets called when you call instance.attachment = File. It clears
72
+ # errors, assigns attributes, and processes the file. It
73
+ # also queues up the previous file for deletion, to be flushed away on
74
+ # #save of its host. In addition to form uploads, you can also assign
75
+ # another Paperclip attachment:
76
+ # new_user.avatar = old_user.avatar
77
+ def assign uploaded_file
78
+ ensure_required_accessors!
79
+
80
+ if uploaded_file.is_a?(Paperclip::Attachment)
81
+ uploaded_file = uploaded_file.to_file(:original)
82
+ close_uploaded_file = uploaded_file.respond_to?(:close)
83
+ end
84
+
85
+ return nil unless valid_assignment?(uploaded_file)
86
+
87
+ uploaded_file.binmode if uploaded_file.respond_to? :binmode
88
+ self.clear
89
+
90
+ return nil if uploaded_file.nil?
91
+
92
+ @queued_for_write[:original] = to_tempfile(uploaded_file)
93
+ instance_write(:file_name, uploaded_file.original_filename.strip)
94
+ instance_write(:content_type, uploaded_file.content_type.to_s.strip)
95
+ instance_write(:file_size, uploaded_file.size.to_i)
96
+ instance_write(:fingerprint, generate_fingerprint(uploaded_file))
97
+ instance_write(:updated_at, Time.now)
98
+
99
+ @dirty = true
100
+
101
+ post_process
102
+
103
+ # Reset the file size if the original file was reprocessed.
104
+ instance_write(:file_size, @queued_for_write[:original].size.to_i)
105
+ instance_write(:fingerprint, generate_fingerprint(@queued_for_write[:original]))
106
+ ensure
107
+ uploaded_file.close if close_uploaded_file
108
+ end
109
+
110
+ # Returns the public URL of the attachment, with a given style. Note that
111
+ # this does not necessarily need to point to a file that your web server
112
+ # can access and can point to an action in your app, if you need fine
113
+ # grained security. This is not recommended if you don't need the
114
+ # security, however, for performance reasons. Set use_timestamp to false
115
+ # if you want to stop the attachment update time appended to the url
116
+ def url(style_name = default_style, use_timestamp = @use_timestamp)
117
+ url = original_filename.nil? ? interpolate(@default_url, style_name) : interpolate(@url, style_name)
118
+ use_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url
119
+ end
120
+
121
+ # Returns the path of the attachment as defined by the :path option. If the
122
+ # file is stored in the filesystem the path refers to the path of the file
123
+ # on disk. If the file is stored in S3, the path is the "key" part of the
124
+ # URL, and the :bucket option refers to the S3 bucket.
125
+ def path style_name = default_style
126
+ original_filename.nil? ? nil : interpolate(@path, style_name)
127
+ end
128
+
129
+ # Alias to +url+
130
+ def to_s style_name = default_style
131
+ url(style_name)
132
+ end
133
+
134
+ # Returns an array containing the errors on this attachment.
135
+ def errors
136
+ @errors
137
+ end
138
+
139
+ # Returns true if there are changes that need to be saved.
140
+ def dirty?
141
+ @dirty
142
+ end
143
+
144
+ # Saves the file, if there are no errors. If there are, it flushes them to
145
+ # the instance's errors and returns false, cancelling the save.
146
+ def save
147
+ flush_deletes
148
+ flush_writes
149
+ @dirty = false
150
+ true
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
+ end
160
+
161
+ # Destroys the attachment. Has the same effect as previously assigning
162
+ # nil to the attachment *and saving*. This is permanent. If you wish to
163
+ # wipe out the existing attachment but not save, use #clear.
164
+ def destroy
165
+ clear
166
+ save
167
+ end
168
+
169
+ # Returns the name of the file as originally assigned, and lives in the
170
+ # <attachment>_file_name attribute of the model.
171
+ def original_filename
172
+ instance_read(:file_name)
173
+ end
174
+
175
+ # Returns the size of the file as originally assigned, and lives in the
176
+ # <attachment>_file_size attribute of the model.
177
+ def size
178
+ instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size)
179
+ end
180
+
181
+ # Returns the hash of the file as originally assigned, and lives in the
182
+ # <attachment>_fingerprint attribute of the model.
183
+ def fingerprint
184
+ instance_read(:fingerprint) || (@queued_for_write[:original] && generate_fingerprint(@queued_for_write[:original]))
185
+ end
186
+
187
+ # Returns the content_type of the file as originally assigned, and lives
188
+ # in the <attachment>_content_type attribute of the model.
189
+ def content_type
190
+ instance_read(:content_type)
191
+ end
192
+
193
+ # Returns the last modified time of the file as originally assigned, and
194
+ # lives in the <attachment>_updated_at attribute of the model.
195
+ def updated_at
196
+ time = instance_read(:updated_at)
197
+ time && time.to_f.to_i
198
+ end
199
+
200
+ def generate_fingerprint(source)
201
+ data = source.read
202
+ source.rewind if source.respond_to?(:rewind)
203
+ Digest::MD5.hexdigest(data)
204
+ end
205
+
206
+ # Paths and URLs can have a number of variables interpolated into them
207
+ # to vary the storage location based on name, id, style, class, etc.
208
+ # This method is a deprecated access into supplying and retrieving these
209
+ # interpolations. Future access should use either Paperclip.interpolates
210
+ # or extend the Paperclip::Interpolations module directly.
211
+ def self.interpolations
212
+ warn('[DEPRECATION] Paperclip::Attachment.interpolations is deprecated ' +
213
+ 'and will be removed from future versions. ' +
214
+ 'Use Paperclip.interpolates instead')
215
+ Paperclip::Interpolations
216
+ end
217
+
218
+ # This method really shouldn't be called that often. It's expected use is
219
+ # in the paperclip:refresh rake task and that's it. It will regenerate all
220
+ # thumbnails forcefully, by reobtaining the original file and going through
221
+ # the post-process again.
222
+ def reprocess!
223
+ new_original = Tempfile.new("paperclip-reprocess")
224
+ new_original.binmode
225
+ if old_original = to_file(:original)
226
+ new_original.write( old_original.respond_to?(:get) ? old_original.get : old_original.read )
227
+ new_original.rewind
228
+
229
+ @queued_for_write = { :original => new_original }
230
+ post_process
231
+
232
+ old_original.close if old_original.respond_to?(:close)
233
+
234
+ save
235
+ else
236
+ true
237
+ end
238
+ rescue Errno::EACCES => e
239
+ warn "#{e} - skipping file"
240
+ false
241
+ end
242
+
243
+ # Returns true if a file has been assigned.
244
+ def file?
245
+ !original_filename.blank?
246
+ end
247
+
248
+ # Writes the attachment-specific attribute on the instance. For example,
249
+ # instance_write(:file_name, "me.jpg") will write "me.jpg" to the instance's
250
+ # "avatar_file_name" field (assuming the attachment is called avatar).
251
+ def instance_write(attr, value)
252
+ setter = :"#{name}_#{attr}="
253
+ responds = instance.respond_to?(setter)
254
+ self.instance_variable_set("@_#{setter.to_s.chop}", value)
255
+ instance.send(setter, value) if responds || attr.to_s == "file_name"
256
+ end
257
+
258
+ # Reads the attachment-specific attribute on the instance. See instance_write
259
+ # for more details.
260
+ def instance_read(attr)
261
+ getter = :"#{name}_#{attr}"
262
+ responds = instance.respond_to?(getter)
263
+ cached = self.instance_variable_get("@_#{getter}")
264
+ return cached if cached
265
+ instance.send(getter) if responds || attr.to_s == "file_name"
266
+ end
267
+
268
+ private
269
+
270
+ def ensure_required_accessors! #:nodoc:
271
+ %w(file_name).each do |field|
272
+ unless @instance.respond_to?("#{name}_#{field}") && @instance.respond_to?("#{name}_#{field}=")
273
+ raise PaperclipError.new("#{@instance.class} model missing required attr_accessor for '#{name}_#{field}'")
274
+ end
275
+ end
276
+ end
277
+
278
+ def log message #:nodoc:
279
+ Paperclip.log(message)
280
+ end
281
+
282
+ def valid_assignment? file #:nodoc:
283
+ file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type))
284
+ end
285
+
286
+ def initialize_storage #:nodoc:
287
+ storage_class_name = @storage.to_s.capitalize
288
+ begin
289
+ @storage_module = Paperclip::Storage.const_get(storage_class_name)
290
+ rescue NameError
291
+ raise StorageMethodNotFound, "Cannot load storage module '#{storage_class_name}'"
292
+ end
293
+ self.extend(@storage_module)
294
+ end
295
+
296
+ def extra_options_for(style) #:nodoc:
297
+ all_options = convert_options[:all]
298
+ all_options = all_options.call(instance) if all_options.respond_to?(:call)
299
+ style_options = convert_options[style]
300
+ style_options = style_options.call(instance) if style_options.respond_to?(:call)
301
+
302
+ [ style_options, all_options ].compact.join(" ")
303
+ end
304
+
305
+ def post_process #:nodoc:
306
+ return if @queued_for_write[:original].nil?
307
+ instance.run_paperclip_callbacks(:post_process) do
308
+ instance.run_paperclip_callbacks(:"#{name}_post_process") do
309
+ post_process_styles
310
+ end
311
+ end
312
+ end
313
+
314
+ def post_process_styles #:nodoc:
315
+ styles.each do |name, style|
316
+ begin
317
+ raise RuntimeError.new("Style #{name} has no processors defined.") if style.processors.blank?
318
+ @queued_for_write[name] = style.processors.inject(@queued_for_write[:original]) do |file, processor|
319
+ Paperclip.processor(processor).make(file, style.processor_options, self)
320
+ end
321
+ rescue PaperclipError => e
322
+ log("An error was received while processing: #{e.inspect}")
323
+ (@errors[:processing] ||= []) << e.message if @whiny
324
+ end
325
+ end
326
+ end
327
+
328
+ def interpolate pattern, style_name = default_style #:nodoc:
329
+ Paperclip::Interpolations.interpolate(pattern, self, style_name)
330
+ end
331
+
332
+ def queue_existing_for_delete #:nodoc:
333
+ return unless file?
334
+ @queued_for_delete += [:original, *styles.keys].uniq.map do |style|
335
+ path(style) if exists?(style)
336
+ end.compact
337
+ instance_write(:file_name, nil)
338
+ instance_write(:content_type, nil)
339
+ instance_write(:file_size, nil)
340
+ instance_write(:updated_at, nil)
341
+ end
342
+
343
+ def flush_errors #:nodoc:
344
+ @errors.each do |error, message|
345
+ [message].flatten.each {|m| instance.errors.add(name, m) }
346
+ end
347
+ end
348
+
349
+ end
350
+ end