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
@@ -137,11 +137,11 @@ module Paperclip
137
137
  attachment.fingerprint
138
138
  end
139
139
 
140
- # Returns a the attachment hash. See Paperclip::Attachment#hash for
140
+ # Returns a the attachment hash. See Paperclip::Attachment#hash_key for
141
141
  # more details.
142
142
  def hash attachment=nil, style_name=nil
143
143
  if attachment && style_name
144
- attachment.hash(style_name)
144
+ attachment.hash_key(style_name)
145
145
  else
146
146
  super()
147
147
  end
@@ -150,10 +150,13 @@ module Paperclip
150
150
  # Returns the id of the instance in a split path form. e.g. returns
151
151
  # 000/001/234 for an id of 1234.
152
152
  def id_partition attachment, style_name
153
- if (id = attachment.instance.id).is_a?(Integer)
153
+ case id = attachment.instance.id
154
+ when Integer
154
155
  ("%09d" % id).scan(/\d{3}/).join("/")
155
- else
156
+ when String
156
157
  id.scan(/.{3}/).first(3).join("/")
158
+ else
159
+ nil
157
160
  end
158
161
  end
159
162
 
@@ -5,13 +5,15 @@ require 'paperclip/matchers/validate_attachment_size_matcher'
5
5
 
6
6
  module Paperclip
7
7
  module Shoulda
8
- # Provides rspec-compatible matchers for testing Paperclip attachments.
8
+ # Provides RSpec-compatible & Test::Unit-compatible matchers for testing Paperclip attachments.
9
+ #
10
+ # *RSpec*
9
11
  #
10
12
  # In spec_helper.rb, you'll need to require the matchers:
11
13
  #
12
14
  # require "paperclip/matchers"
13
15
  #
14
- # And include the module:
16
+ # And _include_ the module:
15
17
  #
16
18
  # Spec::Runner.configure do |config|
17
19
  # config.include Paperclip::Shoulda::Matchers
@@ -27,6 +29,35 @@ module Paperclip
27
29
  # it { should validate_attachment_size(:avatar).
28
30
  # less_than(2.megabytes) }
29
31
  # end
32
+ #
33
+ #
34
+ # *TestUnit*
35
+ #
36
+ # In test_helper.rb, you'll need to require the matchers as well:
37
+ #
38
+ # require "paperclip/matchers"
39
+ #
40
+ # And _extend_ the module:
41
+ #
42
+ # class ActiveSupport::TestCase
43
+ # extend Paperclip::Shoulda::Matchers
44
+ #
45
+ # #...other initializers...#
46
+ # end
47
+ #
48
+ # Example:
49
+ # require 'test_helper'
50
+ #
51
+ # class UserTest < ActiveSupport::TestCase
52
+ # should have_attached_file(:avatar)
53
+ # should validate_attachment_presence(:avatar)
54
+ # should validate_attachment_content_type(:avatar).
55
+ # allowing('image/png', 'image/gif').
56
+ # rejecting('text/plain', 'text/xml')
57
+ # should validate_attachment_size(:avatar).
58
+ # less_than(2.megabytes)
59
+ # end
60
+ #
30
61
  module Matchers
31
62
  end
32
63
  end
@@ -70,7 +70,7 @@ module Paperclip
70
70
  Hash.new.tap do |missing_styles|
71
71
  current_styles.each do |klass, attachment_definitions|
72
72
  attachment_definitions.each do |attachment_name, styles|
73
- registered = registered_styles[klass][attachment_name] rescue []
73
+ registered = registered_styles[klass][attachment_name] || [] rescue []
74
74
  missed = styles - registered
75
75
  if missed.present?
76
76
  klass_sym = klass.to_s.to_sym
@@ -1,4 +1,5 @@
1
1
  require 'paperclip'
2
+ require 'paperclip/schema'
2
3
 
3
4
  module Paperclip
4
5
  if defined? Rails::Railtie
@@ -21,6 +22,10 @@ module Paperclip
21
22
  File.send(:include, Paperclip::Upfile)
22
23
 
23
24
  Paperclip.options[:logger] = defined?(ActiveRecord) ? ActiveRecord::Base.logger : Rails.logger
25
+
26
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.send(:include, Paperclip::Schema)
27
+ ActiveRecord::ConnectionAdapters::Table.send(:include, Paperclip::Schema)
28
+ ActiveRecord::ConnectionAdapters::TableDefinition.send(:include, Paperclip::Schema)
24
29
  end
25
30
  end
26
31
  end
@@ -0,0 +1,39 @@
1
+ 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.
8
+ module Schema
9
+ @@columns = {:file_name => :string,
10
+ :content_type => :string,
11
+ :file_size => :integer,
12
+ :updated_at => :datetime}
13
+
14
+ def has_attached_file(attachment_name)
15
+ with_columns_for(attachment_name) do |column_name, column_type|
16
+ column(column_name, column_type)
17
+ end
18
+ end
19
+
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)
23
+ end
24
+ end
25
+
26
+ protected
27
+
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
32
+ end
33
+ end
34
+
35
+ def full_column_name(attachment_name, column_name)
36
+ "#{attachment_name}_#{column_name}".to_sym
37
+ end
38
+ end
39
+ end
@@ -41,9 +41,9 @@ module Paperclip
41
41
  end unless defined?(Fog)
42
42
 
43
43
  base.instance_eval do
44
- unless @options.url.to_s.match(/^:fog.*url$/)
45
- @options.path = @options.path.gsub(/:url/, @options.url)
46
- @options.url = ':fog_public_url'
44
+ unless @options[:url].to_s.match(/^:fog.*url$/)
45
+ @options[:path] = @options[:path].gsub(/:url/, @options[:url])
46
+ @options[:url] = ':fog_public_url'
47
47
  end
48
48
  Paperclip.interpolates(:fog_public_url) do |attachment, style|
49
49
  attachment.public_url(style)
@@ -51,6 +51,8 @@ module Paperclip
51
51
  end
52
52
  end
53
53
 
54
+ AWS_BUCKET_SUBDOMAIN_RESTRICTON_REGEX = /^(?:[a-z]|\d(?!\d{0,2}(?:\.\d{1,3}){3}$))(?:[a-z0-9]|\.(?![\.\-])|\-(?![\.])){1,61}[a-z0-9]$/
55
+
54
56
  def exists?(style = default_style)
55
57
  if original_filename
56
58
  !!directory.files.head(path(style))
@@ -60,16 +62,16 @@ module Paperclip
60
62
  end
61
63
 
62
64
  def fog_credentials
63
- @fog_credentials ||= parse_credentials(@options.fog_credentials)
65
+ @fog_credentials ||= parse_credentials(@options[:fog_credentials])
64
66
  end
65
67
 
66
68
  def fog_file
67
- @fog_file ||= @options.fog_file || {}
69
+ @fog_file ||= @options[:fog_file] || {}
68
70
  end
69
71
 
70
72
  def fog_public
71
73
  return @fog_public if defined?(@fog_public)
72
- @fog_public = defined?(@options.fog_public) ? @options.fog_public : true
74
+ @fog_public = defined?(@options[:fog_public]) ? @options[:fog_public] : true
73
75
  end
74
76
 
75
77
  def flush_writes
@@ -122,11 +124,20 @@ module Paperclip
122
124
  end
123
125
 
124
126
  def public_url(style = default_style)
125
- if @options.fog_host
126
- host = (@options.fog_host =~ /%d/) ? @options.fog_host % (path(style).hash % 4) : @options.fog_host
127
+ if @options[:fog_host]
128
+ host = (@options[:fog_host] =~ /%d/) ? @options[:fog_host] % (path(style).hash % 4) : @options[:fog_host]
127
129
  "#{host}/#{path(style)}"
128
130
  else
129
- directory.files.new(:key => path(style)).public_url
131
+ if fog_credentials[:provider] == 'AWS'
132
+ if @options[:fog_directory].to_s =~ Fog::AWS_BUCKET_SUBDOMAIN_RESTRICTON_REGEX
133
+ "https://#{@options[:fog_directory]}.s3.amazonaws.com/#{path(style)}"
134
+ else
135
+ # directory is not a valid subdomain, so use path style for access
136
+ "https://s3.amazonaws.com/#{@options[:fog_directory]}/#{path(style)}"
137
+ end
138
+ else
139
+ directory.files.new(:key => path(style)).public_url
140
+ end
130
141
  end
131
142
  end
132
143
 
@@ -156,7 +167,7 @@ module Paperclip
156
167
  end
157
168
 
158
169
  def directory
159
- @directory ||= connection.directories.new(:key => @options.fog_directory)
170
+ @directory ||= connection.directories.new(:key => @options[:fog_directory])
160
171
  end
161
172
  end
162
173
  end
@@ -2,6 +2,9 @@ module Paperclip
2
2
  module Storage
3
3
  # Amazon's S3 file hosting service is a scalable, easy place to store files for
4
4
  # distribution. You can find out more about it at http://aws.amazon.com/s3
5
+ #
6
+ # To use Paperclip with S3, include the +aws-sdk+ gem in your Gemfile:
7
+ # gem 'aws-sdk'
5
8
  # There are a few S3-specific options for has_attached_file:
6
9
  # * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
7
10
  # to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
@@ -38,7 +41,9 @@ module Paperclip
38
41
  # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
39
42
  # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are :public_read (the
40
43
  # default), and 'https' when your :s3_permissions are anything else.
41
- # * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate}
44
+ # * +s3_headers+: A hash of headers or a Proc. You may specify a hash such as
45
+ # {'Expires' => 1.year.from_now.httpdate}. If you use a Proc, headers are determined at
46
+ # runtime. Paperclip will call that Proc with attachment as the only argument.
42
47
  # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
43
48
  # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
44
49
  # Paperclip will attempt to create it. The bucket name will not be interpolated.
@@ -67,41 +72,56 @@ module Paperclip
67
72
  # S3 (strictly speaking) does not support directories, you can still use a / to
68
73
  # separate parts of your file name.
69
74
  # * +s3_host_name+: If you are using your bucket in Tokyo region etc, write host_name.
75
+ # * +s3_metadata+: These key/value pairs will be stored with the
76
+ # object. This option works by prefixing each key with
77
+ # "x-amz-meta-" before sending it as a header on the object
78
+ # upload request.
79
+ # * +s3_storage_class+: If this option is set to
80
+ # <tt>:reduced_redundancy</tt>, the object will be stored using Reduced
81
+ # Redundancy Storage. RRS enables customers to reduce their
82
+ # costs by storing non-critical, reproducible data at lower
83
+ # levels of redundancy than Amazon S3's standard storage.
70
84
  module S3
71
85
  def self.extended base
72
86
  begin
73
- require 'aws/s3'
87
+ require 'aws-sdk'
74
88
  rescue LoadError => e
75
- e.message << " (You may need to install the aws-s3 gem)"
89
+ e.message << " (You may need to install the aws-sdk gem)"
76
90
  raise e
77
- end unless defined?(AWS::S3)
91
+ end unless defined?(AWS::Core)
78
92
 
79
93
  base.instance_eval do
80
- @s3_options = @options.s3_options || {}
81
- @s3_permissions = set_permissions(@options.s3_permissions)
82
- @s3_protocol = @options.s3_protocol ||
94
+ @s3_options = @options[:s3_options] || {}
95
+ @s3_permissions = set_permissions(@options[:s3_permissions])
96
+ @s3_protocol = @options[:s3_protocol] ||
83
97
  Proc.new do |style, attachment|
84
98
  permission = (@s3_permissions[style.to_sym] || @s3_permissions[:default])
85
99
  permission = permission.call(attachment, style) if permission.is_a?(Proc)
86
100
  (permission == :public_read) ? 'http' : 'https'
87
101
  end
88
- @s3_headers = @options.s3_headers || {}
89
-
90
- unless @options.url.to_s.match(/^:s3.*url$/) || @options.url == ":asset_host"
91
- @options.path = @options.path.gsub(/:url/, @options.url).gsub(/^:rails_root\/public\/system/, '')
92
- @options.url = ":s3_path_url"
102
+ @s3_metadata = @options[:s3_metadata] || {}
103
+ @s3_headers = @options[:s3_headers] || {}
104
+ @s3_headers = @s3_headers.call(instance) if @s3_headers.is_a?(Proc)
105
+ @s3_headers = (@s3_headers).inject({}) do |headers,(name,value)|
106
+ case name.to_s
107
+ when /^x-amz-meta-(.*)/i
108
+ @s3_metadata[$1.downcase] = value
109
+ else
110
+ name = name.to_s.downcase.sub(/^x-amz-/,'').tr("-","_").to_sym
111
+ headers[name] = value
112
+ end
113
+ headers
93
114
  end
94
- @options.url = @options.url.inspect if @options.url.is_a?(Symbol)
95
115
 
96
- @http_proxy = @options.http_proxy || nil
97
- if @http_proxy
98
- @s3_options.merge!({:proxy => @http_proxy})
116
+ @s3_headers[:storage_class] = @options[:s3_storage_class] if @options[:s3_storage_class]
117
+
118
+ unless @options[:url].to_s.match(/^:s3.*url$/) || @options[:url] == ":asset_host"
119
+ @options[:path] = @options[:path].gsub(/:url/, @options[:url]).gsub(/^:rails_root\/public\/system/, '')
120
+ @options[:url] = ":s3_path_url"
99
121
  end
122
+ @options[:url] = @options[:url].inspect if @options[:url].is_a?(Symbol)
100
123
 
101
- AWS::S3::Base.establish_connection!( @s3_options.merge(
102
- :access_key_id => s3_credentials[:access_key_id],
103
- :secret_access_key => s3_credentials[:secret_access_key]
104
- ))
124
+ @http_proxy = @options[:http_proxy] || nil
105
125
  end
106
126
  Paperclip.interpolates(:s3_alias_url) do |attachment, style|
107
127
  "#{attachment.s3_protocol(style)}://#{attachment.s3_host_alias}/#{attachment.path(style).gsub(%r{^/}, "")}"
@@ -118,27 +138,61 @@ module Paperclip
118
138
  end
119
139
 
120
140
  def expiring_url(time = 3600, style_name = default_style)
121
- AWS::S3::S3Object.url_for(path(style_name), bucket_name, :expires_in => time, :use_ssl => (s3_protocol(style_name) == 'https'))
141
+ if path
142
+ s3_object(style_name).url_for(:read, :expires => time, :secure => use_secure_protocol?(style_name)).to_s
143
+ end
122
144
  end
123
145
 
124
146
  def s3_credentials
125
- @s3_credentials ||= parse_credentials(@options.s3_credentials)
147
+ @s3_credentials ||= parse_credentials(@options[:s3_credentials])
126
148
  end
127
149
 
128
150
  def s3_host_name
129
- @options.s3_host_name || s3_credentials[:s3_host_name] || "s3.amazonaws.com"
151
+ @options[:s3_host_name] || s3_credentials[:s3_host_name] || "s3.amazonaws.com"
130
152
  end
131
153
 
132
154
  def s3_host_alias
133
- @s3_host_alias = @options.s3_host_alias
155
+ @s3_host_alias = @options[:s3_host_alias]
134
156
  @s3_host_alias = @s3_host_alias.call(self) if @s3_host_alias.is_a?(Proc)
135
157
  @s3_host_alias
136
158
  end
137
159
 
138
160
  def bucket_name
139
- @bucket = @options.bucket || s3_credentials[:bucket]
161
+ @bucket = @options[:bucket] || s3_credentials[:bucket]
140
162
  @bucket = @bucket.call(self) if @bucket.is_a?(Proc)
141
- @bucket
163
+ @bucket or raise ArgumentError, "missing required :bucket option"
164
+ end
165
+
166
+ def s3_interface
167
+ @s3_interface ||= begin
168
+ config = { :s3_endpoint => s3_host_name }
169
+
170
+ if using_http_proxy?
171
+
172
+ proxy_opts = { :host => http_proxy_host }
173
+ proxy_opts[:port] = http_proxy_port if http_proxy_port
174
+ if http_proxy_user
175
+ userinfo = http_proxy_user.to_s
176
+ userinfo += ":#{http_proxy_password}" if http_proxy_password
177
+ proxy_opts[:userinfo] = userinfo
178
+ end
179
+ config[:proxy_uri] = URI::HTTP.build(proxy_opts)
180
+ end
181
+
182
+ [:access_key_id, :secret_access_key].each do |opt|
183
+ config[opt] = s3_credentials[opt] if s3_credentials[opt]
184
+ end
185
+
186
+ AWS::S3.new(config.merge(@s3_options))
187
+ end
188
+ end
189
+
190
+ def s3_bucket
191
+ @s3_bucket ||= s3_interface.buckets[bucket_name]
192
+ end
193
+
194
+ def s3_object style_name = default_style
195
+ s3_bucket.objects[path(style_name).sub(%r{^/},'')]
142
196
  end
143
197
 
144
198
  def using_http_proxy?
@@ -178,7 +232,7 @@ module Paperclip
178
232
 
179
233
  def exists?(style = default_style)
180
234
  if original_filename
181
- AWS::S3::S3Object.exists?(path(style), bucket_name)
235
+ s3_object(style).exists?
182
236
  else
183
237
  false
184
238
  end
@@ -207,30 +261,31 @@ module Paperclip
207
261
  basename = File.basename(filename, extname)
208
262
  file = Tempfile.new([basename, extname])
209
263
  file.binmode
210
- file.write(AWS::S3::S3Object.value(path(style), bucket_name))
264
+ file.write(s3_object(style).read)
211
265
  file.rewind
212
266
  return file
213
267
  end
214
268
 
215
269
  def create_bucket
216
- AWS::S3::Bucket.create(bucket_name)
270
+ s3_interface.buckets.create(bucket_name)
217
271
  end
218
272
 
219
273
  def flush_writes #:nodoc:
220
274
  @queued_for_write.each do |style, file|
221
275
  begin
222
276
  log("saving #{path(style)}")
223
- AWS::S3::S3Object.store(path(style),
224
- file,
225
- bucket_name,
226
- {:content_type => file.content_type.to_s.strip,
227
- :access => s3_permissions(style),
228
- }.merge(@s3_headers))
229
- rescue AWS::S3::NoSuchBucket => e
277
+ acl = @s3_permissions[style] || @s3_permissions[:default]
278
+ acl = acl.call(self, style) if acl.respond_to?(:call)
279
+ write_options = {
280
+ :content_type => file.content_type.to_s.strip,
281
+ :acl => acl
282
+ }
283
+ write_options[:metadata] = @s3_metadata unless @s3_metadata.empty?
284
+ write_options.merge!(@s3_headers)
285
+ s3_object(style).write(file, write_options)
286
+ rescue AWS::S3::Errors::NoSuchBucket => e
230
287
  create_bucket
231
288
  retry
232
- rescue AWS::S3::ResponseError => e
233
- raise
234
289
  end
235
290
  end
236
291
 
@@ -243,8 +298,8 @@ module Paperclip
243
298
  @queued_for_delete.each do |path|
244
299
  begin
245
300
  log("deleting #{path}")
246
- AWS::S3::S3Object.delete(path, bucket_name)
247
- rescue AWS::S3::ResponseError
301
+ s3_bucket.objects[path.sub(%r{^/},'')].delete
302
+ rescue AWS::Errors::Base => e
248
303
  # Ignore this.
249
304
  end
250
305
  end
@@ -265,6 +320,18 @@ module Paperclip
265
320
  end
266
321
  private :find_credentials
267
322
 
323
+ def establish_connection!
324
+ @connection ||= AWS::S3::Base.establish_connection!( @s3_options.merge(
325
+ :access_key_id => s3_credentials[:access_key_id],
326
+ :secret_access_key => s3_credentials[:secret_access_key]
327
+ ))
328
+ end
329
+ private :establish_connection!
330
+
331
+ def use_secure_protocol?(style_name)
332
+ s3_protocol(style_name) == "https"
333
+ end
334
+ private :use_secure_protocol?
268
335
  end
269
336
  end
270
337
  end