dm-paperclip 2.1.4 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -34,13 +34,24 @@ In your model:
34
34
  class User
35
35
  include DataMapper::Resource
36
36
  include Paperclip::Resource
37
- property :id, Integer, :serial => true
37
+ property :id, Serial
38
38
  property :username, String
39
39
  has_attached_file :avatar,
40
40
  :styles => { :medium => "300x300>",
41
41
  :thumb => "100x100>" }
42
42
  end
43
43
 
44
+ You will need to add an initializer to configure Paperclip. If on Rails, can add a config/initializers/paperclip.rb, on Merb
45
+ can use config/init.rb and add it to the Merb::BootLoader.after_app_loads section. Can also use environment configs, rackup
46
+ file, Rake task, wherever.
47
+
48
+ Paperclip.configure do |config|
49
+ config.root = Rails.root # the application root to anchor relative urls (defaults to Dir.pwd)
50
+ config.env = Rails.env # server env support, defaults to ENV['RACK_ENV'] or 'development'
51
+ config.use_dm_validations = true # validate attachment sizes and such, defaults to false
52
+ config.processors_path = 'lib/pc' # relative path to look for processors, defaults to 'lib/paperclip_processors'
53
+ end
54
+
44
55
  Your database will need to add four columns, avatar_file_name (varchar), avatar_content_type (varchar), and
45
56
  avatar_file_size (integer), and avatar_updated_at (datetime). You can either add these manually, auto-
46
57
  migrate, or use the following migration:
@@ -96,7 +107,7 @@ into your mode:
96
107
  include DataMapper::Resource
97
108
  include DataMapper::Validate
98
109
  include Paperclip::Resource
99
- property :id, Integer, :serial => true
110
+ property :id, Serial
100
111
  property :username, String
101
112
  has_attached_file :avatar,
102
113
  :styles => { :medium => "300x300>",
data/Rakefile CHANGED
@@ -4,6 +4,7 @@ require 'rake/rdoctask'
4
4
  require 'rake/gempackagetask'
5
5
 
6
6
  $LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
7
+ require 'dm-core'
7
8
  require 'dm-validations'
8
9
  require 'dm-paperclip'
9
10
 
@@ -78,6 +79,11 @@ Rake::GemPackageTask.new(spec) do |pkg|
78
79
  pkg.need_tar = true
79
80
  end
80
81
 
82
+ desc 'Generate gemspec'
83
+ task :gemspec do
84
+ File.open("#{spec.name}.gemspec", 'w') { |f| f.puts(spec.to_ruby) }
85
+ end
86
+
81
87
  WIN32 = (PLATFORM =~ /win32|cygwin/) rescue nil
82
88
  SUDO = WIN32 ? '' : ('sudo' unless ENV['SUDOLESS'])
83
89
 
@@ -26,46 +26,188 @@
26
26
  # See the +has_attached_file+ documentation for more details.
27
27
 
28
28
  require 'tempfile'
29
- require File.join(File.dirname(__FILE__), 'dm-paperclip', 'upfile')
30
- require File.join(File.dirname(__FILE__), 'dm-paperclip', 'iostream')
31
- require File.join(File.dirname(__FILE__), 'dm-paperclip', 'geometry')
32
- require File.join(File.dirname(__FILE__), 'dm-paperclip', 'thumbnail')
33
- require File.join(File.dirname(__FILE__), 'dm-paperclip', 'storage')
34
- require File.join(File.dirname(__FILE__), 'dm-paperclip', 'attachment')
35
29
 
36
- # Only include validations if dm-validations is loaded
37
- require File.join(File.dirname(__FILE__), 'dm-paperclip', 'validations') unless defined?(DataMapper::Validate).nil?
30
+ require 'dm-core'
38
31
 
32
+ require 'dm-paperclip/upfile'
33
+ require 'dm-paperclip/iostream'
34
+ require 'dm-paperclip/geometry'
35
+ require 'dm-paperclip/processor'
36
+ require 'dm-paperclip/thumbnail'
37
+ require 'dm-paperclip/storage'
38
+ require 'dm-paperclip/interpolations'
39
+ require 'dm-paperclip/attachment'
40
+
41
+ # The base module that gets included in ActiveRecord::Base. See the
42
+ # documentation for Paperclip::ClassMethods for more useful information.
39
43
  module Paperclip
40
- VERSION = "2.1.4"
44
+
45
+ VERSION = "2.3.0"
46
+
47
+ # To configure Paperclip, put this code in an initializer, Rake task, or wherever:
48
+ #
49
+ # Paperclip.configure do |config|
50
+ # config.root = Rails.root # the application root to anchor relative urls (defaults to Dir.pwd)
51
+ # config.env = Rails.env # server env support, defaults to ENV['RACK_ENV'] or 'development'
52
+ # config.use_dm_validations = true # validate attachment sizes and such, defaults to false
53
+ # config.processors_path = 'lib/pc' # relative path to look for processors, defaults to 'lib/paperclip_processors'
54
+ # end
55
+ #
56
+ def self.configure
57
+ yield @config = Configuration.new
58
+ Paperclip.config = @config
59
+ end
60
+
61
+ def self.config=(config)
62
+ @config = config
63
+ end
64
+
65
+ def self.config
66
+ @config ||= Configuration.new
67
+ end
68
+
69
+ def self.require_processors
70
+ return if @processors_already_required
71
+ Dir.glob(File.expand_path("#{Paperclip.config.processors_path}/*.rb")).sort.each do |processor|
72
+ require processor
73
+ end
74
+ @processors_already_required = true
75
+ end
76
+
77
+ class Configuration
78
+
79
+ DEFAULT_PROCESSORS_PATH = 'lib/paperclip_processors'
80
+
81
+ attr_writer :root, :env
82
+ attr_accessor :use_dm_validations
83
+
84
+ def root
85
+ @root ||= Dir.pwd
86
+ end
87
+
88
+ def env
89
+ @env ||= (ENV['RACK_ENV'] || 'development')
90
+ end
91
+
92
+ def processors_path=(path)
93
+ @processors_path = File.expand_path(path, root)
94
+ end
95
+
96
+ def processors_path
97
+ @processors_path ||= File.expand_path("../#{DEFAULT_PROCESSORS_PATH}", root)
98
+ end
99
+
100
+ end
101
+
41
102
  class << self
103
+
42
104
  # Provides configurability to Paperclip. There are a number of options available, such as:
43
- # * whiny_thumbnails: Will raise an error if Paperclip cannot process thumbnails of
105
+ # * whiny: Will raise an error if Paperclip cannot process thumbnails of
44
106
  # an uploaded image. Defaults to true.
45
- # * image_magick_path: Defines the path at which to find the +convert+ and +identify+
107
+ # * log: Logs progress to the Rails log. Uses ActiveRecord's logger, so honors
108
+ # log levels, etc. Defaults to true.
109
+ # * command_path: Defines the path at which to find the command line
46
110
  # programs if they are not visible to Rails the system's search path. Defaults to
47
- # nil, which uses the first executable found in the search path.
111
+ # nil, which uses the first executable found in the user's search path.
112
+ # * image_magick_path: Deprecated alias of command_path.
48
113
  def options
49
114
  @options ||= {
50
- :whiny_thumbnails => true,
51
- :image_magick_path => nil
115
+ :whiny => true,
116
+ :image_magick_path => nil,
117
+ :command_path => nil,
118
+ :log => true,
119
+ :log_command => false,
120
+ :swallow_stderr => true
52
121
  }
53
122
  end
54
123
 
55
124
  def path_for_command command #:nodoc:
56
- path = [options[:image_magick_path], command].compact
125
+ if options[:image_magick_path]
126
+ warn("[DEPRECATION] :image_magick_path is deprecated and will be removed. Use :command_path instead")
127
+ end
128
+ path = [options[:command_path] || options[:image_magick_path], command].compact
57
129
  File.join(*path)
58
130
  end
131
+
132
+ def interpolates key, &block
133
+ Paperclip::Interpolations[key] = block
134
+ end
135
+
136
+ # The run method takes a command to execute and a string of parameters
137
+ # that get passed to it. The command is prefixed with the :command_path
138
+ # option from Paperclip.options. If you have many commands to run and
139
+ # they are in different paths, the suggested course of action is to
140
+ # symlink them so they are all in the same directory.
141
+ #
142
+ # If the command returns with a result code that is not one of the
143
+ # expected_outcodes, a PaperclipCommandLineError will be raised. Generally
144
+ # a code of 0 is expected, but a list of codes may be passed if necessary.
145
+ #
146
+ # This method can log the command being run when
147
+ # Paperclip.options[:log_command] is set to true (defaults to false). This
148
+ # will only log if logging in general is set to true as well.
149
+ def run cmd, params = "", expected_outcodes = 0
150
+ command = %Q<#{%Q[#{path_for_command(cmd)} #{params}].gsub(/\s+/, " ")}>
151
+ command = "#{command} 2>#{bit_bucket}" if Paperclip.options[:swallow_stderr]
152
+ Paperclip.log(command) if Paperclip.options[:log_command]
153
+ output = `#{command}`
154
+ unless [expected_outcodes].flatten.include?($?.exitstatus)
155
+ raise PaperclipCommandLineError, "Error while running #{cmd}"
156
+ end
157
+ output
158
+ end
159
+
160
+ def bit_bucket #:nodoc:
161
+ File.exists?("/dev/null") ? "/dev/null" : "NUL"
162
+ end
163
+
164
+ def included base #:nodoc:
165
+ base.extend ClassMethods
166
+ unless base.respond_to?(:define_callbacks)
167
+ base.send(:include, Paperclip::CallbackCompatability)
168
+ end
169
+ end
170
+
171
+ def processor name #:nodoc:
172
+ name = name.to_s.camel_case
173
+ processor = Paperclip.const_get(name)
174
+ unless processor.ancestors.include?(Paperclip::Processor)
175
+ raise PaperclipError.new("[paperclip] Processor #{name} was not found")
176
+ end
177
+ processor
178
+ end
179
+
180
+ # Log a paperclip-specific line. Uses ActiveRecord::Base.logger
181
+ # by default. Set Paperclip.options[:log] to false to turn off.
182
+ def log message
183
+ logger.info("[paperclip] #{message}") if logging?
184
+ end
185
+
186
+ def logger #:nodoc:
187
+ DataMapper.logger
188
+ end
189
+
190
+ def logging? #:nodoc:
191
+ options[:log]
192
+ end
59
193
  end
60
194
 
61
195
  class PaperclipError < StandardError #:nodoc:
62
196
  end
63
197
 
64
- class NotIdentifiedByImageMagickError < PaperclipError #:nodoc:
198
+ class PaperclipCommandLineError < StandardError #:nodoc:
65
199
  end
66
200
 
201
+ class NotIdentifiedByImageMagickError < PaperclipError #:nodoc:
202
+ end
203
+
204
+ class InfiniteInterpolationError < PaperclipError #:nodoc:
205
+ end
206
+
67
207
  module Resource
208
+
68
209
  def self.included(base)
210
+
69
211
  base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
70
212
  class_variable_set(:@@attachment_definitions,nil) unless class_variable_defined?(:@@attachment_definitions)
71
213
  def self.attachment_definitions
@@ -76,12 +218,24 @@ module Paperclip
76
218
  @@attachment_definitions = obj
77
219
  end
78
220
  RUBY
221
+
79
222
  base.extend Paperclip::ClassMethods
223
+
224
+ # Done at this time to ensure that the user
225
+ # had a chance to configure the app in an initializer
226
+ if Paperclip.config.use_dm_validations
227
+ require 'dm-validations'
228
+ require 'dm-paperclip/validations'
229
+ base.extend Paperclip::Validate::ClassMethods
230
+ end
231
+
232
+ Paperclip.require_processors
233
+
80
234
  end
235
+
81
236
  end
82
237
 
83
238
  module ClassMethods
84
-
85
239
  # +has_attached_file+ gives the class it is called on an attribute that maps to a file. This
86
240
  # is typically a file stored somewhere on the filesystem and has been uploaded by a user.
87
241
  # The attribute returns a Paperclip::Attachment object which handles the management of
@@ -145,13 +299,17 @@ module Paperclip
145
299
 
146
300
  property_options = options.delete_if { |k,v| ![ :public, :protected, :private, :accessor, :reader, :writer ].include?(key) }
147
301
 
148
- property "#{name}_file_name".to_sym, String, property_options
149
- property "#{name}_content_type".to_sym, String, property_options
150
- property "#{name}_file_size".to_sym, Integer, property_options
151
- property "#{name}_updated_at".to_sym, DateTime, property_options
302
+ property :"#{name}_file_name", String, property_options.merge(:length => 255)
303
+ property :"#{name}_content_type", String, property_options.merge(:length => 255)
304
+ property :"#{name}_file_size", Integer, property_options
305
+ property :"#{name}_updated_at", DateTime, property_options
152
306
 
153
307
  after :save, :save_attached_files
154
308
  before :destroy, :destroy_attached_files
309
+
310
+ # not needed with extlib just do before :post_process, or after :post_process
311
+ # define_callbacks :before_post_process, :after_post_process
312
+ # define_callbacks :"before_#{name}_post_process", :"after_#{name}_post_process"
155
313
 
156
314
  define_method name do |*args|
157
315
  a = attachment_for(name)
@@ -166,46 +324,17 @@ module Paperclip
166
324
  ! attachment_for(name).original_filename.blank?
167
325
  end
168
326
 
169
- unless defined?(DataMapper::Validate).nil?
327
+ if Paperclip.config.use_dm_validations
170
328
  add_validator_to_context(opts_from_validator_args([name]), [name], Paperclip::Validate::CopyAttachmentErrors)
171
329
  end
172
- end
173
330
 
174
- unless defined?(DataMapper::Validate).nil?
175
-
176
- # Places ActiveRecord-style validations on the size of the file assigned. The
177
- # possible options are:
178
- # * +in+: a Range of bytes (i.e. +1..1.megabyte+),
179
- # * +less_than+: equivalent to :in => 0..options[:less_than]
180
- # * +greater_than+: equivalent to :in => options[:greater_than]..Infinity
181
- # * +message+: error message to display, use :min and :max as replacements
182
- def validates_attachment_size(*fields)
183
- opts = opts_from_validator_args(fields)
184
- add_validator_to_context(opts, fields, Paperclip::Validate::SizeValidator)
185
331
  end
186
332
 
187
- # Adds errors if thumbnail creation fails. The same as specifying :whiny_thumbnails => true.
188
- def validates_attachment_thumbnails name, options = {}
189
- self.attachment_definitions[name][:whiny_thumbnails] = true
333
+ # Returns the attachment definitions defined by each call to
334
+ # has_attached_file.
335
+ def attachment_definitions
336
+ read_inheritable_attribute(:attachment_definitions)
190
337
  end
191
-
192
- # Places ActiveRecord-style validations on the presence of a file.
193
- def validates_attachment_presence(*fields)
194
- opts = opts_from_validator_args(fields)
195
- add_validator_to_context(opts, fields, Paperclip::Validate::RequiredFieldValidator)
196
- end
197
-
198
- # Places ActiveRecord-style validations on the content type of the file assigned. The
199
- # possible options are:
200
- # * +content_type+: Allowed content types. Can be a single content type or an array. Allows all by default.
201
- # * +message+: The message to display when the uploaded file has an invalid content type.
202
- def validates_attachment_content_type(*fields)
203
- opts = opts_from_validator_args(fields)
204
- add_validator_to_context(opts, fields, Paperclip::Validate::ContentTypeValidator)
205
- end
206
-
207
- end
208
-
209
338
  end
210
339
 
211
340
  module InstanceMethods #:nodoc:
@@ -213,7 +342,7 @@ module Paperclip
213
342
  @attachments ||= {}
214
343
  @attachments[name] ||= Attachment.new(name, self, self.class.attachment_definitions[name])
215
344
  end
216
-
345
+
217
346
  def each_attachment
218
347
  self.class.attachment_definitions.each do |name, definition|
219
348
  yield(name, attachment_for(name))
@@ -221,21 +350,18 @@ module Paperclip
221
350
  end
222
351
 
223
352
  def save_attached_files
224
- #logger.info("[paperclip] Saving attachments.")
353
+ Paperclip.log("Saving attachments.")
225
354
  each_attachment do |name, attachment|
226
355
  attachment.send(:save)
227
356
  end
228
357
  end
229
358
 
230
359
  def destroy_attached_files
231
- #logger.info("[paperclip] Deleting attachments.")
360
+ Paperclip.log("Deleting attachments.")
232
361
  each_attachment do |name, attachment|
233
362
  attachment.send(:queue_existing_for_delete)
234
363
  attachment.send(:flush_deletes)
235
364
  end
236
365
  end
237
366
  end
238
-
239
367
  end
240
-
241
- File.send(:include, Paperclip::Upfile)
@@ -1,25 +1,27 @@
1
1
  module Paperclip
2
- # The Attachment class manages the files for a given attachment. It saves when the model saves,
3
- # deletes when the model is destroyed, and processes the file upon assignment.
2
+ # The Attachment class manages the files for a given attachment. It saves
3
+ # when the model saves, deletes when the model is destroyed, and processes
4
+ # the file upon assignment.
4
5
  class Attachment
5
-
6
+
6
7
  def self.default_options
7
8
  @default_options ||= {
8
- :url => "/:attachment/:id/:style/:basename.:extension",
9
- :path => ":merb_root/public/:attachment/:id/:style/:basename.:extension",
9
+ :url => "/system/:attachment/:id/:style/:filename",
10
+ :path => ":rails_root/public:url",
10
11
  :styles => {},
11
12
  :default_url => "/:attachment/:style/missing.png",
12
13
  :default_style => :original,
13
14
  :validations => [],
14
- :storage => :filesystem
15
+ :storage => :filesystem,
16
+ :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails]
15
17
  }
16
18
  end
17
19
 
18
- attr_reader :name, :instance, :styles, :default_style, :convert_options
20
+ attr_reader :name, :instance, :styles, :default_style, :convert_options, :queued_for_write, :options
19
21
 
20
- # Creates an Attachment object. +name+ is the name of the attachment, +instance+ is the
21
- # ActiveRecord object instance it's attached to, and +options+ is the same as the hash
22
- # passed to +has_attached_file+.
22
+ # Creates an Attachment object. +name+ is the name of the attachment,
23
+ # +instance+ is the ActiveRecord object instance it's attached to, and
24
+ # +options+ is the same as the hash passed to +has_attached_file+.
23
25
  def initialize name, instance, options = {}
24
26
  @name = name
25
27
  @instance = instance
@@ -27,96 +29,99 @@ module Paperclip
27
29
  options = self.class.default_options.merge(options)
28
30
 
29
31
  @url = options[:url]
32
+ @url = @url.call(self) if @url.is_a?(Proc)
30
33
  @path = options[:path]
34
+ @path = @path.call(self) if @path.is_a?(Proc)
31
35
  @styles = options[:styles]
36
+ @styles = @styles.call(self) if @styles.is_a?(Proc)
32
37
  @default_url = options[:default_url]
33
38
  @validations = options[:validations]
34
39
  @default_style = options[:default_style]
35
40
  @storage = options[:storage]
36
- @whiny_thumbnails = options[:whiny_thumbnails]
41
+ @whiny = options[:whiny_thumbnails] || options[:whiny]
37
42
  @convert_options = options[:convert_options] || {}
43
+ @processors = options[:processors] || [:thumbnail]
38
44
  @options = options
39
45
  @queued_for_delete = []
40
46
  @queued_for_write = {}
41
- @errors = []
47
+ @errors = {}
42
48
  @validation_errors = nil
43
49
  @dirty = false
44
50
 
45
51
  normalize_style_definition
46
52
  initialize_storage
47
-
48
- #logger.info("[paperclip] Paperclip attachment #{name} on #{instance.class} initialized.")
49
53
  end
50
54
 
51
- # What gets called when you call instance.attachment = File. It clears errors,
52
- # assigns attributes, processes the file, and runs validations. It also queues up
53
- # the previous file for deletion, to be flushed away on #save of its host.
54
- # In addition to form uploads, you can also assign another Paperclip attachment:
55
+ # What gets called when you call instance.attachment = File. It clears
56
+ # errors, assigns attributes, processes the file, and runs validations. It
57
+ # also queues up the previous file for deletion, to be flushed away on
58
+ # #save of its host. In addition to form uploads, you can also assign
59
+ # another Paperclip attachment:
55
60
  # 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.
56
63
  def assign uploaded_file
64
+
65
+ ensure_required_accessors!
66
+
57
67
  if uploaded_file.is_a?(Paperclip::Attachment)
58
68
  uploaded_file = uploaded_file.to_file(:original)
69
+ close_uploaded_file = uploaded_file.respond_to?(:close)
59
70
  end
60
71
 
61
72
  return nil unless valid_assignment?(uploaded_file)
62
- #logger.info("[paperclip] Assigning #{uploaded_file.inspect} to #{name}")
63
73
 
64
- queue_existing_for_delete
65
- @errors = []
66
- @validation_errors = nil
74
+ uploaded_file.binmode if uploaded_file.respond_to? :binmode
75
+ self.clear
67
76
 
68
77
  return nil if uploaded_file.nil?
69
78
 
70
- #logger.info("[paperclip] Writing attributes for #{name}")
71
- newvals = {}
72
- if uploaded_file.is_a?(Mash)
73
- @queued_for_write[:original] = uploaded_file['tempfile']
74
- newvals = { :"#{@name}_file_name" => uploaded_file['filename'].strip.gsub(/[^\w\d\.\-]+/, '_'),
75
- :"#{@name}_content_type" => uploaded_file['content_type'].strip,
76
- :"#{@name}_file_size" => uploaded_file['size'],
77
- :"#{@name}_updated_at" => Time.now }
79
+ if uploaded_file.respond_to?(:[])
80
+ uploaded_file = uploaded_file.to_mash
81
+
82
+ @queued_for_write[:original] = uploaded_file['tempfile']
83
+ 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)
85
+ 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)
78
87
  else
79
- @queued_for_write[:original] = uploaded_file.to_tempfile
80
- newvals = { :"#{@name}_file_name" => uploaded_file.original_filename.strip.gsub(/[^\w\d\.\-]+/, '_'),
81
- :"#{@name}_content_type" => uploaded_file.content_type.strip,
82
- :"#{@name}_file_size" => uploaded_file.size,
83
- :"#{@name}_updated_at" => Time.now }
88
+ @queued_for_write[:original] = uploaded_file.to_tempfile
89
+ instance_write(:file_name, uploaded_file.original_filename.strip.gsub(/[^\w\d\.\-]+/, '_'))
90
+ instance_write(:content_type, uploaded_file.content_type.to_s.strip)
91
+ instance_write(:file_size, uploaded_file.size.to_i)
92
+ instance_write(:updated_at, Time.now)
84
93
  end
85
94
 
86
- post_process
87
95
  @dirty = true
88
96
 
97
+ post_process if valid?
98
+
89
99
  # Reset the file size if the original file was reprocessed.
90
- #newvals[:"#{@name}_file_size"] = uploaded_file.size.to_i
91
- if @styles[:original]
92
- newvals[:"#{@name}_file_size"] = @queued_for_write[:original].size.to_i
93
- end
100
+ instance_write(:file_size, @queued_for_write[:original].size.to_i)
94
101
 
95
- begin
96
- @instance.attributes = newvals
97
- rescue NameError
98
- raise PaperclipError, "There was an error processing this attachment"
99
- end
100
102
  ensure
103
+ uploaded_file.close if close_uploaded_file
101
104
  validate
102
105
  end
103
106
 
104
- # Returns the public URL of the attachment, with a given style. Note that this
105
- # does not necessarily need to point to a file that your web server can access
106
- # and can point to an action in your app, if you need fine grained security.
107
- # This is not recommended if you don't need the security, however, for
108
- # performance reasons.
109
- def url style = default_style
110
- url = original_filename.nil? ? interpolate(@default_url, style) : interpolate(@url, style)
111
- updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url
107
+ # Returns the public URL of the attachment, with a given style. Note that
108
+ # this does not necessarily need to point to a file that your web server
109
+ # can access and can point to an action in your app, if you need fine
110
+ # 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_i].compact.join(the_url.include?("?") ? "&" : "?") : the_url
112
117
  end
113
118
 
114
119
  # Returns the path of the attachment as defined by the :path option. If the
115
- # file is stored in the filesystem the path refers to the path of the file on
116
- # disk. If the file is stored in S3, the path is the "key" part of the URL,
117
- # and the :bucket option refers to the S3 bucket.
118
- def path style = nil #:nodoc:
119
- interpolate(@path, style)
120
+ # file is stored in the filesystem the path refers to the path of the file
121
+ # on disk. If the file is stored in S3, the path is the "key" part of the
122
+ # URL, and the :bucket option refers to the S3 bucket.
123
+ def path style = default_style
124
+ original_filename.nil? ? nil : interpolate(@path, style)
120
125
  end
121
126
 
122
127
  # Alias to +url+
@@ -126,12 +131,13 @@ module Paperclip
126
131
 
127
132
  # Returns true if there are no errors on this attachment.
128
133
  def valid?
129
- @errors.length == 0
134
+ validate
135
+ errors.empty?
130
136
  end
131
137
 
132
138
  # Returns an array containing the errors on this attachment.
133
139
  def errors
134
- @errors.compact.uniq
140
+ @errors
135
141
  end
136
142
 
137
143
  # Returns true if there are changes that need to be saved.
@@ -143,67 +149,76 @@ module Paperclip
143
149
  # the instance's errors and returns false, cancelling the save.
144
150
  def save
145
151
  if valid?
146
- #logger.info("[paperclip] Saving files for #{name}")
147
152
  flush_deletes
148
153
  flush_writes
149
154
  @dirty = false
150
155
  true
151
156
  else
152
- #logger.info("[paperclip] Errors on #{name}. Not saving.")
153
157
  flush_errors
154
158
  false
155
159
  end
156
160
  end
157
161
 
158
- # Returns the name of the file as originally assigned, and as lives in the
162
+ # Clears out the attachment. Has the same effect as previously assigning
163
+ # nil to the attachment. Does NOT save. If you wish to clear AND save,
164
+ # use #destroy.
165
+ def clear
166
+ queue_existing_for_delete
167
+ @errors = {}
168
+ @validation_errors = nil
169
+ end
170
+
171
+ # Destroys the attachment. Has the same effect as previously assigning
172
+ # nil to the attachment *and saving*. This is permanent. If you wish to
173
+ # wipe out the existing attachment but not save, use #clear.
174
+ def destroy
175
+ clear
176
+ save
177
+ end
178
+
179
+ # Returns the name of the file as originally assigned, and lives in the
159
180
  # <attachment>_file_name attribute of the model.
160
181
  def original_filename
161
- begin
162
- @instance.attribute_get(:"#{name}_file_name")
163
- rescue ArgumentError
164
- nil
165
- end
182
+ instance_read(:file_name)
183
+ end
184
+
185
+ # Returns the size of the file as originally assigned, and lives in the
186
+ # <attachment>_file_size attribute of the model.
187
+ def size
188
+ instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size)
166
189
  end
167
-
190
+
191
+ # Returns the content_type of the file as originally assigned, and lives
192
+ # in the <attachment>_content_type attribute of the model.
193
+ def content_type
194
+ instance_read(:content_type)
195
+ end
196
+
197
+ # Returns the last modified time of the file as originally assigned, and
198
+ # lives in the <attachment>_updated_at attribute of the model.
168
199
  def updated_at
169
- time = @instance.attribute_get(:"#{name}_updated_at")
170
- time && "#{time.year}#{time.month}#{time.day}#{time.hour}#{time.min}#{time.sec}"
200
+ instance_read(:updated_at)
171
201
  end
172
202
 
173
- # A hash of procs that are run during the interpolation of a path or url.
174
- # A variable of the format :name will be replaced with the return value of
175
- # the proc named ":name". Each lambda takes the attachment and the current
176
- # style as arguments. This hash can be added to with your own proc if
177
- # necessary.
203
+ # Paths and URLs can have a number of variables interpolated into them
204
+ # to vary the storage location based on name, id, style, class, etc.
205
+ # This method is a deprecated access into supplying and retrieving these
206
+ # interpolations. Future access should use either Paperclip.interpolates
207
+ # or extend the Paperclip::Interpolations module directly.
178
208
  def self.interpolations
179
- @interpolations ||= {
180
- :merb_root => lambda{|attachment,style| Merb.root },
181
- :merb_env => lambda{|attachment,style| Merb.env },
182
- :class => lambda do |attachment,style|
183
- underscore(attachment.instance.class.name.pluralize)
184
- end,
185
- :basename => lambda do |attachment,style|
186
- attachment.original_filename.gsub(File.extname(attachment.original_filename), "")
187
- end,
188
- :extension => lambda do |attachment,style|
189
- ((style = attachment.styles[style]) && style.last) ||
190
- File.extname(attachment.original_filename).gsub(/^\.+/, "")
191
- end,
192
- :id => lambda{|attachment,style| attachment.instance.id },
193
- :id_partition => lambda do |attachment, style|
194
- ("%09d" % attachment.instance.id).scan(/\d{3}/).join("/")
195
- end,
196
- :attachment => lambda{|attachment,style| attachment.name.to_s.downcase.pluralize },
197
- :style => lambda{|attachment,style| style || attachment.default_style },
198
- }
209
+ warn('[DEPRECATION] Paperclip::Attachment.interpolations is deprecated ' +
210
+ 'and will be removed from future versions. ' +
211
+ 'Use Paperclip.interpolates instead')
212
+ Paperclip::Interpolations
199
213
  end
200
214
 
201
- # This method really shouldn't be called that often. It's expected use is in the
202
- # paperclip:refresh rake task and that's it. It will regenerate all thumbnails
203
- # forcefully, by reobtaining the original file and going through the post-process
204
- # again.
215
+ # This method really shouldn't be called that often. It's expected use is
216
+ # in the paperclip:refresh rake task and that's it. It will regenerate all
217
+ # thumbnails forcefully, by reobtaining the original file and going through
218
+ # the post-process again.
205
219
  def reprocess!
206
220
  new_original = Tempfile.new("paperclip-reprocess")
221
+ new_original.binmode
207
222
  if old_original = to_file(:original)
208
223
  new_original.write( old_original.read )
209
224
  new_original.rewind
@@ -218,107 +233,183 @@ module Paperclip
218
233
  true
219
234
  end
220
235
  end
221
-
236
+
237
+ # Returns true if a file has been assigned.
222
238
  def file?
223
239
  !original_filename.blank?
224
240
  end
225
241
 
242
+ # Writes the attachment-specific attribute on the instance. For example,
243
+ # instance_write(:file_name, "me.jpg") will write "me.jpg" to the instance's
244
+ # "avatar_file_name" field (assuming the attachment is called avatar).
245
+ def instance_write(attr, value)
246
+ setter = :"#{name}_#{attr}="
247
+ responds = instance.respond_to?(setter)
248
+ self.instance_variable_set("@_#{setter.to_s.chop}", value)
249
+ instance.send(setter, value) if responds || attr.to_s == "file_name"
250
+ end
251
+
252
+ # Reads the attachment-specific attribute on the instance. See instance_write
253
+ # for more details.
254
+ def instance_read(attr)
255
+ getter = :"#{name}_#{attr}"
256
+ responds = instance.respond_to?(getter)
257
+ cached = self.instance_variable_get("@_#{getter}")
258
+ return cached if cached
259
+ instance.send(getter) if responds || attr.to_s == "file_name"
260
+ end
261
+
226
262
  private
227
263
 
228
- def self.underscore(camel_cased_word)
229
- camel_cased_word.to_s.gsub(/::/, '/').
230
- gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
231
- gsub(/([a-z\d])([A-Z])/,'\1_\2').
232
- tr("-", "_").
233
- downcase
264
+ def ensure_required_accessors! #:nodoc:
265
+ %w(file_name).each do |field|
266
+ unless @instance.respond_to?("#{name}_#{field}") && @instance.respond_to?("#{name}_#{field}=")
267
+ raise PaperclipError.new("#{@instance.class} model missing required attr_accessor for '#{name}_#{field}'")
268
+ end
269
+ end
234
270
  end
235
271
 
236
- def logger
237
- instance.logger
272
+ def log message #:nodoc:
273
+ Paperclip.log(message)
238
274
  end
239
275
 
240
276
  def valid_assignment? file #:nodoc:
241
- return true if file.nil?
242
- if(file.is_a?(File))
243
- (file.respond_to?(:original_filename) && file.respond_to?(:content_type))
244
- elsif(file.is_a?(Mash))
245
- (file.include?('tempfile') && file.include?('content_type') && file.include?('size') && file.include?('filename'))
277
+ if file.respond_to?(:[])
278
+ file[:filename] || file['filename']
279
+ else
280
+ file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type))
246
281
  end
247
282
  end
248
283
 
249
284
  def validate #:nodoc:
250
285
  unless @validation_errors
251
- @validation_errors = @validations.collect do |v|
252
- v.call(self, @instance)
253
- end.flatten.compact.uniq
254
- @errors += @validation_errors
286
+ @validation_errors = @validations.inject({}) do |errors, validation|
287
+ name, options = validation
288
+ errors[name] = send(:"validate_#{name}", options) if allow_validation?(options)
289
+ errors
290
+ end
291
+ @validation_errors.reject!{|k,v| v == nil }
292
+ @errors.merge!(@validation_errors)
255
293
  end
256
294
  @validation_errors
257
295
  end
258
296
 
259
- def normalize_style_definition
297
+ def allow_validation? options #:nodoc:
298
+ (options[:if].nil? || check_guard(options[:if])) && (options[:unless].nil? || !check_guard(options[:unless]))
299
+ end
300
+
301
+ def check_guard guard #:nodoc:
302
+ if guard.respond_to? :call
303
+ guard.call(instance)
304
+ elsif ! guard.blank?
305
+ instance.send(guard.to_s)
306
+ end
307
+ end
308
+
309
+ def validate_size options #:nodoc:
310
+ if file? && !options[:range].include?(size.to_i)
311
+ options[:message].gsub(/:min/, options[:min].to_s).gsub(/:max/, options[:max].to_s)
312
+ end
313
+ end
314
+
315
+ def validate_presence options #:nodoc:
316
+ options[:message] unless file?
317
+ end
318
+
319
+ def validate_content_type options #:nodoc:
320
+ valid_types = [options[:content_type]].flatten
321
+ unless original_filename.blank?
322
+ unless valid_types.blank?
323
+ content_type = instance_read(:content_type)
324
+ unless valid_types.any?{|t| content_type.nil? || t === content_type }
325
+ options[:message] || "is not one of the allowed file types."
326
+ end
327
+ end
328
+ end
329
+ end
330
+
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:
260
354
  @styles.each do |name, args|
261
- dimensions, format = [args, nil].flatten[0..1]
262
- format = nil if format == ""
263
- @styles[name] = [dimensions, format]
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)
264
357
  end
265
358
  end
266
359
 
267
- def initialize_storage
360
+ def initialize_storage #:nodoc:
268
361
  @storage_module = Paperclip::Storage.const_get(@storage.to_s.capitalize)
269
362
  self.extend(@storage_module)
270
363
  end
271
364
 
272
365
  def extra_options_for(style) #:nodoc:
273
- [ convert_options[style], convert_options[:all] ].compact.join(" ")
366
+ all_options = convert_options[:all]
367
+ all_options = all_options.call(instance) if all_options.respond_to?(:call)
368
+ style_options = convert_options[style]
369
+ style_options = style_options.call(instance) if style_options.respond_to?(:call)
370
+
371
+ [ style_options, all_options ].compact.join(" ")
274
372
  end
275
373
 
276
374
  def post_process #:nodoc:
277
375
  return if @queued_for_write[:original].nil?
278
- #logger.info("[paperclip] Post-processing #{name}")
376
+ solidify_style_definitions
377
+ post_process_styles
378
+ end
379
+
380
+ def post_process_styles #:nodoc:
279
381
  @styles.each do |name, args|
280
382
  begin
281
- dimensions, format = args
282
- dimensions = dimensions.call(instance) if dimensions.respond_to? :call
283
- @queued_for_write[name] = Thumbnail.make(@queued_for_write[:original],
284
- dimensions,
285
- format,
286
- extra_options_for(name),
287
- @whiny_thumnails)
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)
386
+ end
288
387
  rescue PaperclipError => e
289
- @errors << e.message if @whiny_thumbnails
388
+ log("An error was received while processing: #{e.inspect}")
389
+ (@errors[:processing] ||= []) << e.message if @whiny
290
390
  end
291
391
  end
292
392
  end
293
393
 
294
394
  def interpolate pattern, style = default_style #:nodoc:
295
- interpolations = self.class.interpolations.sort{|a,b| a.first.to_s <=> b.first.to_s }
296
- interpolations.reverse.inject( pattern.dup ) do |result, interpolation|
297
- tag, blk = interpolation
298
- result.gsub(/:#{tag}/) do |match|
299
- blk.call( self, style )
300
- end
301
- end
395
+ Paperclip::Interpolations.interpolate(pattern, self, style)
302
396
  end
303
397
 
304
398
  def queue_existing_for_delete #:nodoc:
305
399
  return unless file?
306
- #logger.info("[paperclip] Queueing the existing files for #{name} for deletion.")
307
400
  @queued_for_delete += [:original, *@styles.keys].uniq.map do |style|
308
401
  path(style) if exists?(style)
309
402
  end.compact
310
- newvals = { :"#{@name}_file_name" => nil,
311
- :"#{@name}_content_type" => nil,
312
- :"#{@name}_file_size" => nil }
313
- @instance.attributes = newvals
403
+ instance_write(:file_name, nil)
404
+ instance_write(:content_type, nil)
405
+ instance_write(:file_size, nil)
406
+ instance_write(:updated_at, nil)
314
407
  end
315
408
 
316
409
  def flush_errors #:nodoc:
317
- @errors.each do |error|
318
- @instance.errors.add(name, error)
410
+ @errors.each do |error, message|
411
+ [message].flatten.each {|m| instance.errors.add(name, m) }
319
412
  end
320
413
  end
321
-
322
414
  end
323
415
  end
324
-