dm-paperclip 2.1.4 → 2.3.0

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.
@@ -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
-