paperclip 2.4.5 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of paperclip might be problematic. Click here for more details.

Files changed (63) hide show
  1. data/.gitignore +22 -0
  2. data/.travis.yml +13 -0
  3. data/Appraisals +14 -0
  4. data/CONTRIBUTING.md +38 -0
  5. data/Gemfile +5 -0
  6. data/NEWS +23 -0
  7. data/README.md +72 -42
  8. data/Rakefile +1 -46
  9. data/cucumber/paperclip_steps.rb +6 -0
  10. data/features/basic_integration.feature +46 -0
  11. data/features/rake_tasks.feature +63 -0
  12. data/features/step_definitions/attachment_steps.rb +65 -0
  13. data/features/step_definitions/html_steps.rb +15 -0
  14. data/features/step_definitions/rails_steps.rb +182 -0
  15. data/features/step_definitions/s3_steps.rb +14 -0
  16. data/features/step_definitions/web_steps.rb +209 -0
  17. data/features/support/env.rb +8 -0
  18. data/features/support/fakeweb.rb +3 -0
  19. data/features/support/fixtures/.boot_config.rb.swo +0 -0
  20. data/features/support/fixtures/boot_config.txt +15 -0
  21. data/features/support/fixtures/gemfile.txt +5 -0
  22. data/features/support/fixtures/preinitializer.txt +20 -0
  23. data/features/support/paths.rb +28 -0
  24. data/features/support/rails.rb +46 -0
  25. data/features/support/selectors.rb +19 -0
  26. data/gemfiles/rails2.gemfile +9 -0
  27. data/gemfiles/rails3.gemfile +9 -0
  28. data/gemfiles/rails3_1.gemfile +9 -0
  29. data/lib/paperclip.rb +26 -19
  30. data/lib/paperclip/attachment.rb +123 -109
  31. data/lib/paperclip/interpolations.rb +7 -4
  32. data/lib/paperclip/matchers.rb +33 -2
  33. data/lib/paperclip/missing_attachment_styles.rb +1 -1
  34. data/lib/paperclip/railtie.rb +5 -0
  35. data/lib/paperclip/schema.rb +39 -0
  36. data/lib/paperclip/storage/fog.rb +21 -10
  37. data/lib/paperclip/storage/s3.rb +107 -40
  38. data/lib/paperclip/style.rb +13 -5
  39. data/lib/paperclip/url_generator.rb +64 -0
  40. data/lib/paperclip/version.rb +1 -1
  41. data/lib/tasks/paperclip.rake +1 -1
  42. data/paperclip.gemspec +41 -0
  43. data/test/.gitignore +1 -0
  44. data/test/attachment_test.rb +155 -168
  45. data/test/fixtures/question?mark.png +0 -0
  46. data/test/helper.rb +24 -1
  47. data/test/interpolations_test.rb +16 -2
  48. data/test/paperclip_missing_attachment_styles_test.rb +16 -0
  49. data/test/paperclip_test.rb +72 -22
  50. data/test/schema_test.rb +98 -0
  51. data/test/storage/filesystem_test.rb +2 -2
  52. data/test/{fog_test.rb → storage/fog_test.rb} +35 -8
  53. data/test/storage/s3_live_test.rb +63 -13
  54. data/test/storage/s3_test.rb +394 -91
  55. data/test/style_test.rb +50 -21
  56. data/test/support/mock_attachment.rb +22 -0
  57. data/test/support/mock_interpolator.rb +24 -0
  58. data/test/support/mock_model.rb +2 -0
  59. data/test/support/mock_url_generator_builder.rb +27 -0
  60. data/test/url_generator_test.rb +187 -0
  61. metadata +307 -125
  62. data/lib/paperclip/options.rb +0 -78
  63. data/test/options_test.rb +0 -75
@@ -0,0 +1,8 @@
1
+ require 'aruba/cucumber'
2
+ require 'capybara/cucumber'
3
+ require 'test/unit/assertions'
4
+ World(Test::Unit::Assertions)
5
+
6
+ Before do
7
+ @aruba_timeout_seconds = 120
8
+ end
@@ -0,0 +1,3 @@
1
+ require 'fake_web'
2
+
3
+ FakeWeb.allow_net_connect = false
@@ -0,0 +1,15 @@
1
+ class Rails::Boot
2
+ def run
3
+ load_initializer
4
+
5
+ Rails::Initializer.class_eval do
6
+ def load_gems
7
+ @bundler_loaded ||= Bundler.require :default, Rails.env
8
+ end
9
+ end
10
+
11
+ Rails::Initializer.run(:set_load_path)
12
+ end
13
+ end
14
+
15
+ Rails.boot!
@@ -0,0 +1,5 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "rails", "RAILS_VERSION"
4
+ gem "rdoc"
5
+ gem "sqlite3"
@@ -0,0 +1,20 @@
1
+ begin
2
+ require "rubygems"
3
+ require "bundler"
4
+ rescue LoadError
5
+ raise "Could not load the bundler gem. Install it with `gem install bundler`."
6
+ end
7
+
8
+ if Gem::Version.new(Bundler::VERSION) <= Gem::Version.new("0.9.24")
9
+ raise RuntimeError, "Your bundler version is too old for Rails 2.3." +
10
+ "Run `gem install bundler` to upgrade."
11
+ end
12
+
13
+ begin
14
+ # Set up load paths for all bundled gems
15
+ ENV["BUNDLE_GEMFILE"] = File.expand_path("../../Gemfile", __FILE__)
16
+ Bundler.setup
17
+ rescue Bundler::GemNotFound
18
+ raise RuntimeError, "Bundler couldn't find some gems." +
19
+ "Did you run `bundle install`?"
20
+ end
@@ -0,0 +1,28 @@
1
+ module NavigationHelpers
2
+ # Maps a name to a path. Used by the
3
+ #
4
+ # When /^I go to (.+)$/ do |page_name|
5
+ #
6
+ # step definition in web_steps.rb
7
+ #
8
+ def path_to(page_name)
9
+ case page_name
10
+
11
+ when /the home\s?page/
12
+ '/'
13
+ when /the new user page/
14
+ '/users/new'
15
+ else
16
+ begin
17
+ page_name =~ /the (.*) page/
18
+ path_components = $1.split(/\s+/)
19
+ self.send(path_components.push('path').join('_').to_sym)
20
+ rescue Object => e
21
+ raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
22
+ "Now, go and add a mapping in #{__FILE__}"
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ World(NavigationHelpers)
@@ -0,0 +1,46 @@
1
+ PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..', '..')).freeze
2
+ APP_NAME = 'testapp'.freeze
3
+ BUNDLE_ENV_VARS = %w(RUBYOPT BUNDLE_PATH BUNDLE_BIN_PATH BUNDLE_GEMFILE)
4
+ ORIGINAL_BUNDLE_VARS = Hash[ENV.select{ |key,value| BUNDLE_ENV_VARS.include?(key) }]
5
+
6
+ ENV['RAILS_ENV'] = 'test'
7
+
8
+ Before do
9
+ ENV['BUNDLE_GEMFILE'] = File.join(Dir.pwd, ENV['BUNDLE_GEMFILE']) unless ENV['BUNDLE_GEMFILE'].start_with?(Dir.pwd)
10
+ @framework_version = nil
11
+ end
12
+
13
+ After do
14
+ ORIGINAL_BUNDLE_VARS.each_pair do |key, value|
15
+ ENV[key] = value
16
+ end
17
+ end
18
+
19
+ When /^I reset Bundler environment variable$/ do
20
+ BUNDLE_ENV_VARS.each do |key|
21
+ ENV[key] = nil
22
+ end
23
+ end
24
+
25
+ module RailsCommandHelpers
26
+ def framework_version?(version_string)
27
+ framework_version =~ /^#{version_string}/
28
+ end
29
+
30
+ def framework_version
31
+ @framework_version ||= `rails -v`[/^Rails (.+)$/, 1]
32
+ end
33
+
34
+ def new_application_command
35
+ framework_version?("3") ? "rails new" : "rails"
36
+ end
37
+
38
+ def generator_command
39
+ framework_version?("3") ? "script/rails generate" : "script/generate"
40
+ end
41
+
42
+ def runner_command
43
+ framework_version?("3") ? "script/rails runner" : "script/runner"
44
+ end
45
+ end
46
+ World(RailsCommandHelpers)
@@ -0,0 +1,19 @@
1
+ module HtmlSelectorsHelpers
2
+ # Maps a name to a selector. Used primarily by the
3
+ #
4
+ # When /^(.+) within (.+)$/ do |step, scope|
5
+ #
6
+ # step definitions in web_steps.rb
7
+ #
8
+ def selector_for(locator)
9
+ case locator
10
+ when "the page"
11
+ "html > body"
12
+ else
13
+ raise "Can't find mapping from \"#{locator}\" to a selector.\n" +
14
+ "Now, go and add a mapping in #{__FILE__}"
15
+ end
16
+ end
17
+ end
18
+
19
+ World(HtmlSelectorsHelpers)
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "jruby-openssl", :platform=>:jruby
6
+ gem "rails", "~> 2.3.14"
7
+ gem "paperclip", :path=>"../"
8
+
9
+ gemspec :path=>"../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "jruby-openssl", :platform=>:jruby
6
+ gem "rails", "~> 3.0.10"
7
+ gem "paperclip", :path=>"../"
8
+
9
+ gemspec :path=>"../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "jruby-openssl", :platform=>:jruby
6
+ gem "rails", "~> 3.1.0"
7
+ gem "paperclip", :path=>"../"
8
+
9
+ gemspec :path=>"../"
@@ -28,7 +28,6 @@
28
28
  require 'erb'
29
29
  require 'digest'
30
30
  require 'tempfile'
31
- require 'paperclip/options'
32
31
  require 'paperclip/version'
33
32
  require 'paperclip/upfile'
34
33
  require 'paperclip/iostream'
@@ -50,7 +49,7 @@ require 'cocaine'
50
49
  module Paperclip
51
50
 
52
51
  class << self
53
- # Provides configurability to Paperclip. There are a number of options available, such as:
52
+ # Provides configurability to Paperclip. The options available are:
54
53
  # * whiny: Will raise an error if Paperclip cannot process thumbnails of
55
54
  # an uploaded image. Defaults to true.
56
55
  # * log: Logs progress to the Rails log. Uses ActiveRecord's logger, so honors
@@ -140,8 +139,11 @@ module Paperclip
140
139
  # Find all instances of the given Active Record model +klass+ with attachment +name+.
141
140
  # This method is used by the refresh rake tasks.
142
141
  def each_instance_with_attachment(klass, name)
143
- class_for(klass).find(:all, :order => 'id').each do |instance|
144
- yield(instance) if instance.send(:"#{name}?")
142
+ unscope_method = class_for(klass).respond_to?(:unscoped) ? :unscoped : :with_exclusive_scope
143
+ class_for(klass).send(unscope_method) do
144
+ class_for(klass).find(:all, :order => 'id').each do |instance|
145
+ yield(instance) if instance.send(:"#{name}?")
146
+ end
145
147
  end
146
148
  end
147
149
 
@@ -177,9 +179,9 @@ module Paperclip
177
179
  end
178
180
  end
179
181
  rescue ArgumentError => e
180
- # Sadly, we need to capture ArguementError here because Rails 2.3.x
181
- # Active Support dependency's management will try to the constant inherited
182
- # from Object, and fail misably with "Object is not missing constant X" error
182
+ # Sadly, we need to capture ArgumentError here because Rails 2.3.x
183
+ # ActiveSupport dependency management will try to the constant inherited
184
+ # from Object, and fail miserably with "Object is not missing constant X" error
183
185
  # https://github.com/rails/rails/blob/v2.3.12/activesupport/lib/active_support/dependencies.rb#L124
184
186
  if e.message =~ /is not missing constant/
185
187
  raise NameError, "uninitialized constant #{class_name}"
@@ -191,7 +193,7 @@ module Paperclip
191
193
  def check_for_url_clash(name,url,klass)
192
194
  @names_url ||= {}
193
195
  default_url = url || Attachment.default_options[:url]
194
- if @names_url[name] && @names_url[name][:url] == default_url && @names_url[name][:class] != klass
196
+ if @names_url[name] && @names_url[name][:url] == default_url && @names_url[name][:class] != klass && @names_url[name][:url] !~ /:class/
195
197
  log("Duplicate URL for #{name} with #{default_url}. This will clash with attachment defined in #{@names_url[name][:class]} class")
196
198
  end
197
199
  @names_url[name] = {:url => default_url, :class => klass}
@@ -264,6 +266,9 @@ module Paperclip
264
266
  # has_attached_file :avatar, :styles => { :normal => "100x100#" },
265
267
  # :default_style => :normal
266
268
  # user.avatar.url # => "/avatars/23/normal_me.png"
269
+ # * +keep_old_files+: Keep the existing attachment files (original + resized) from
270
+ # being automatically deleted when an attachment is cleared or updated.
271
+ # Defaults to +false+.#
267
272
  # * +whiny+: Will raise an error if Paperclip cannot post_process an uploaded file due
268
273
  # to a command line error. This will override the global setting for this attachment.
269
274
  # Defaults to true. This option used to be called :whiny_thumbanils, but this is
@@ -290,11 +295,11 @@ module Paperclip
290
295
  # shell quoting for safety. If your options require a space, please pre-split them
291
296
  # and pass an array to :convert_options instead.
292
297
  # * +storage+: Chooses the storage backend where the files will be stored. The current
293
- # choices are :filesystem and :s3. The default is :filesystem. Make sure you read the
294
- # documentation for Paperclip::Storage::Filesystem and Paperclip::Storage::S3
298
+ # choices are :filesystem, :fog and :s3. The default is :filesystem. Make sure you read the
299
+ # documentation for Paperclip::Storage::Filesystem, Paperclip::Storage::Fog and Paperclip::Storage::S3
295
300
  # for backend-specific options.
296
301
  #
297
- # It's also possible for you to dynamicly define your interpolation string for :url,
302
+ # It's also possible for you to dynamically define your interpolation string for :url,
298
303
  # :default_url, and :path in your model by passing a method name as a symbol as a argument
299
304
  # for your has_attached_file definition:
300
305
  #
@@ -316,6 +321,8 @@ module Paperclip
316
321
  else
317
322
  write_inheritable_attribute(:attachment_definitions, {})
318
323
  end
324
+ else
325
+ self.attachment_definitions = self.attachment_definitions.dup
319
326
  end
320
327
 
321
328
  attachment_definitions[name] = {:validations => []}.merge(options)
@@ -353,8 +360,8 @@ module Paperclip
353
360
  # * +less_than+: equivalent to :in => 0..options[:less_than]
354
361
  # * +greater_than+: equivalent to :in => options[:greater_than]..Infinity
355
362
  # * +message+: error message to display, use :min and :max as replacements
356
- # * +if+: A lambda or name of a method on the instance. Validation will only
357
- # be run is this lambda or method returns true.
363
+ # * +if+: A lambda or name of an instance method. Validation will only
364
+ # be run if this lambda or method returns true.
358
365
  # * +unless+: Same as +if+ but validates if lambda or method returns false.
359
366
  def validates_attachment_size name, options = {}
360
367
  min = options[:greater_than] || (options[:in] && options[:in].first) || 0
@@ -382,14 +389,14 @@ module Paperclip
382
389
 
383
390
  # Places ActiveRecord-style validations on the presence of a file.
384
391
  # Options:
385
- # * +if+: A lambda or name of a method on the instance. Validation will only
386
- # be run is this lambda or method returns true.
392
+ # * +if+: A lambda or name of an instance method. Validation will only
393
+ # be run if this lambda or method returns true.
387
394
  # * +unless+: Same as +if+ but validates if lambda or method returns false.
388
395
  def validates_attachment_presence name, options = {}
389
396
  message = options[:message] || :empty
390
397
  validates_each :"#{name}_file_name" do |record, attr, value|
391
- if_clause_passed = options[:if].nil? || (options[:if].call(record) != false)
392
- unless_clause_passed = options[:unless].nil? || (!!options[:unless].call(record) == false)
398
+ if_clause_passed = options[:if].nil? || (options[:if].respond_to?(:call) ? options[:if].call(record) != false : record.send(options[:if]))
399
+ unless_clause_passed = options[:unless].nil? || (options[:unless].respond_to?(:call) ? !!options[:unless].call(record) == false : !record.send(options[:unless]))
393
400
  if if_clause_passed && unless_clause_passed && value.blank?
394
401
  record.errors.add(name, message)
395
402
  record.errors.add("#{name}_file_name", message)
@@ -401,13 +408,13 @@ module Paperclip
401
408
  # assigned. The possible options are:
402
409
  # * +content_type+: Allowed content types. Can be a single content type
403
410
  # or an array. Each type can be a String or a Regexp. It should be
404
- # noted that Internet Explorer upload files with content_types that you
411
+ # noted that Internet Explorer uploads files with content_types that you
405
412
  # may not expect. For example, JPEG images are given image/pjpeg and
406
413
  # PNGs are image/x-png, so keep that in mind when determining how you
407
414
  # match. Allows all by default.
408
415
  # * +message+: The message to display when the uploaded file has an invalid
409
416
  # content type.
410
- # * +if+: A lambda or name of a method on the instance. Validation will only
417
+ # * +if+: A lambda or name of an instance method. Validation will only
411
418
  # be run is this lambda or method returns true.
412
419
  # * +unless+: Same as +if+ but validates if lambda or method returns false.
413
420
  # NOTE: If you do not specify an [attachment]_content_type field on your
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
  require 'uri'
3
+ require 'paperclip/url_generator'
3
4
 
4
5
  module Paperclip
5
6
  # The Attachment class manages the files for a given attachment. It saves
@@ -25,11 +26,14 @@ module Paperclip
25
26
  :use_default_time_zone => true,
26
27
  :hash_digest => "SHA1",
27
28
  :hash_data => ":class/:attachment/:id/:style/:updated_at",
28
- :preserve_files => false
29
+ :preserve_files => false,
30
+ :interpolator => Paperclip::Interpolations,
31
+ :url_generator => Paperclip::UrlGenerator
29
32
  }
30
33
  end
31
34
 
32
35
  attr_reader :name, :instance, :default_style, :convert_options, :queued_for_write, :whiny, :options, :interpolator
36
+ attr_reader :source_file_options, :whiny
33
37
  attr_accessor :post_processing
34
38
 
35
39
  # Creates an Attachment object. +name+ is the name of the attachment,
@@ -38,49 +42,45 @@ module Paperclip
38
42
  #
39
43
  # Options include:
40
44
  #
41
- # +url+ - a relative URL of the attachment. This is interpolated using +interpolator+
42
- # +path+ - where on the filesystem to store the attachment. This is interpolated using +interpolator+
43
- # +styles+ - a hash of options for processing the attachment. See +has_attached_file+ for the details
44
- # +only_process+ - style args to be run through the post-processor. This defaults to the empty list
45
- # +default_url+ - a URL for the missing image
46
- # +default_style+ - the style to use when don't specify an argument to e.g. #url, #path
47
- # +storage+ - the storage mechanism. Defaults to :filesystem
48
- # +use_timestamp+ - whether to append an anti-caching timestamp to image URLs. Defaults to true
49
- # +whiny+, +whiny_thumbnails+ - whether to raise when thumbnailing fails
50
- # +use_default_time_zone+ - related to +use_timestamp+. Defaults to true
51
- # +hash_digest+ - a string representing a class that will be used to hash URLs for obfuscation
52
- # +hash_data+ - the relative URL for the hash data. This is interpolated using +interpolator+
53
- # +hash_secret+ - a secret passed to the +hash_digest+
54
- # +convert_options+ - flags passed to the +convert+ command for processing
55
- # +source_file_options+ - flags passed to the +convert+ command that controls how the file is read
56
- # +processors+ - classes that transform the attachment. Defaults to [:thumbnail]
57
- # +preserve_files+ - whether to keep files on the filesystem when deleting to clearing the attachment. Defaults to false
58
- # +interpolator+ - the object used to interpolate filenames and URLs. Defaults to Paperclip::Interpolations
59
- def initialize name, instance, options = {}
45
+ # +url+ - a relative URL of the attachment. This is interpolated using +interpolator+
46
+ # +path+ - where on the filesystem to store the attachment. This is interpolated using +interpolator+
47
+ # +styles+ - a hash of options for processing the attachment. See +has_attached_file+ for the details
48
+ # +only_process+ - style args to be run through the post-processor. This defaults to the empty list
49
+ # +default_url+ - a URL for the missing image
50
+ # +default_style+ - the style to use when don't specify an argument to e.g. #url, #path
51
+ # +storage+ - the storage mechanism. Defaults to :filesystem
52
+ # +use_timestamp+ - whether to append an anti-caching timestamp to image URLs. Defaults to true
53
+ # +whiny+, +whiny_thumbnails+ - whether to raise when thumbnailing fails
54
+ # +use_default_time_zone+ - related to +use_timestamp+. Defaults to true
55
+ # +hash_digest+ - a string representing a class that will be used to hash URLs for obfuscation
56
+ # +hash_data+ - the relative URL for the hash data. This is interpolated using +interpolator+
57
+ # +hash_secret+ - a secret passed to the +hash_digest+
58
+ # +convert_options+ - flags passed to the +convert+ command for processing
59
+ # +source_file_options+ - flags passed to the +convert+ command that controls how the file is read
60
+ # +processors+ - classes that transform the attachment. Defaults to [:thumbnail]
61
+ # +preserve_files+ - whether to keep files on the filesystem when deleting to clearing the attachment. Defaults to false
62
+ # +interpolator+ - the object used to interpolate filenames and URLs. Defaults to Paperclip::Interpolations
63
+ # +url_generator+ - the object used to generate URLs, using the interpolator. Defaults to Paperclip::UrlGenerator
64
+ def initialize(name, instance, options = {})
60
65
  @name = name
61
66
  @instance = instance
62
67
 
63
68
  options = self.class.default_options.merge(options)
64
69
 
65
- @options = Paperclip::Options.new(self, options)
70
+ @options = options
66
71
  @post_processing = true
67
72
  @queued_for_delete = []
68
73
  @queued_for_write = {}
69
74
  @errors = {}
70
75
  @dirty = false
71
- @interpolator = (options[:interpolator] || Paperclip::Interpolations)
76
+ @interpolator = options[:interpolator]
77
+ @url_generator = options[:url_generator].new(self, @options)
78
+ @source_file_options = options[:source_file_options]
79
+ @whiny = options[:whiny]
72
80
 
73
81
  initialize_storage
74
82
  end
75
83
 
76
- # [:url, :path, :only_process, :normalized_styles, :default_url, :default_style,
77
- # :storage, :use_timestamp, :whiny, :use_default_time_zone, :hash_digest, :hash_secret,
78
- # :convert_options, :preserve_files].each do |field|
79
- # define_method field do
80
- # @options.send(field)
81
- # end
82
- # end
83
-
84
84
  # What gets called when you call instance.attachment = File. It clears
85
85
  # errors, assigns attributes, and processes the file. It
86
86
  # also queues up the previous file for deletion, to be flushed away on
@@ -106,37 +106,55 @@ module Paperclip
106
106
  return nil if uploaded_file.nil?
107
107
 
108
108
  uploaded_filename ||= uploaded_file.original_filename
109
+ stores_fingerprint = @instance.respond_to?("#{name}_fingerprint".to_sym)
109
110
  @queued_for_write[:original] = to_tempfile(uploaded_file)
110
111
  instance_write(:file_name, uploaded_filename.strip)
111
112
  instance_write(:content_type, uploaded_file.content_type.to_s.strip)
112
113
  instance_write(:file_size, uploaded_file.size.to_i)
113
- instance_write(:fingerprint, generate_fingerprint(uploaded_file))
114
+ instance_write(:fingerprint, generate_fingerprint(uploaded_file)) if stores_fingerprint
114
115
  instance_write(:updated_at, Time.now)
115
116
 
116
117
  @dirty = true
117
118
 
118
- post_process(*@options.only_process) if post_processing
119
+ post_process(*@options[:only_process]) if post_processing
119
120
 
120
121
  # Reset the file size if the original file was reprocessed.
121
122
  instance_write(:file_size, @queued_for_write[:original].size.to_i)
122
- instance_write(:fingerprint, generate_fingerprint(@queued_for_write[:original]))
123
+ instance_write(:fingerprint, generate_fingerprint(@queued_for_write[:original])) if stores_fingerprint
123
124
  ensure
124
125
  uploaded_file.close if close_uploaded_file
125
126
  end
126
127
 
127
- # Returns the public URL of the attachment, with a given style. Note that
128
- # this does not necessarily need to point to a file that your web server
129
- # can access and can point to an action in your app, if you need fine
130
- # grained security. This is not recommended if you don't need the
131
- # security, however, for performance reasons. Set use_timestamp to false
132
- # if you want to stop the attachment update time appended to the url
128
+ # Returns the public URL of the attachment with a given style. This does
129
+ # not necessarily need to point to a file that your Web server can access
130
+ # and can instead point to an action in your app, for example for fine grained
131
+ # security; this has a serious performance tradeoff.
132
+ #
133
+ # Options:
134
+ #
135
+ # +timestamp+ - Add a timestamp to the end of the URL. Default: true.
136
+ # +escape+ - Perform URI escaping to the URL. Default: true.
137
+ #
138
+ # Global controls (set on has_attached_file):
139
+ #
140
+ # +interpolator+ - The object that fills in a URL pattern's variables.
141
+ # +default_url+ - The image to show when the attachment has no image.
142
+ # +url+ - The URL for a saved image.
143
+ # +url_generator+ - The object that generates a URL. Default: Paperclip::UrlGenerator.
144
+ #
145
+ # As mentioned just above, the object that generates this URL can be passed
146
+ # in, for finer control. This object must respond to two methods:
147
+ #
148
+ # +#new(Paperclip::Attachment, options_hash)+
149
+ # +#for(style_name, options_hash)+
133
150
  def url(style_name = default_style, options = {})
134
- options = handle_url_options(options)
135
- url = interpolate(most_appropriate_url, style_name)
151
+ default_options = {:timestamp => @options[:use_timestamp], :escape => true}
136
152
 
137
- url = url_timestamp(url) if options[:timestamp]
138
- url = escape_url(url) if options[:escape]
139
- url
153
+ if options == true || options == false # Backwards compatibility.
154
+ @url_generator.for(style_name, default_options.merge(:timestamp => options))
155
+ else
156
+ @url_generator.for(style_name, default_options.merge(options))
157
+ end
140
158
  end
141
159
 
142
160
  # Returns the path of the attachment as defined by the :path option. If the
@@ -144,7 +162,7 @@ module Paperclip
144
162
  # on disk. If the file is stored in S3, the path is the "key" part of the
145
163
  # URL, and the :bucket option refers to the S3 bucket.
146
164
  def path(style_name = default_style)
147
- path = original_filename.nil? ? nil : interpolate(@options.path, style_name)
165
+ path = original_filename.nil? ? nil : interpolate(path_option, style_name)
148
166
  path.respond_to?(:unescape) ? path.unescape : path
149
167
  end
150
168
 
@@ -154,11 +172,28 @@ module Paperclip
154
172
  end
155
173
 
156
174
  def default_style
157
- @options.default_style
175
+ @options[:default_style]
158
176
  end
159
177
 
160
178
  def styles
161
- @options.styles
179
+ styling_option = @options[:styles]
180
+ if styling_option.respond_to?(:call) || !@normalized_styles
181
+ @normalized_styles = ActiveSupport::OrderedHash.new
182
+ (styling_option.respond_to?(:call) ? styling_option.call(self) : styling_option).each do |name, args|
183
+ @normalized_styles[name] = Paperclip::Style.new(name, args.dup, self)
184
+ end
185
+ end
186
+ @normalized_styles
187
+ end
188
+
189
+ def processors
190
+ processing_option = @options[:processors]
191
+
192
+ if processing_option.respond_to?(:call)
193
+ processing_option.call(instance)
194
+ else
195
+ processing_option
196
+ end
162
197
  end
163
198
 
164
199
  # Returns an array containing the errors on this attachment.
@@ -174,7 +209,7 @@ module Paperclip
174
209
  # Saves the file, if there are no errors. If there are, it flushes them to
175
210
  # the instance's errors and returns false, cancelling the save.
176
211
  def save
177
- flush_deletes
212
+ flush_deletes unless @options[:keep_old_files]
178
213
  flush_writes
179
214
  @dirty = false
180
215
  true
@@ -193,7 +228,7 @@ module Paperclip
193
228
  # nil to the attachment *and saving*. This is permanent. If you wish to
194
229
  # wipe out the existing attachment but not save, use #clear.
195
230
  def destroy
196
- unless @options.preserve_files
231
+ unless @options[:preserve_files]
197
232
  clear
198
233
  save
199
234
  end
@@ -219,7 +254,13 @@ module Paperclip
219
254
  # Returns the hash of the file as originally assigned, and lives in the
220
255
  # <attachment>_fingerprint attribute of the model.
221
256
  def fingerprint
222
- instance_read(:fingerprint) || (@queued_for_write[:original] && generate_fingerprint(@queued_for_write[:original]))
257
+ if instance_read(:fingerprint)
258
+ instance_read(:fingerprint)
259
+ elsif @instance.respond_to?("#{name}_fingerprint".to_sym)
260
+ @queued_for_write[:original] && generate_fingerprint(@queued_for_write[:original])
261
+ else
262
+ nil
263
+ end
223
264
  end
224
265
 
225
266
  # Returns the content_type of the file as originally assigned, and lives
@@ -238,16 +279,16 @@ module Paperclip
238
279
  # The time zone to use for timestamp interpolation. Using the default
239
280
  # time zone ensures that results are consistent across all threads.
240
281
  def time_zone
241
- @options.use_default_time_zone ? Time.zone_default : Time.zone
282
+ @options[:use_default_time_zone] ? Time.zone_default : Time.zone
242
283
  end
243
284
 
244
285
  # Returns a unique hash suitable for obfuscating the URL of an otherwise
245
286
  # publicly viewable attachment.
246
- def hash(style_name = default_style)
247
- raise ArgumentError, "Unable to generate hash without :hash_secret" unless @options.hash_secret
287
+ def hash_key(style_name = default_style)
288
+ raise ArgumentError, "Unable to generate hash without :hash_secret" unless @options[:hash_secret]
248
289
  require 'openssl' unless defined?(OpenSSL)
249
- data = interpolate(@options.hash_data, style_name)
250
- OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@options.hash_digest).new, @options.hash_secret, data)
290
+ data = interpolate(@options[:hash_data], style_name)
291
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@options[:hash_digest]).new, @options[:hash_secret], data)
251
292
  end
252
293
 
253
294
  def generate_fingerprint(source)
@@ -328,42 +369,8 @@ module Paperclip
328
369
 
329
370
  private
330
371
 
331
- def handle_url_options(options)
332
- timestamp = extract_timestamp(options)
333
- options = {} if options == true || options == false
334
- options[:timestamp] = timestamp
335
- options[:escape] = true if options[:escape].nil?
336
- options
337
- end
338
-
339
- def extract_timestamp(options)
340
- possibilities = [((options == true || options == false) ? options : nil),
341
- (options.respond_to?(:[]) ? options[:timestamp] : nil),
342
- @options.use_timestamp]
343
- possibilities.find{|n| !n.nil? }
344
- end
345
-
346
- def default_url
347
- return @options.default_url.call(self) if @options.default_url.is_a?(Proc)
348
- @options.default_url
349
- end
350
-
351
- def most_appropriate_url
352
- if original_filename.nil?
353
- default_url
354
- else
355
- @options.url
356
- end
357
- end
358
-
359
- def url_timestamp(url)
360
- return url unless updated_at
361
- delimiter_char = url.include?("?") ? "&" : "?"
362
- "#{url}#{delimiter_char}#{updated_at.to_s}"
363
- end
364
-
365
- def escape_url(url)
366
- url.respond_to?(:escape) ? url.escape : URI.escape(url)
372
+ def path_option
373
+ @options[:path].respond_to?(:call) ? @options[:path].call(self) : @options[:path]
367
374
  end
368
375
 
369
376
  def ensure_required_accessors! #:nodoc:
@@ -383,7 +390,7 @@ module Paperclip
383
390
  end
384
391
 
385
392
  def initialize_storage #:nodoc:
386
- storage_class_name = @options.storage.to_s.downcase.camelize
393
+ storage_class_name = @options[:storage].to_s.downcase.camelize
387
394
  begin
388
395
  storage_module = Paperclip::Storage.const_get(storage_class_name)
389
396
  rescue NameError
@@ -393,18 +400,18 @@ module Paperclip
393
400
  end
394
401
 
395
402
  def extra_options_for(style) #:nodoc:
396
- all_options = @options.convert_options[:all]
403
+ all_options = @options[:convert_options][:all]
397
404
  all_options = all_options.call(instance) if all_options.respond_to?(:call)
398
- style_options = @options.convert_options[style]
405
+ style_options = @options[:convert_options][style]
399
406
  style_options = style_options.call(instance) if style_options.respond_to?(:call)
400
407
 
401
408
  [ style_options, all_options ].compact.join(" ")
402
409
  end
403
410
 
404
411
  def extra_source_file_options_for(style) #:nodoc:
405
- all_options = @options.source_file_options[:all]
412
+ all_options = @options[:source_file_options][:all]
406
413
  all_options = all_options.call(instance) if all_options.respond_to?(:call)
407
- style_options = @options.source_file_options[style]
414
+ style_options = @options[:source_file_options][style]
408
415
  style_options = style_options.call(instance) if style_options.respond_to?(:call)
409
416
 
410
417
  [ style_options, all_options ].compact.join(" ")
@@ -420,28 +427,35 @@ module Paperclip
420
427
  end
421
428
 
422
429
  def post_process_styles(*style_args) #:nodoc:
423
- @options.styles.each do |name, style|
424
- begin
425
- if style_args.empty? || style_args.include?(name)
426
- raise RuntimeError.new("Style #{name} has no processors defined.") if style.processors.blank?
427
- @queued_for_write[name] = style.processors.inject(@queued_for_write[:original]) do |file, processor|
428
- Paperclip.processor(processor).make(file, style.processor_options, self)
429
- end
430
- end
431
- rescue PaperclipError => e
432
- log("An error was received while processing: #{e.inspect}")
433
- (@errors[:processing] ||= []) << e.message if @options.whiny
430
+ post_process_style(:original, styles[:original]) if styles.include?(:original) && process_style?(:original, style_args)
431
+ styles.reject{ |name, style| name == :original }.each do |name, style|
432
+ post_process_style(name, style) if process_style?(name, style_args)
433
+ end
434
+ end
435
+
436
+ def post_process_style(name, style) #:nodoc:
437
+ begin
438
+ raise RuntimeError.new("Style #{name} has no processors defined.") if style.processors.blank?
439
+ @queued_for_write[name] = style.processors.inject(@queued_for_write[:original]) do |file, processor|
440
+ Paperclip.processor(processor).make(file, style.processor_options, self)
434
441
  end
442
+ rescue PaperclipError => e
443
+ log("An error was received while processing: #{e.inspect}")
444
+ (@errors[:processing] ||= []) << e.message if @options[:whiny]
435
445
  end
436
446
  end
437
447
 
448
+ def process_style?(style_name, style_args) #:nodoc:
449
+ style_args.empty? || style_args.include?(style_name)
450
+ end
451
+
438
452
  def interpolate(pattern, style_name = default_style) #:nodoc:
439
453
  interpolator.interpolate(pattern, self, style_name)
440
454
  end
441
455
 
442
456
  def queue_existing_for_delete #:nodoc:
443
- return if @options.preserve_files || !file?
444
- @queued_for_delete += [:original, *@options.styles.keys].uniq.map do |style|
457
+ return if @options[:preserve_files] || !file?
458
+ @queued_for_delete += [:original, *styles.keys].uniq.map do |style|
445
459
  path(style) if exists?(style)
446
460
  end.compact
447
461
  instance_write(:file_name, nil)