paperclip 3.0.4 → 3.1.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 (56) hide show
  1. data/Appraisals +3 -3
  2. data/NEWS +43 -0
  3. data/README.md +81 -5
  4. data/features/basic_integration.feature +20 -2
  5. data/features/migration.feature +94 -0
  6. data/features/step_definitions/attachment_steps.rb +28 -0
  7. data/features/step_definitions/rails_steps.rb +19 -1
  8. data/features/step_definitions/web_steps.rb +3 -3
  9. data/gemfiles/3.0.gemfile +1 -1
  10. data/gemfiles/3.1.gemfile +1 -1
  11. data/gemfiles/3.2.gemfile +1 -1
  12. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +4 -8
  13. data/lib/paperclip.rb +2 -0
  14. data/lib/paperclip/attachment.rb +4 -0
  15. data/lib/paperclip/geometry.rb +33 -0
  16. data/lib/paperclip/glue.rb +2 -1
  17. data/lib/paperclip/io_adapters/abstract_adapter.rb +45 -0
  18. data/lib/paperclip/io_adapters/attachment_adapter.rb +13 -48
  19. data/lib/paperclip/io_adapters/file_adapter.rb +11 -61
  20. data/lib/paperclip/io_adapters/identity_adapter.rb +1 -1
  21. data/lib/paperclip/io_adapters/nil_adapter.rb +1 -1
  22. data/lib/paperclip/io_adapters/stringio_adapter.rb +11 -42
  23. data/lib/paperclip/io_adapters/uploaded_file_adapter.rb +6 -45
  24. data/lib/paperclip/matchers.rb +2 -2
  25. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +36 -17
  26. data/lib/paperclip/railtie.rb +5 -1
  27. data/lib/paperclip/schema.rb +59 -23
  28. data/lib/paperclip/storage/filesystem.rb +5 -0
  29. data/lib/paperclip/storage/fog.rb +36 -14
  30. data/lib/paperclip/storage/s3.rb +14 -16
  31. data/lib/paperclip/style.rb +2 -2
  32. data/lib/paperclip/tempfile_factory.rb +21 -0
  33. data/lib/paperclip/thumbnail.rb +10 -1
  34. data/lib/paperclip/version.rb +1 -1
  35. data/paperclip.gemspec +1 -1
  36. data/test/attachment_test.rb +56 -24
  37. data/test/fixtures/animated +0 -0
  38. data/test/fixtures/animated.unknown +0 -0
  39. data/test/generator_test.rb +26 -24
  40. data/test/geometry_test.rb +19 -0
  41. data/test/helper.rb +8 -0
  42. data/test/integration_test.rb +23 -23
  43. data/test/io_adapters/abstract_adapter_test.rb +44 -0
  44. data/test/io_adapters/attachment_adapter_test.rb +96 -34
  45. data/test/io_adapters/file_adapter_test.rb +13 -1
  46. data/test/io_adapters/stringio_adapter_test.rb +9 -10
  47. data/test/io_adapters/uploaded_file_adapter_test.rb +2 -1
  48. data/test/schema_test.rb +179 -77
  49. data/test/storage/filesystem_test.rb +18 -3
  50. data/test/storage/fog_test.rb +64 -1
  51. data/test/storage/s3_test.rb +38 -2
  52. data/test/tempfile_factory_test.rb +13 -0
  53. data/test/thumbnail_test.rb +45 -0
  54. metadata +16 -9
  55. data/features/support/fixtures/.boot_config.rb.swo +0 -0
  56. data/images.rake +0 -21
@@ -41,7 +41,7 @@ module Paperclip
41
41
  #
42
42
  # class ActiveSupport::TestCase
43
43
  # extend Paperclip::Shoulda::Matchers
44
- #
44
+ #
45
45
  # #...other initializers...#
46
46
  # end
47
47
  #
@@ -57,7 +57,7 @@ module Paperclip
57
57
  # should validate_attachment_size(:avatar).
58
58
  # less_than(2.megabytes)
59
59
  # end
60
- #
60
+ #
61
61
  module Matchers
62
62
  end
63
63
  end
@@ -39,18 +39,10 @@ module Paperclip
39
39
  end
40
40
 
41
41
  def failure_message
42
- "".tap do |str|
43
- str << "Content types #{@allowed_types.join(", ")} should be accepted" if @allowed_types.present?
44
- str << "\n" if @allowed_types.present? && @rejected_types.present?
45
- str << "Content types #{@rejected_types.join(", ")} should be rejected by #{@attachment_name}" if @rejected_types.present?
46
- end
47
- end
48
-
49
- def negative_failure_message
50
- "".tap do |str|
51
- str << "Content types #{@allowed_types.join(", ")} should be rejected" if @allowed_types.present?
52
- str << "\n" if @allowed_types.present? && @rejected_types.present?
53
- str << "Content types #{@rejected_types.join(", ")} should be accepted by #{@attachment_name}" if @rejected_types.present?
42
+ "#{expected_attachment}\n".tap do |message|
43
+ message << accepted_types_and_failures
44
+ message << "\n\n" if @allowed_types.present? && @rejected_types.present?
45
+ message << rejected_types_and_failures
54
46
  end
55
47
  end
56
48
 
@@ -60,20 +52,47 @@ module Paperclip
60
52
 
61
53
  protected
62
54
 
55
+ def accepted_types_and_failures
56
+ if @allowed_types.present?
57
+ "Accept content types: #{@allowed_types.join(", ")}\n".tap do |message|
58
+ if @missing_allowed_types.any?
59
+ message << " #{@missing_allowed_types.join(", ")} were rejected."
60
+ else
61
+ message << " All were accepted successfully."
62
+ end
63
+ end
64
+ end
65
+ end
66
+ def rejected_types_and_failures
67
+ if @rejected_types.present?
68
+ "Reject content types: #{@rejected_types.join(", ")}\n".tap do |message|
69
+ if @missing_rejected_types.any?
70
+ message << " #{@missing_rejected_types.join(", ")} were accepted."
71
+ else
72
+ message << " All were rejected successfully."
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def expected_attachment
79
+ "Expected #{@attachment_name}:\n"
80
+ end
81
+
63
82
  def type_allowed?(type)
64
- file = Paperclip.io_adapters.for(StringIO.new("."))
65
- file.content_type = type
66
- @subject.attachment_for(@attachment_name).assign(file)
83
+ @subject.send("#{@attachment_name}_content_type=", type)
67
84
  @subject.valid?
68
85
  @subject.errors[:"#{@attachment_name}_content_type"].blank?
69
86
  end
70
87
 
71
88
  def allowed_types_allowed?
72
- @allowed_types.all? { |type| type_allowed?(type) }
89
+ @missing_allowed_types ||= @allowed_types.reject { |type| type_allowed?(type) }
90
+ @missing_allowed_types.none?
73
91
  end
74
92
 
75
93
  def rejected_types_rejected?
76
- !@rejected_types.any? { |type| type_allowed?(type) }
94
+ @missing_rejected_types ||= @rejected_types.select { |type| type_allowed?(type) }
95
+ @missing_rejected_types.none?
77
96
  end
78
97
  end
79
98
  end
@@ -5,10 +5,14 @@ module Paperclip
5
5
  require 'rails'
6
6
 
7
7
  class Railtie < Rails::Railtie
8
- initializer 'paperclip.insert_into_active_record' do
8
+ initializer 'paperclip.insert_into_active_record' do |app|
9
9
  ActiveSupport.on_load :active_record do
10
10
  Paperclip::Railtie.insert
11
11
  end
12
+
13
+ if app.config.respond_to?(:paperclip_defaults)
14
+ Paperclip::Attachment.default_options.merge!(app.config.paperclip_defaults)
15
+ end
12
16
  end
13
17
 
14
18
  rake_tasks { load "tasks/paperclip.rake" }
@@ -1,39 +1,75 @@
1
+ require 'active_support/deprecation'
2
+
1
3
  module Paperclip
2
- # Provides two helpers that can be used in migrations.
3
- #
4
- # In order to use this module, the target class should implement a
5
- # +column+ method that takes the column name and type, both as symbols,
6
- # as well as a +remove_column+ method that takes a table and column name,
7
- # also both symbols.
4
+ # Provides helper methods that can be used in migrations.
8
5
  module Schema
9
- @@columns = {:file_name => :string,
10
- :content_type => :string,
11
- :file_size => :integer,
12
- :updated_at => :datetime}
6
+ COLUMNS = {:file_name => :string,
7
+ :content_type => :string,
8
+ :file_size => :integer,
9
+ :updated_at => :datetime}
10
+
11
+ def self.included(base)
12
+ ActiveRecord::ConnectionAdapters::Table.send :include, TableDefinition
13
+ ActiveRecord::ConnectionAdapters::TableDefinition.send :include, TableDefinition
14
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.send :include, Statements
13
15
 
14
- def has_attached_file(attachment_name)
15
- with_columns_for(attachment_name) do |column_name, column_type|
16
- column(column_name, column_type)
16
+ if defined?(ActiveRecord::Migration::CommandRecorder) # Rails 3.1+
17
+ ActiveRecord::Migration::CommandRecorder.send :include, CommandRecorder
17
18
  end
18
19
  end
19
20
 
20
- def drop_attached_file(table_name, attachment_name)
21
- with_columns_for(attachment_name) do |column_name, column_type|
22
- remove_column(table_name, column_name)
21
+ module Statements
22
+ def add_attachment(table_name, *attachment_names)
23
+ raise ArgumentError, "Please specify attachment name in your add_attachment call in your migration." if attachment_names.empty?
24
+
25
+ attachment_names.each do |attachment_name|
26
+ COLUMNS.each_pair do |column_name, column_type|
27
+ add_column(table_name, "#{attachment_name}_#{column_name}", column_type)
28
+ end
29
+ end
30
+ end
31
+
32
+ def remove_attachment(table_name, *attachment_names)
33
+ raise ArgumentError, "Please specify attachment name in your remove_attachment call in your migration." if attachment_names.empty?
34
+
35
+ attachment_names.each do |attachment_name|
36
+ COLUMNS.each_pair do |column_name, column_type|
37
+ remove_column(table_name, "#{attachment_name}_#{column_name}", column_type)
38
+ end
39
+ end
40
+ end
41
+
42
+ def drop_attached_file(*args)
43
+ ActiveSupport::Deprecation.warn "Method `drop_attached_file` in the migration has been deprecated and will be replaced by `remove_attachment`."
44
+ remove_attachment(*args)
23
45
  end
24
46
  end
25
47
 
26
- protected
48
+ module TableDefinition
49
+ def attachment(*attachment_names)
50
+ attachment_names.each do |attachment_name|
51
+ COLUMNS.each_pair do |column_name, column_type|
52
+ column("#{attachment_name}_#{column_name}", column_type)
53
+ end
54
+ end
55
+ end
27
56
 
28
- def with_columns_for(attachment_name)
29
- @@columns.each do |suffix, column_type|
30
- column_name = full_column_name(attachment_name, suffix)
31
- yield column_name, column_type
57
+ def has_attached_file(*attachment_names)
58
+ ActiveSupport::Deprecation.warn "Method `t.has_attached_file` in the migration has been deprecated and will be replaced by `t.attachment`."
59
+ attachment(*attachment_names)
32
60
  end
33
61
  end
34
62
 
35
- def full_column_name(attachment_name, column_name)
36
- "#{attachment_name}_#{column_name}".to_sym
63
+ module CommandRecorder
64
+ def add_attachment(*args)
65
+ record(:add_attachment, args)
66
+ end
67
+
68
+ private
69
+
70
+ def invert_add_attachment(args)
71
+ [:remove_attachment, args]
72
+ end
37
73
  end
38
74
  end
39
75
  end
@@ -36,6 +36,7 @@ module Paperclip
36
36
  end
37
37
  end
38
38
  FileUtils.chmod(0666&~File.umask, path(style_name))
39
+ file.rewind
39
40
  end
40
41
 
41
42
  after_flush_writes # allows attachment to clean up temp files
@@ -68,5 +69,9 @@ module Paperclip
68
69
  end
69
70
  end
70
71
 
72
+ def copy_to_local_file(style, local_dest_path)
73
+ FileUtils.cp(path(style), local_dest_path)
74
+ end
75
+
71
76
  end
72
77
  end
@@ -90,6 +90,8 @@ module Paperclip
90
90
  retried = true
91
91
  directory.save
92
92
  retry
93
+ ensure
94
+ file.rewind
93
95
  end
94
96
  end
95
97
 
@@ -108,27 +110,26 @@ module Paperclip
108
110
 
109
111
  def public_url(style = default_style)
110
112
  if @options[:fog_host]
111
- host = if @options[:fog_host].respond_to?(:call)
112
- @options[:fog_host].call(self)
113
- else
114
- (@options[:fog_host] =~ /%d/) ? @options[:fog_host] % (path(style).hash % 4) : @options[:fog_host]
115
- end
116
-
117
- "#{host}/#{path(style)}"
113
+ "#{dynamic_fog_host_for_style(style)}/#{path(style)}"
118
114
  else
119
115
  if fog_credentials[:provider] == 'AWS'
120
- if @options[:fog_directory].to_s =~ Fog::AWS_BUCKET_SUBDOMAIN_RESTRICTON_REGEX
121
- "https://#{@options[:fog_directory]}.s3.amazonaws.com/#{path(style)}"
122
- else
123
- # directory is not a valid subdomain, so use path style for access
124
- "https://s3.amazonaws.com/#{@options[:fog_directory]}/#{path(style)}"
125
- end
116
+ "https://#{host_name_for_directory}/#{path(style)}"
126
117
  else
127
118
  directory.files.new(:key => path(style)).public_url
128
119
  end
129
120
  end
130
121
  end
131
122
 
123
+ def expiring_url(time = 3600, style = default_style)
124
+ expiring_url = directory.files.get_http_url(path(style), time)
125
+
126
+ if @options[:fog_host]
127
+ expiring_url.gsub!(/#{host_name_for_directory}/, dynamic_fog_host_for_style(style))
128
+ end
129
+
130
+ return expiring_url
131
+ end
132
+
132
133
  def parse_credentials(creds)
133
134
  creds = find_credentials(creds).stringify_keys
134
135
  env = Object.const_defined?(:Rails) ? Rails.env : nil
@@ -148,6 +149,27 @@ module Paperclip
148
149
 
149
150
  private
150
151
 
152
+ def dynamic_fog_host_for_style(style)
153
+ if @options[:fog_host].respond_to?(:call)
154
+ @options[:fog_host].call(self)
155
+ else
156
+ (@options[:fog_host] =~ /%d/) ? @options[:fog_host] % (path(style).hash % 4) : @options[:fog_host]
157
+ end
158
+ end
159
+
160
+ def host_name_for_directory
161
+ if @options[:fog_directory].to_s =~ Fog::AWS_BUCKET_SUBDOMAIN_RESTRICTON_REGEX
162
+ # This:
163
+ "#{@options[:fog_directory]}."
164
+
165
+ # Should be modified to this:
166
+ # "#{@options[:fog_directory]}.s3.amazonaws.com"
167
+ # When fog with https://github.com/fog/fog/pull/857 gets released
168
+ else
169
+ "s3.amazonaws.com/#{@options[:fog_directory]}"
170
+ end
171
+ end
172
+
151
173
  def find_credentials(creds)
152
174
  case creds
153
175
  when File
@@ -175,7 +197,7 @@ module Paperclip
175
197
  else
176
198
  @options[:fog_directory]
177
199
  end
178
-
200
+
179
201
  @directory ||= connection.directories.new(:key => dir)
180
202
  end
181
203
  end
@@ -61,7 +61,7 @@ module Paperclip
61
61
  # Normally, this won't matter in the slightest and you can leave the default (which is
62
62
  # path-style, or :s3_path_url). But in some cases paths don't work and you need to use
63
63
  # the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
64
- #
64
+ #
65
65
  # Notes:
66
66
  # * The value of this option is a string, not a symbol.
67
67
  # <b>right:</b> <tt>":s3_domain_url"</tt>
@@ -119,12 +119,12 @@ module Paperclip
119
119
  @s3_protocol = @options[:s3_protocol] ||
120
120
  Proc.new do |style, attachment|
121
121
  permission = (@s3_permissions[style.to_s.to_sym] || @s3_permissions[:default])
122
- permission = permission.call(attachment, style) if permission.is_a?(Proc)
122
+ permission = permission.call(attachment, style) if permission.respond_to?(:call)
123
123
  (permission == :public_read) ? 'http' : 'https'
124
124
  end
125
125
  @s3_metadata = @options[:s3_metadata] || {}
126
126
  @s3_headers = @options[:s3_headers] || {}
127
- @s3_headers = @s3_headers.call(instance) if @s3_headers.is_a?(Proc)
127
+ @s3_headers = @s3_headers.call(instance) if @s3_headers.respond_to?(:call)
128
128
  @s3_headers = (@s3_headers).inject({}) do |headers,(name,value)|
129
129
  case name.to_s
130
130
  when /^x-amz-meta-(.*)/i
@@ -180,19 +180,19 @@ module Paperclip
180
180
 
181
181
  def s3_host_alias
182
182
  @s3_host_alias = @options[:s3_host_alias]
183
- @s3_host_alias = @s3_host_alias.call(self) if @s3_host_alias.is_a?(Proc)
183
+ @s3_host_alias = @s3_host_alias.call(self) if @s3_host_alias.respond_to?(:call)
184
184
  @s3_host_alias
185
185
  end
186
186
 
187
187
  def s3_url_options
188
188
  s3_url_options = @options[:s3_url_options] || {}
189
- s3_url_options = s3_url_options.call(instance) if s3_url_options.is_a?(Proc)
189
+ s3_url_options = s3_url_options.call(instance) if s3_url_options.respond_to?(:call)
190
190
  s3_url_options
191
191
  end
192
192
 
193
193
  def bucket_name
194
194
  @bucket = @options[:bucket] || s3_credentials[:bucket]
195
- @bucket = @bucket.call(self) if @bucket.is_a?(Proc)
195
+ @bucket = @bucket.call(self) if @bucket.respond_to?(:call)
196
196
  @bucket or raise ArgumentError, "missing required :bucket option"
197
197
  end
198
198
 
@@ -249,12 +249,8 @@ module Paperclip
249
249
  end
250
250
 
251
251
  def set_permissions permissions
252
- if permissions.is_a?(Hash)
253
- permissions[:default] = permissions[:default] || :public_read
254
- else
255
- permissions = { :default => permissions || :public_read }
256
- end
257
- permissions
252
+ permissions = { :default => permissions } unless permissions.respond_to?(:merge)
253
+ permissions.merge :default => (permissions[:default] || :public_read)
258
254
  end
259
255
 
260
256
  def parse_credentials creds
@@ -276,15 +272,15 @@ module Paperclip
276
272
 
277
273
  def s3_permissions(style = default_style)
278
274
  s3_permissions = @s3_permissions[style] || @s3_permissions[:default]
279
- s3_permissions = s3_permissions.call(self, style) if s3_permissions.is_a?(Proc)
275
+ s3_permissions = s3_permissions.call(self, style) if s3_permissions.respond_to?(:call)
280
276
  s3_permissions
281
277
  end
282
278
 
283
279
  def s3_protocol(style = default_style)
284
- protocol = if @s3_protocol.is_a?(Proc)
285
- @s3_protocol.call(style, self)
280
+ protocol = if @s3_protocol.respond_to?(:call)
281
+ @s3_protocol.call(style, self).to_s
286
282
  else
287
- @s3_protocol
283
+ @s3_protocol.to_s
288
284
  end
289
285
 
290
286
  protocol = protocol.split(":").first + ":" unless protocol.empty?
@@ -314,6 +310,8 @@ module Paperclip
314
310
  rescue AWS::S3::Errors::NoSuchBucket => e
315
311
  create_bucket
316
312
  retry
313
+ ensure
314
+ file.rewind
317
315
  end
318
316
  end
319
317
 
@@ -52,12 +52,12 @@ module Paperclip
52
52
  end
53
53
 
54
54
  def convert_options
55
- @convert_options.respond_to?(:call) ? @convert_options.call(attachment.instance) :
55
+ @convert_options.respond_to?(:call) ? @convert_options.call(attachment.instance) :
56
56
  (@convert_options || attachment.send(:extra_options_for, name))
57
57
  end
58
58
 
59
59
  def source_file_options
60
- @source_file_options.respond_to?(:call) ? @source_file_options.call(attachment.instance) :
60
+ @source_file_options.respond_to?(:call) ? @source_file_options.call(attachment.instance) :
61
61
  (@source_file_options || attachment.send(:extra_source_file_options_for, name))
62
62
  end
63
63
 
@@ -0,0 +1,21 @@
1
+ module Paperclip
2
+ class TempfileFactory
3
+
4
+ ILLEGAL_FILENAME_CHARACTERS = /^~/
5
+
6
+ def generate(name)
7
+ @name = name
8
+ file = Tempfile.new([basename, extension])
9
+ file.binmode
10
+ file
11
+ end
12
+
13
+ def extension
14
+ File.extname(@name)
15
+ end
16
+
17
+ def basename
18
+ File.basename(@name, extension).gsub(ILLEGAL_FILENAME_CHARACTERS, '_')
19
+ end
20
+ end
21
+ end
@@ -98,7 +98,16 @@ module Paperclip
98
98
 
99
99
  # Return true if the format is animated
100
100
  def animated?
101
- @animated && ANIMATED_FORMATS.include?(@current_format[1..-1]) && (ANIMATED_FORMATS.include?(@format.to_s) || @format.blank?)
101
+ @animated && (ANIMATED_FORMATS.include?(@format.to_s) || @format.blank?) && identified_as_animated?
102
+ end
103
+
104
+ # Return true if ImageMagick's +identify+ returns an animated format
105
+ def identified_as_animated?
106
+ ANIMATED_FORMATS.include? identify("-format %m :file", :file => "#{@file.path}[0]").to_s.downcase.strip
107
+ rescue Cocaine::ExitStatusError => e
108
+ raise Paperclip::Error, "There was an error running `identify` for #{@basename}" if @whiny
109
+ rescue Cocaine::CommandNotFoundError => e
110
+ raise Paperclip::Errors::CommandNotFoundError.new("Could not run the `identify` command. Please install ImageMagick.")
102
111
  end
103
112
  end
104
113
  end