mbailey-paperclip 2.3.2

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 (56) hide show
  1. data/LICENSE +26 -0
  2. data/README.rdoc +179 -0
  3. data/Rakefile +76 -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/attachment.rb +326 -0
  12. data/lib/paperclip/callback_compatability.rb +61 -0
  13. data/lib/paperclip/geometry.rb +115 -0
  14. data/lib/paperclip/interpolations.rb +108 -0
  15. data/lib/paperclip/iostream.rb +59 -0
  16. data/lib/paperclip/matchers/have_attached_file_matcher.rb +57 -0
  17. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +74 -0
  18. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +54 -0
  19. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +95 -0
  20. data/lib/paperclip/matchers.rb +33 -0
  21. data/lib/paperclip/processor.rb +49 -0
  22. data/lib/paperclip/railtie.rb +24 -0
  23. data/lib/paperclip/storage.rb +247 -0
  24. data/lib/paperclip/style.rb +90 -0
  25. data/lib/paperclip/thumbnail.rb +78 -0
  26. data/lib/paperclip/upfile.rb +52 -0
  27. data/lib/paperclip/version.rb +3 -0
  28. data/lib/paperclip.rb +397 -0
  29. data/lib/tasks/paperclip.rake +79 -0
  30. data/rails/init.rb +2 -0
  31. data/shoulda_macros/paperclip.rb +119 -0
  32. data/test/attachment_test.rb +758 -0
  33. data/test/database.yml +4 -0
  34. data/test/fixtures/12k.png +0 -0
  35. data/test/fixtures/50x50.png +0 -0
  36. data/test/fixtures/5k.png +0 -0
  37. data/test/fixtures/bad.png +1 -0
  38. data/test/fixtures/s3.yml +8 -0
  39. data/test/fixtures/text.txt +0 -0
  40. data/test/fixtures/twopage.pdf +0 -0
  41. data/test/geometry_test.rb +177 -0
  42. data/test/helper.rb +148 -0
  43. data/test/integration_test.rb +483 -0
  44. data/test/interpolations_test.rb +124 -0
  45. data/test/iostream_test.rb +78 -0
  46. data/test/matchers/have_attached_file_matcher_test.rb +24 -0
  47. data/test/matchers/validate_attachment_content_type_matcher_test.rb +37 -0
  48. data/test/matchers/validate_attachment_presence_matcher_test.rb +26 -0
  49. data/test/matchers/validate_attachment_size_matcher_test.rb +51 -0
  50. data/test/paperclip_test.rb +317 -0
  51. data/test/processor_test.rb +10 -0
  52. data/test/storage_test.rb +343 -0
  53. data/test/style_test.rb +141 -0
  54. data/test/thumbnail_test.rb +227 -0
  55. data/test/upfile_test.rb +36 -0
  56. metadata +205 -0
@@ -0,0 +1,90 @@
1
+ # encoding: utf-8
2
+ module Paperclip
3
+ # The Style class holds the definition of a thumbnail style, applying
4
+ # whatever processing is required to normalize the definition and delaying
5
+ # the evaluation of block parameters until useful context is available.
6
+
7
+ class Style
8
+
9
+ attr_reader :name, :attachment, :format
10
+
11
+ # Creates a Style object. +name+ is the name of the attachment,
12
+ # +definition+ is the style definition from has_attached_file, which
13
+ # can be string, array or hash
14
+ def initialize name, definition, attachment
15
+ @name = name
16
+ @attachment = attachment
17
+ if definition.is_a? Hash
18
+ @geometry = definition.delete(:geometry)
19
+ @format = definition.delete(:format)
20
+ @processors = definition.delete(:processors)
21
+ @other_args = definition
22
+ else
23
+ @geometry, @format = [definition, nil].flatten[0..1]
24
+ @other_args = {}
25
+ end
26
+ @format = nil if @format.blank?
27
+ end
28
+
29
+ # retrieves from the attachment the processors defined in the has_attached_file call
30
+ # (which method (in the attachment) will call any supplied procs)
31
+ # There is an important change of interface here: a style rule can set its own processors
32
+ # by default we behave as before, though.
33
+ def processors
34
+ @processors || attachment.processors
35
+ end
36
+
37
+ # retrieves from the attachment the whiny setting
38
+ def whiny
39
+ attachment.whiny
40
+ end
41
+
42
+ # returns true if we're inclined to grumble
43
+ def whiny?
44
+ !!whiny
45
+ end
46
+
47
+ def convert_options
48
+ attachment.send(:extra_options_for, name)
49
+ end
50
+
51
+ # returns the geometry string for this style
52
+ # if a proc has been supplied, we call it here
53
+ def geometry
54
+ @geometry.respond_to?(:call) ? @geometry.call(attachment.instance) : @geometry
55
+ end
56
+
57
+ # Supplies the hash of options that processors expect to receive as their second argument
58
+ # Arguments other than the standard geometry, format etc are just passed through from
59
+ # initialization and any procs are called here, just before post-processing.
60
+ def processor_options
61
+ args = {}
62
+ @other_args.each do |k,v|
63
+ args[k] = v.respond_to?(:call) ? v.call(attachment) : v
64
+ end
65
+ [:processors, :geometry, :format, :whiny, :convert_options].each do |k|
66
+ (arg = send(k)) && args[k] = arg
67
+ end
68
+ args
69
+ end
70
+
71
+ # Supports getting and setting style properties with hash notation to ensure backwards-compatibility
72
+ # eg. @attachment.styles[:large][:geometry]@ will still work
73
+ def [](key)
74
+ if [:name, :convert_options, :whiny, :processors, :geometry, :format].include?(key)
75
+ send(key)
76
+ elsif defined? @other_args[key]
77
+ @other_args[key]
78
+ end
79
+ end
80
+
81
+ def []=(key, value)
82
+ if [:name, :convert_options, :whiny, :processors, :geometry, :format].include?(key)
83
+ send("#{key}=".intern, value)
84
+ else
85
+ @other_args[key] = value
86
+ end
87
+ end
88
+
89
+ end
90
+ end
@@ -0,0 +1,78 @@
1
+ module Paperclip
2
+ # Handles thumbnailing images that are uploaded.
3
+ class Thumbnail < Processor
4
+
5
+ attr_accessor :current_geometry, :target_geometry, :format, :whiny, :convert_options, :source_file_options
6
+
7
+ # Creates a Thumbnail object set to work on the +file+ given. It
8
+ # will attempt to transform the image into one defined by +target_geometry+
9
+ # which is a "WxH"-style string. +format+ will be inferred from the +file+
10
+ # unless specified. Thumbnail creation will raise no errors unless
11
+ # +whiny+ is true (which it is, by default. If +convert_options+ is
12
+ # set, the options will be appended to the convert command upon image conversion
13
+ def initialize file, options = {}, attachment = nil
14
+ super
15
+
16
+ geometry = options[:geometry]
17
+ @file = file
18
+ @crop = geometry[-1,1] == '#'
19
+ @target_geometry = Geometry.parse geometry
20
+ @current_geometry = Geometry.from_file @file
21
+ @source_file_options = options[:source_file_options]
22
+ @convert_options = options[:convert_options]
23
+ @whiny = options[:whiny].nil? ? true : options[:whiny]
24
+ @format = options[:format]
25
+
26
+ @source_file_options = @source_file_options.split(/\s+/) if @source_file_options.respond_to?(:split)
27
+ @convert_options = @convert_options.split(/\s+/) if @convert_options.respond_to?(:split)
28
+
29
+ @current_format = File.extname(@file.path)
30
+ @basename = File.basename(@file.path, @current_format)
31
+
32
+ end
33
+
34
+ # Returns true if the +target_geometry+ is meant to crop.
35
+ def crop?
36
+ @crop
37
+ end
38
+
39
+ # Returns true if the image is meant to make use of additional convert options.
40
+ def convert_options?
41
+ !@convert_options.nil? && !@convert_options.empty?
42
+ end
43
+
44
+ # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
45
+ # that contains the new image.
46
+ def make
47
+ src = @file
48
+ dst = Tempfile.new([@basename, @format].compact.join("."))
49
+ dst.binmode
50
+
51
+ begin
52
+ options = [
53
+ source_file_options,
54
+ "#{ File.expand_path(src.path) }[0]",
55
+ transformation_command,
56
+ convert_options,
57
+ "#{ File.expand_path(dst.path) }"
58
+ ].flatten.compact
59
+
60
+ success = Paperclip.run("convert", *options)
61
+ rescue PaperclipCommandLineError => e
62
+ raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny
63
+ end
64
+
65
+ dst
66
+ end
67
+
68
+ # Returns the command ImageMagick's +convert+ needs to transform the image
69
+ # into the thumbnail.
70
+ def transformation_command
71
+ scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
72
+ trans = []
73
+ trans << "-resize" << scale unless scale.nil? || scale.empty?
74
+ trans << "-crop" << crop << "+repage" if crop
75
+ trans
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,52 @@
1
+ module Paperclip
2
+ # The Upfile module is a convenience module for adding uploaded-file-type methods
3
+ # to the +File+ class. Useful for testing.
4
+ # user.avatar = File.new("test/test_avatar.jpg")
5
+ module Upfile
6
+
7
+ # Infer the MIME-type of the file from the extension.
8
+ def content_type
9
+ type = (self.path.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase
10
+ case type
11
+ when %r"jp(e|g|eg)" then "image/jpeg"
12
+ when %r"tiff?" then "image/tiff"
13
+ when %r"png", "gif", "bmp" then "image/#{type}"
14
+ when "txt" then "text/plain"
15
+ when %r"html?" then "text/html"
16
+ when "js" then "application/js"
17
+ when "csv", "xml", "css" then "text/#{type}"
18
+ else
19
+ # On BSDs, `file` doesn't give a result code of 1 if the file doesn't exist.
20
+ content_type = (Paperclip.run("file", "--mime-type", self.path).split(':').last.strip rescue "application/x-#{type}")
21
+ content_type = "application/x-#{type}" if content_type.match(/\(.*?\)/)
22
+ content_type
23
+ end
24
+ end
25
+
26
+ # Returns the file's normal name.
27
+ def original_filename
28
+ File.basename(self.path)
29
+ end
30
+
31
+ # Returns the size of the file.
32
+ def size
33
+ File.size(self)
34
+ end
35
+ end
36
+ end
37
+
38
+ if defined? StringIO
39
+ class StringIO
40
+ attr_accessor :original_filename, :content_type
41
+ def original_filename
42
+ @original_filename ||= "stringio.txt"
43
+ end
44
+ def content_type
45
+ @content_type ||= "text/plain"
46
+ end
47
+ end
48
+ end
49
+
50
+ class File #:nodoc:
51
+ include Paperclip::Upfile
52
+ end
@@ -0,0 +1,3 @@
1
+ module Paperclip
2
+ VERSION = "2.3.2" unless defined? Paperclip::VERSION
3
+ end
data/lib/paperclip.rb ADDED
@@ -0,0 +1,397 @@
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 'tempfile'
30
+ require 'paperclip/version'
31
+ require 'paperclip/upfile'
32
+ require 'paperclip/iostream'
33
+ require 'paperclip/geometry'
34
+ require 'paperclip/processor'
35
+ require 'paperclip/thumbnail'
36
+ require 'paperclip/storage'
37
+ require 'paperclip/interpolations'
38
+ require 'paperclip/style'
39
+ require 'paperclip/attachment'
40
+ require 'paperclip/callback_compatability'
41
+ require 'paperclip/railtie'
42
+ if defined?(Rails.root) && Rails.root
43
+ Dir.glob(File.join(File.expand_path(Rails.root), "lib", "paperclip_processors", "*.rb")).each do |processor|
44
+ require processor
45
+ end
46
+ end
47
+
48
+ # The base module that gets included in ActiveRecord::Base. See the
49
+ # documentation for Paperclip::ClassMethods for more useful information.
50
+ module Paperclip
51
+
52
+ class << self
53
+ # Provides configurability to Paperclip. There are a number of options available, such as:
54
+ # * whiny: Will raise an error if Paperclip cannot process thumbnails of
55
+ # an uploaded image. Defaults to true.
56
+ # * log: Logs progress to the Rails log. Uses ActiveRecord's logger, so honors
57
+ # log levels, etc. Defaults to true.
58
+ # * command_path: Defines the path at which to find the command line
59
+ # programs if they are not visible to Rails the system's search path. Defaults to
60
+ # nil, which uses the first executable found in the user's search path.
61
+ # * image_magick_path: Deprecated alias of command_path.
62
+ def options
63
+ @options ||= {
64
+ :whiny => true,
65
+ :image_magick_path => nil,
66
+ :command_path => nil,
67
+ :log => true,
68
+ :log_command => true,
69
+ :swallow_stderr => true
70
+ }
71
+ end
72
+
73
+ def configure
74
+ yield(self) if block_given?
75
+ end
76
+
77
+ def path_for_command command #:nodoc:
78
+ if options[:image_magick_path]
79
+ warn("[DEPRECATION] :image_magick_path is deprecated and will be removed. Use :command_path instead")
80
+ end
81
+ path = [options[:command_path] || options[:image_magick_path], command].compact
82
+ File.join(*path)
83
+ end
84
+
85
+ def interpolates key, &block
86
+ Paperclip::Interpolations[key] = block
87
+ end
88
+
89
+ # The run method takes a command to execute and an array of parameters
90
+ # that get passed to it. The command is prefixed with the :command_path
91
+ # option from Paperclip.options. If you have many commands to run and
92
+ # they are in different paths, the suggested course of action is to
93
+ # symlink them so they are all in the same directory.
94
+ #
95
+ # If the command returns with a result code that is not one of the
96
+ # expected_outcodes, a PaperclipCommandLineError will be raised. Generally
97
+ # a code of 0 is expected, but a list of codes may be passed if necessary.
98
+ # These codes should be passed as a hash as the last argument, like so:
99
+ #
100
+ # Paperclip.run("echo", "something", :expected_outcodes => [0,1,2,3])
101
+ #
102
+ # This method can log the command being run when
103
+ # Paperclip.options[:log_command] is set to true (defaults to false). This
104
+ # will only log if logging in general is set to true as well.
105
+ def run cmd, *params
106
+ options = params.last.is_a?(Hash) ? params.pop : {}
107
+ expected_outcodes = options[:expected_outcodes] || [0]
108
+ params = quote_command_options(*params).join(" ")
109
+
110
+ command = %Q[#{path_for_command(cmd)} #{params}]
111
+ command = "#{command} 2>#{bit_bucket}" if Paperclip.options[:swallow_stderr]
112
+ Paperclip.log(command) if Paperclip.options[:log_command]
113
+
114
+ begin
115
+ output = `#{command}`
116
+
117
+ raise CommandNotFoundError if $?.exitstatus == 127
118
+
119
+ unless expected_outcodes.include?($?.exitstatus)
120
+ raise PaperclipCommandLineError,
121
+ "Error while running #{cmd}. Expected return code to be #{expected_outcodes.join(", ")} but was #{$?.exitstatus}",
122
+ output
123
+ end
124
+ rescue Errno::ENOENT => e
125
+ raise CommandNotFoundError
126
+ end
127
+
128
+ output
129
+ end
130
+
131
+ def quote_command_options(*options)
132
+ options.map do |option|
133
+ option.split("'").map{|m| "'#{m}'" }.join("\\'")
134
+ end
135
+ end
136
+
137
+ def bit_bucket #:nodoc:
138
+ File.exists?("/dev/null") ? "/dev/null" : "NUL"
139
+ end
140
+
141
+ def included base #:nodoc:
142
+ base.extend ClassMethods
143
+ if base.respond_to?("set_callback")
144
+ base.send :include, Paperclip::CallbackCompatability::Rails3
145
+ else
146
+ base.send :include, Paperclip::CallbackCompatability::Rails21
147
+ end
148
+ end
149
+
150
+ def processor name #:nodoc:
151
+ name = name.to_s.camelize
152
+ processor = Paperclip.const_get(name)
153
+ unless processor.ancestors.include?(Paperclip::Processor)
154
+ raise PaperclipError.new("Processor #{name} was not found")
155
+ end
156
+ processor
157
+ end
158
+
159
+ # Log a paperclip-specific line. Uses ActiveRecord::Base.logger
160
+ # by default. Set Paperclip.options[:log] to false to turn off.
161
+ def log message
162
+ logger.info("[paperclip] #{message}") if logging?
163
+ end
164
+
165
+ def logger #:nodoc:
166
+ ActiveRecord::Base.logger
167
+ end
168
+
169
+ def logging? #:nodoc:
170
+ options[:log]
171
+ end
172
+ end
173
+
174
+ class PaperclipError < StandardError #:nodoc:
175
+ end
176
+
177
+ class PaperclipCommandLineError < PaperclipError #:nodoc:
178
+ attr_accessor :output
179
+ def initialize(msg = nil, output = nil)
180
+ super(msg)
181
+ @output = output
182
+ end
183
+ end
184
+
185
+ class CommandNotFoundError < PaperclipError
186
+ end
187
+
188
+ class NotIdentifiedByImageMagickError < PaperclipError #:nodoc:
189
+ end
190
+
191
+ class InfiniteInterpolationError < PaperclipError #:nodoc:
192
+ end
193
+
194
+ module ClassMethods
195
+ # +has_attached_file+ gives the class it is called on an attribute that maps to a file. This
196
+ # is typically a file stored somewhere on the filesystem and has been uploaded by a user.
197
+ # The attribute returns a Paperclip::Attachment object which handles the management of
198
+ # that file. The intent is to make the attachment as much like a normal attribute. The
199
+ # thumbnails will be created when the new file is assigned, but they will *not* be saved
200
+ # until +save+ is called on the record. Likewise, if the attribute is set to +nil+ is
201
+ # called on it, the attachment will *not* be deleted until +save+ is called. See the
202
+ # Paperclip::Attachment documentation for more specifics. There are a number of options
203
+ # you can set to change the behavior of a Paperclip attachment:
204
+ # * +url+: The full URL of where the attachment is publically accessible. This can just
205
+ # as easily point to a directory served directly through Apache as it can to an action
206
+ # that can control permissions. You can specify the full domain and path, but usually
207
+ # just an absolute path is sufficient. The leading slash *must* be included manually for
208
+ # absolute paths. The default value is
209
+ # "/system/:attachment/:id/:style/:filename". See
210
+ # Paperclip::Attachment#interpolate for more information on variable interpolaton.
211
+ # :url => "/:class/:attachment/:id/:style_:filename"
212
+ # :url => "http://some.other.host/stuff/:class/:id_:extension"
213
+ # * +default_url+: The URL that will be returned if there is no attachment assigned.
214
+ # This field is interpolated just as the url is. The default value is
215
+ # "/:attachment/:style/missing.png"
216
+ # has_attached_file :avatar, :default_url => "/images/default_:style_avatar.png"
217
+ # User.new.avatar_url(:small) # => "/images/default_small_avatar.png"
218
+ # * +styles+: A hash of thumbnail styles and their geometries. You can find more about
219
+ # geometry strings at the ImageMagick website
220
+ # (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
221
+ # also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally
222
+ # inside the dimensions and then crop the rest off (weighted at the center). The
223
+ # default value is to generate no thumbnails.
224
+ # * +default_style+: The thumbnail style that will be used by default URLs.
225
+ # Defaults to +original+.
226
+ # has_attached_file :avatar, :styles => { :normal => "100x100#" },
227
+ # :default_style => :normal
228
+ # user.avatar.url # => "/avatars/23/normal_me.png"
229
+ # * +whiny+: Will raise an error if Paperclip cannot post_process an uploaded file due
230
+ # to a command line error. This will override the global setting for this attachment.
231
+ # Defaults to true. This option used to be called :whiny_thumbanils, but this is
232
+ # deprecated.
233
+ # * +convert_options+: When creating thumbnails, use this free-form options
234
+ # array to pass in various convert command options. Typical options are "-strip" to
235
+ # remove all Exif data from the image (save space for thumbnails and avatars) or
236
+ # "-depth 8" to specify the bit depth of the resulting conversion. See ImageMagick
237
+ # convert documentation for more options: (http://www.imagemagick.org/script/convert.php)
238
+ # Note that this option takes a hash of options, each of which correspond to the style
239
+ # of thumbnail being generated. You can also specify :all as a key, which will apply
240
+ # to all of the thumbnails being generated. If you specify options for the :original,
241
+ # it would be best if you did not specify destructive options, as the intent of keeping
242
+ # the original around is to regenerate all the thumbnails when requirements change.
243
+ # has_attached_file :avatar, :styles => { :large => "300x300", :negative => "100x100" }
244
+ # :convert_options => {
245
+ # :all => "-strip",
246
+ # :negative => "-negate"
247
+ # }
248
+ # NOTE: While not deprecated yet, it is not recommended to specify options this way.
249
+ # It is recommended that :convert_options option be included in the hash passed to each
250
+ # :styles for compatability with future versions.
251
+ # NOTE: Strings supplied to :convert_options are split on space in order to undergo
252
+ # shell quoting for safety. If your options require a space, please pre-split them
253
+ # and pass an array to :convert_options instead.
254
+ # * +storage+: Chooses the storage backend where the files will be stored. The current
255
+ # choices are :filesystem and :s3. The default is :filesystem. Make sure you read the
256
+ # documentation for Paperclip::Storage::Filesystem and Paperclip::Storage::S3
257
+ # for backend-specific options.
258
+ def has_attached_file name, options = {}
259
+ include InstanceMethods
260
+
261
+ write_inheritable_attribute(:attachment_definitions, {}) if attachment_definitions.nil?
262
+ attachment_definitions[name] = {:validations => []}.merge(options)
263
+
264
+ after_save :save_attached_files
265
+ before_destroy :destroy_attached_files
266
+
267
+ define_paperclip_callbacks :post_process, :"#{name}_post_process"
268
+
269
+ define_method name do |*args|
270
+ a = attachment_for(name)
271
+ (args.length > 0) ? a.to_s(args.first) : a
272
+ end
273
+
274
+ define_method "#{name}=" do |file|
275
+ attachment_for(name).assign(file)
276
+ end
277
+
278
+ define_method "#{name}?" do
279
+ attachment_for(name).file?
280
+ end
281
+
282
+ validates_each(name) do |record, attr, value|
283
+ attachment = record.attachment_for(name)
284
+ attachment.send(:flush_errors)
285
+ end
286
+ end
287
+
288
+ # Places ActiveRecord-style validations on the size of the file assigned. The
289
+ # possible options are:
290
+ # * +in+: a Range of bytes (i.e. +1..1.megabyte+),
291
+ # * +less_than+: equivalent to :in => 0..options[:less_than]
292
+ # * +greater_than+: equivalent to :in => options[:greater_than]..Infinity
293
+ # * +message+: error message to display, use :min and :max as replacements
294
+ # * +if+: A lambda or name of a method on the instance. Validation will only
295
+ # be run is this lambda or method returns true.
296
+ # * +unless+: Same as +if+ but validates if lambda or method returns false.
297
+ def validates_attachment_size name, options = {}
298
+ min = options[:greater_than] || (options[:in] && options[:in].first) || 0
299
+ max = options[:less_than] || (options[:in] && options[:in].last) || (1.0/0)
300
+ range = (min..max)
301
+ message = options[:message] || "file size must be between :min and :max bytes."
302
+ message = message.gsub(/:min/, min.to_s).gsub(/:max/, max.to_s)
303
+
304
+ validates_inclusion_of :"#{name}_file_size",
305
+ :in => range,
306
+ :message => message,
307
+ :if => options[:if],
308
+ :unless => options[:unless]
309
+ end
310
+
311
+ # Adds errors if thumbnail creation fails. The same as specifying :whiny_thumbnails => true.
312
+ def validates_attachment_thumbnails name, options = {}
313
+ warn('[DEPRECATION] validates_attachment_thumbnail is deprecated. ' +
314
+ 'This validation is on by default and will be removed from future versions. ' +
315
+ 'If you wish to turn it off, supply :whiny => false in your definition.')
316
+ attachment_definitions[name][:whiny_thumbnails] = true
317
+ end
318
+
319
+ # Places ActiveRecord-style validations on the presence of a file.
320
+ # Options:
321
+ # * +if+: A lambda or name of a method on the instance. Validation will only
322
+ # be run is this lambda or method returns true.
323
+ # * +unless+: Same as +if+ but validates if lambda or method returns false.
324
+ def validates_attachment_presence name, options = {}
325
+ message = options[:message] || "must be set."
326
+ validates_presence_of :"#{name}_file_name",
327
+ :message => message,
328
+ :if => options[:if],
329
+ :unless => options[:unless]
330
+ end
331
+
332
+ # Places ActiveRecord-style validations on the content type of the file
333
+ # assigned. The possible options are:
334
+ # * +content_type+: Allowed content types. Can be a single content type
335
+ # or an array. Each type can be a String or a Regexp. It should be
336
+ # noted that Internet Explorer upload files with content_types that you
337
+ # may not expect. For example, JPEG images are given image/pjpeg and
338
+ # PNGs are image/x-png, so keep that in mind when determining how you
339
+ # match. Allows all by default.
340
+ # * +message+: The message to display when the uploaded file has an invalid
341
+ # content type.
342
+ # * +if+: A lambda or name of a method on the instance. Validation will only
343
+ # be run is this lambda or method returns true.
344
+ # * +unless+: Same as +if+ but validates if lambda or method returns false.
345
+ # NOTE: If you do not specify an [attachment]_content_type field on your
346
+ # model, content_type validation will work _ONLY upon assignment_ and
347
+ # re-validation after the instance has been reloaded will always succeed.
348
+ def validates_attachment_content_type name, options = {}
349
+ types = [options.delete(:content_type)].flatten
350
+ validates_each(:"#{name}_content_type", options) do |record, attr, value|
351
+ unless types.any?{|t| t === value }
352
+ if record.errors.method(:add).arity == -2
353
+ message = options[:message] || "is not one of #{types.join(", ")}"
354
+ record.errors.add(:"#{name}_content_type", message)
355
+ else
356
+ record.errors.add(:"#{name}_content_type", :inclusion, :default => options[:message], :value => value)
357
+ end
358
+ end
359
+ end
360
+ end
361
+
362
+ # Returns the attachment definitions defined by each call to
363
+ # has_attached_file.
364
+ def attachment_definitions
365
+ read_inheritable_attribute(:attachment_definitions)
366
+ end
367
+ end
368
+
369
+ module InstanceMethods #:nodoc:
370
+ def attachment_for name
371
+ @_paperclip_attachments ||= {}
372
+ @_paperclip_attachments[name] ||= Attachment.new(name, self, self.class.attachment_definitions[name])
373
+ end
374
+
375
+ def each_attachment
376
+ self.class.attachment_definitions.each do |name, definition|
377
+ yield(name, attachment_for(name))
378
+ end
379
+ end
380
+
381
+ def save_attached_files
382
+ Paperclip.log("Saving attachments.")
383
+ each_attachment do |name, attachment|
384
+ attachment.send(:save)
385
+ end
386
+ end
387
+
388
+ def destroy_attached_files
389
+ Paperclip.log("Deleting attachments.")
390
+ each_attachment do |name, attachment|
391
+ attachment.send(:queue_existing_for_delete)
392
+ attachment.send(:flush_deletes)
393
+ end
394
+ end
395
+ end
396
+
397
+ end