paperclip 3.3.1 → 3.4.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 (41) hide show
  1. data/.gitignore +2 -1
  2. data/.travis.yml +2 -0
  3. data/NEWS +1 -0
  4. data/README.md +1 -1
  5. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +1 -1
  6. data/lib/paperclip.rb +4 -1
  7. data/lib/paperclip/attachment.rb +27 -6
  8. data/lib/paperclip/callbacks.rb +2 -2
  9. data/lib/paperclip/content_type_detector.rb +1 -12
  10. data/lib/paperclip/file_command_content_type_detector.rb +32 -0
  11. data/lib/paperclip/geometry.rb +33 -30
  12. data/lib/paperclip/geometry_detector_factory.rb +41 -0
  13. data/lib/paperclip/geometry_parser_factory.rb +31 -0
  14. data/lib/paperclip/helpers.rb +7 -7
  15. data/lib/paperclip/io_adapters/uploaded_file_adapter.rb +17 -1
  16. data/lib/paperclip/io_adapters/uri_adapter.rb +1 -0
  17. data/lib/paperclip/storage/filesystem.rb +11 -2
  18. data/lib/paperclip/storage/fog.rb +28 -13
  19. data/lib/paperclip/storage/s3.rb +39 -18
  20. data/lib/paperclip/thumbnail.rb +3 -0
  21. data/lib/paperclip/validators/attachment_content_type_validator.rb +29 -8
  22. data/lib/paperclip/version.rb +1 -1
  23. data/paperclip.gemspec +1 -1
  24. data/test/attachment_processing_test.rb +29 -0
  25. data/test/attachment_test.rb +97 -11
  26. data/test/file_command_content_type_detector_test.rb +25 -0
  27. data/test/generator_test.rb +3 -3
  28. data/test/geometry_detector_test.rb +24 -0
  29. data/test/geometry_parser_test.rb +73 -0
  30. data/test/geometry_test.rb +39 -7
  31. data/test/helper.rb +5 -1
  32. data/test/integration_test.rb +46 -1
  33. data/test/io_adapters/uploaded_file_adapter_test.rb +28 -4
  34. data/test/io_adapters/uri_adapter_test.rb +4 -0
  35. data/test/paperclip_test.rb +17 -7
  36. data/test/storage/fog_test.rb +66 -7
  37. data/test/storage/s3_test.rb +1 -1
  38. data/test/style_test.rb +18 -14
  39. data/test/thumbnail_test.rb +10 -56
  40. data/test/validators/attachment_content_type_validator_test.rb +155 -55
  41. metadata +137 -127
data/.gitignore CHANGED
@@ -5,8 +5,9 @@
5
5
  tmp
6
6
  .DS_Store
7
7
 
8
+ *.log
9
+
8
10
  test/s3.yml
9
- test/debug.log
10
11
  test/paperclip.db
11
12
  test/doc
12
13
  test/pkg
@@ -2,6 +2,7 @@ rvm:
2
2
  - 1.9.2
3
3
  - 1.9.3
4
4
  - jruby-19mode
5
+ - rbx-19mode
5
6
 
6
7
  before_script: "sudo ntpdate -ub ntp.ubuntu.com pool.ntp.org; true"
7
8
  script: "bundle exec rake clean test cucumber"
@@ -14,3 +15,4 @@ gemfile:
14
15
  matrix:
15
16
  allow_failures:
16
17
  - rvm: jruby-19mode
18
+ - rvm: rbx-19mode
data/NEWS CHANGED
@@ -123,6 +123,7 @@ New in 3.0.1:
123
123
 
124
124
  * Feature: Introduce Paperlip IO adapter.
125
125
  * Bug fix: Regression in AttachmentContentTypeValidator has been fixed.
126
+ * API CHANGE: #to_file has been removed. Use the #copy_to_local_file method instead.
126
127
 
127
128
  New in 3.0.0:
128
129
 
data/README.md CHANGED
@@ -615,7 +615,7 @@ Please see `CONTRIBUTING.md` for more details on contributing and running test.
615
615
  Credits
616
616
  -------
617
617
 
618
- ![thoughtbot](http://thoughtbot.com/images/tm/logo.png)
618
+ ![thoughtbot](http://thoughtbot.com/assets/tm/logo.png)
619
619
 
620
620
  Paperclip is maintained and funded by [thoughtbot, inc](http://thoughtbot.com/community)
621
621
 
@@ -2,7 +2,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration
2
2
  def self.up
3
3
  change_table :<%= table_name %> do |t|
4
4
  <% attachment_names.each do |attachment| -%>
5
- t.has_attached_file :<%= attachment %>
5
+ t.attachment :<%= attachment %>
6
6
  <% end -%>
7
7
  end
8
8
  end
@@ -29,6 +29,8 @@ require 'erb'
29
29
  require 'digest'
30
30
  require 'tempfile'
31
31
  require 'paperclip/version'
32
+ require 'paperclip/geometry_parser_factory'
33
+ require 'paperclip/geometry_detector_factory'
32
34
  require 'paperclip/geometry'
33
35
  require 'paperclip/processor'
34
36
  require 'paperclip/tempfile'
@@ -40,6 +42,7 @@ require 'paperclip/attachment'
40
42
  require 'paperclip/attachment_options'
41
43
  require 'paperclip/storage'
42
44
  require 'paperclip/callbacks'
45
+ require 'paperclip/file_command_content_type_detector'
43
46
  require 'paperclip/content_type_detector'
44
47
  require 'paperclip/glue'
45
48
  require 'paperclip/errors'
@@ -179,7 +182,7 @@ module Paperclip
179
182
 
180
183
  attachment_definitions[name] = Paperclip::AttachmentOptions.new(options)
181
184
  Paperclip.classes_with_attachments << self.name
182
- Paperclip.check_for_url_clash(name,attachment_definitions[name][:url],self.name)
185
+ Paperclip.check_for_path_clash(name,attachment_definitions[name][:path],self.name)
183
186
 
184
187
  after_save :save_attached_files
185
188
  before_destroy :prepare_for_destroy
@@ -12,6 +12,7 @@ module Paperclip
12
12
  :convert_options => {},
13
13
  :default_style => :original,
14
14
  :default_url => "/:attachment/:style/missing.png",
15
+ :escape_url => true,
15
16
  :restricted_characters => /[&$+,\/:;=?@<>\[\]\{\}\|\\\^~%# ]/,
16
17
  :hash_data => ":class/:attachment/:id/:style/:updated_at",
17
18
  :hash_digest => "SHA1",
@@ -60,6 +61,7 @@ module Paperclip
60
61
  # +preserve_files+ - whether to keep files on the filesystem when deleting or clearing the attachment. Defaults to false
61
62
  # +interpolator+ - the object used to interpolate filenames and URLs. Defaults to Paperclip::Interpolations
62
63
  # +url_generator+ - the object used to generate URLs, using the interpolator. Defaults to Paperclip::UrlGenerator
64
+ # +escape_url+ - Perform URI escaping to URLs. Defaults to true
63
65
  def initialize(name, instance, options = {})
64
66
  @name = name
65
67
  @instance = instance
@@ -90,8 +92,7 @@ module Paperclip
90
92
  ensure_required_accessors!
91
93
  file = Paperclip.io_adapters.for(uploaded_file)
92
94
 
93
- @options[:only_process].map!(&:to_sym)
94
- self.clear(*@options[:only_process])
95
+ self.clear(*only_process)
95
96
  return nil if file.nil?
96
97
 
97
98
  @queued_for_write[:original] = file
@@ -104,7 +105,7 @@ module Paperclip
104
105
 
105
106
  @dirty = true
106
107
 
107
- post_process(*@options[:only_process]) if post_processing
108
+ post_process(*only_process) if post_processing && valid_assignment?
108
109
 
109
110
  # Reset the file size if the original file was reprocessed.
110
111
  instance_write(:file_size, @queued_for_write[:original].size)
@@ -134,7 +135,7 @@ module Paperclip
134
135
  # +#new(Paperclip::Attachment, options_hash)+
135
136
  # +#for(style_name, options_hash)+
136
137
  def url(style_name = default_style, options = {})
137
- default_options = {:timestamp => @options[:use_timestamp], :escape => true}
138
+ default_options = {:timestamp => @options[:use_timestamp], :escape => @options[:escape_url]}
138
139
 
139
140
  if options == true || options == false # Backwards compatibility.
140
141
  @url_generator.for(style_name, default_options.merge(:timestamp => options))
@@ -157,6 +158,10 @@ module Paperclip
157
158
  url(style_name)
158
159
  end
159
160
 
161
+ def as_json(options = nil)
162
+ to_s((options && options[:style]) || default_style)
163
+ end
164
+
160
165
  def default_style
161
166
  @options[:default_style]
162
167
  end
@@ -174,6 +179,12 @@ module Paperclip
174
179
  @normalized_styles
175
180
  end
176
181
 
182
+ def only_process
183
+ only_process = @options[:only_process].dup
184
+ only_process = only_process.call(self) if only_process.respond_to?(:call)
185
+ only_process.map(&:to_sym)
186
+ end
187
+
177
188
  def processors
178
189
  processing_option = @options[:processors]
179
190
 
@@ -311,6 +322,10 @@ module Paperclip
311
322
 
312
323
  alias :present? :file?
313
324
 
325
+ def blank?
326
+ not present?
327
+ end
328
+
314
329
  # Determines whether the instance responds to this attribute. Used to prevent
315
330
  # calculations on fields we won't even store.
316
331
  def instance_respond_to?(attr)
@@ -354,8 +369,14 @@ module Paperclip
354
369
  Paperclip.log(message)
355
370
  end
356
371
 
357
- def valid_assignment? file #:nodoc:
358
- file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type))
372
+ def valid_assignment? #:nodoc:
373
+ if instance.valid?
374
+ true
375
+ else
376
+ instance.errors.none? do |attr, message|
377
+ attr.to_s.start_with?(@name.to_s)
378
+ end
379
+ end
359
380
  end
360
381
 
361
382
  def initialize_storage #:nodoc:
@@ -22,8 +22,8 @@ module Paperclip
22
22
  end
23
23
 
24
24
  module Running
25
- def run_paperclip_callbacks(callback, opts = nil, &block)
26
- run_callbacks(callback, opts, &block)
25
+ def run_paperclip_callbacks(callback, &block)
26
+ run_callbacks(callback, &block)
27
27
  end
28
28
  end
29
29
  end
@@ -49,18 +49,7 @@ module Paperclip
49
49
  end
50
50
 
51
51
  def type_from_file_command
52
- type = begin
53
- # On BSDs, `file` doesn't give a result code of 1 if the file doesn't exist.
54
- Paperclip.run("file", "-b --mime :file", :file => @filename)
55
- rescue Cocaine::CommandLineError => e
56
- Paperclip.log("Error while determining content type: #{e}")
57
- SENSIBLE_DEFAULT
58
- end
59
-
60
- if type.match(/\(.*?\)/)
61
- type = SENSIBLE_DEFAULT
62
- end
63
- type.split(/[:;\s]+/)[0]
52
+ FileCommandContentTypeDetector.new(@filename).detect
64
53
  end
65
54
 
66
55
  end
@@ -0,0 +1,32 @@
1
+ module Paperclip
2
+ class FileCommandContentTypeDetector
3
+ SENSIBLE_DEFAULT = "application/octet-stream"
4
+
5
+ def initialize(filename)
6
+ @filename = filename
7
+ end
8
+
9
+ def detect
10
+ type_from_file_command
11
+ end
12
+
13
+ private
14
+
15
+ def type_from_file_command
16
+ type = begin
17
+ # On BSDs, `file` doesn't give a result code of 1 if the file doesn't exist.
18
+ Paperclip.run("file", "-b --mime :file", :file => @filename)
19
+ rescue Cocaine::CommandLineError => e
20
+ Paperclip.log("Error while determining content type: #{e}")
21
+ SENSIBLE_DEFAULT
22
+ end
23
+
24
+ if type.nil? || type.match(/\(.*?\)/)
25
+ type = SENSIBLE_DEFAULT
26
+ end
27
+ type.split(/[:;\s]+/)[0]
28
+ end
29
+
30
+ end
31
+ end
32
+
@@ -4,37 +4,40 @@ module Paperclip
4
4
  class Geometry
5
5
  attr_accessor :height, :width, :modifier
6
6
 
7
+ EXIF_ROTATED_ORIENTATION_VALUES = [5, 6, 7, 8]
8
+
7
9
  # Gives a Geometry representing the given height and width
8
- def initialize width = nil, height = nil, modifier = nil
9
- @height = height.to_f
10
- @width = width.to_f
11
- @modifier = modifier
12
- end
13
-
14
- # Uses ImageMagick to determing the dimensions of a file, passed in as either a
15
- # File or path.
16
- # NOTE: (race cond) Do not reassign the 'file' variable inside this method as it is likely to be
17
- # a Tempfile object, which would be eligible for file deletion when no longer referenced.
18
- def self.from_file file
19
- file_path = file.respond_to?(:path) ? file.path : file
20
- raise(Errors::NotIdentifiedByImageMagickError.new("Cannot find the geometry of a file with a blank name")) if file_path.blank?
21
- geometry = begin
22
- silence_stream(STDERR) do
23
- Paperclip.run("identify", "-format %wx%h :file", :file => "#{file_path}[0]")
24
- end
25
- rescue Cocaine::ExitStatusError
26
- ""
27
- rescue Cocaine::CommandNotFoundError => e
28
- raise Errors::CommandNotFoundError.new("Could not run the `identify` command. Please install ImageMagick.")
29
- end
30
- parse(geometry) ||
31
- raise(Errors::NotIdentifiedByImageMagickError.new("#{file_path} is not recognized by the 'identify' command."))
32
- end
33
-
34
- # Parses a "WxH" formatted string, where W is the width and H is the height.
35
- def self.parse string
36
- if match = (string && string.match(/\b(\d*)x?(\d*)\b([\>\<\#\@\%^!])?/i))
37
- Geometry.new(*match[1,3])
10
+ def initialize(width = nil, height = nil, modifier = nil)
11
+ if width.is_a?(Hash)
12
+ options = width
13
+ @height = options[:height].to_f
14
+ @width = options[:width].to_f
15
+ @modifier = options[:modifier]
16
+ @orientation = options[:orientation].to_i
17
+ else
18
+ @height = height.to_f
19
+ @width = width.to_f
20
+ @modifier = modifier
21
+ end
22
+ end
23
+
24
+ # Extracts the Geometry from a file (or path to a file)
25
+ def self.from_file(file)
26
+ GeometryDetector.new(file).make
27
+ end
28
+
29
+ # Extracts the Geometry from a "WxH,O" string
30
+ # Where W is the width, H is the height,
31
+ # and O is the EXIF orientation
32
+ def self.parse(string)
33
+ GeometryParser.new(string).make
34
+ end
35
+
36
+ # Swaps the height and width if necessary
37
+ def auto_orient
38
+ if EXIF_ROTATED_ORIENTATION_VALUES.include?(@orientation)
39
+ @height, @width = @width, @height
40
+ @orientation -= 4
38
41
  end
39
42
  end
40
43
 
@@ -0,0 +1,41 @@
1
+ module Paperclip
2
+ class GeometryDetector
3
+ def initialize(file)
4
+ @file = file
5
+ raise_if_blank_file
6
+ end
7
+
8
+ def make
9
+ geometry = GeometryParser.new(geometry_string.strip).make
10
+ geometry || raise(Errors::NotIdentifiedByImageMagickError.new)
11
+ end
12
+
13
+ private
14
+
15
+ def geometry_string
16
+ begin
17
+ silence_stream(STDERR) do
18
+ Paperclip.run("identify", "-format '%wx%h,%[exif:orientation]' :file", :file => "#{path}[0]")
19
+ end
20
+ rescue Cocaine::ExitStatusError
21
+ ""
22
+ rescue Cocaine::CommandNotFoundError => e
23
+ raise_because_imagemagick_missing
24
+ end
25
+ end
26
+
27
+ def path
28
+ @file.respond_to?(:path) ? @file.path : @file
29
+ end
30
+
31
+ def raise_if_blank_file
32
+ if path.blank?
33
+ raise Errors::NotIdentifiedByImageMagickError.new("Cannot find the geometry of a file with a blank name")
34
+ end
35
+ end
36
+
37
+ def raise_because_imagemagick_missing
38
+ raise Errors::CommandNotFoundError.new("Could not run the `identify` command. Please install ImageMagick.")
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,31 @@
1
+ module Paperclip
2
+ class GeometryParser
3
+ FORMAT = /\b(\d*)x?(\d*)\b(?:,(\d?))?([\>\<\#\@\%^!])?/i
4
+ def initialize(string)
5
+ @string = string
6
+ end
7
+
8
+ def make
9
+ if match
10
+ Geometry.new(
11
+ :height => @height,
12
+ :width => @width,
13
+ :modifier => @modifier,
14
+ :orientation => @orientation
15
+ )
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def match
22
+ if actual_match = @string && @string.match(FORMAT)
23
+ @width = actual_match[1]
24
+ @height = actual_match[2]
25
+ @orientation = actual_match[3]
26
+ @modifier = actual_match[4]
27
+ end
28
+ actual_match
29
+ end
30
+ end
31
+ end
@@ -17,7 +17,7 @@ module Paperclip
17
17
  # :expected_outcodes -> An array of integers that defines the expected exit codes
18
18
  # of the binary. Defaults to [0].
19
19
  #
20
- # :log_command -> Log the command being run when set to true (defaults to false).
20
+ # :log_command -> Log the command being run when set to true (defaults to true).
21
21
  # This will only log if logging in general is set to true as well.
22
22
  #
23
23
  # :swallow_stderr -> Set to true if you don't care what happens on STDERR.
@@ -49,13 +49,13 @@ module Paperclip
49
49
  end
50
50
  end
51
51
 
52
- def check_for_url_clash(name,url,klass)
53
- @names_url ||= {}
54
- default_url = url || Attachment.default_options[:url]
55
- if @names_url[name] && @names_url[name][:url] == default_url && @names_url[name][:class] != klass && @names_url[name][:url] !~ /:class/
56
- log("Duplicate URL for #{name} with #{default_url}. This will clash with attachment defined in #{@names_url[name][:class]} class")
52
+ def check_for_path_clash(name,path,klass)
53
+ @names_path ||= {}
54
+ default_path = path || Attachment.default_options[:path]
55
+ if @names_path[name] && @names_path[name][:path] == default_path && @names_path[name][:class] != klass && @names_path[name][:path] !~ /:class/
56
+ log("Duplicate path for #{name} with #{default_path}. This will clash with attachment defined in #{@names_path[name][:class]} class")
57
57
  end
58
- @names_url[name] = {:url => default_url, :class => klass}
58
+ @names_path[name] = {:path => default_path, :class => klass}
59
59
  end
60
60
 
61
61
  def reset_duplicate_clash_check!
@@ -11,13 +11,29 @@ module Paperclip
11
11
  end
12
12
  end
13
13
 
14
+ class << self
15
+ attr_accessor :content_type_detector
16
+ end
17
+
14
18
  private
15
19
 
16
20
  def cache_current_values
17
21
  @original_filename = @target.original_filename
18
- @content_type = @target.content_type.to_s.strip
22
+ @content_type = determine_content_type
19
23
  @size = File.size(@target.path)
20
24
  end
25
+
26
+ def content_type_detector
27
+ self.class.content_type_detector
28
+ end
29
+
30
+ def determine_content_type
31
+ content_type = @target.content_type.to_s.strip
32
+ if content_type_detector
33
+ content_type = content_type_detector.new(@target.path).detect
34
+ end
35
+ content_type
36
+ end
21
37
  end
22
38
  end
23
39
 
@@ -32,6 +32,7 @@ module Paperclip
32
32
  while data = src.read(16*1024)
33
33
  destination.write(data)
34
34
  end
35
+ src.close
35
36
  destination.rewind
36
37
  destination
37
38
  end