carrierwave 0.9.0 → 2.1.1

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

Potentially problematic release.


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

Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +362 -116
  3. data/lib/carrierwave/compatibility/paperclip.rb +29 -21
  4. data/lib/carrierwave/downloader/base.rb +83 -0
  5. data/lib/carrierwave/downloader/remote_file.rb +65 -0
  6. data/lib/carrierwave/error.rb +1 -0
  7. data/lib/carrierwave/locale/en.yml +7 -4
  8. data/lib/carrierwave/mount.rb +238 -186
  9. data/lib/carrierwave/mounter.rb +188 -0
  10. data/lib/carrierwave/orm/activerecord.rb +60 -24
  11. data/lib/carrierwave/processing/mini_magick.rb +139 -78
  12. data/lib/carrierwave/processing/rmagick.rb +68 -23
  13. data/lib/carrierwave/processing.rb +0 -1
  14. data/lib/carrierwave/sanitized_file.rb +67 -27
  15. data/lib/carrierwave/storage/abstract.rb +15 -2
  16. data/lib/carrierwave/storage/file.rb +69 -2
  17. data/lib/carrierwave/storage/fog.rb +180 -41
  18. data/lib/carrierwave/storage.rb +1 -7
  19. data/lib/carrierwave/test/matchers.rb +77 -12
  20. data/lib/carrierwave/uploader/cache.rb +74 -38
  21. data/lib/carrierwave/uploader/callbacks.rb +0 -2
  22. data/lib/carrierwave/uploader/configuration.rb +72 -6
  23. data/lib/carrierwave/uploader/content_type_blacklist.rb +48 -0
  24. data/lib/carrierwave/uploader/content_type_whitelist.rb +48 -0
  25. data/lib/carrierwave/uploader/default_url.rb +3 -5
  26. data/lib/carrierwave/uploader/download.rb +5 -69
  27. data/lib/carrierwave/uploader/extension_blacklist.rb +14 -10
  28. data/lib/carrierwave/uploader/extension_whitelist.rb +13 -10
  29. data/lib/carrierwave/uploader/file_size.rb +43 -0
  30. data/lib/carrierwave/uploader/mountable.rb +13 -8
  31. data/lib/carrierwave/uploader/processing.rb +15 -17
  32. data/lib/carrierwave/uploader/proxy.rb +16 -7
  33. data/lib/carrierwave/uploader/remove.rb +0 -2
  34. data/lib/carrierwave/uploader/serialization.rb +3 -5
  35. data/lib/carrierwave/uploader/store.rb +17 -24
  36. data/lib/carrierwave/uploader/url.rb +3 -5
  37. data/lib/carrierwave/uploader/versions.rb +117 -86
  38. data/lib/carrierwave/uploader.rb +6 -2
  39. data/lib/carrierwave/utilities/uri.rb +5 -6
  40. data/lib/carrierwave/utilities.rb +1 -3
  41. data/lib/carrierwave/validations/active_model.rb +3 -7
  42. data/lib/carrierwave/version.rb +1 -1
  43. data/lib/carrierwave.rb +36 -3
  44. data/lib/generators/templates/uploader.rb +4 -8
  45. data/lib/generators/uploader_generator.rb +1 -1
  46. metadata +195 -94
  47. data/lib/carrierwave/locale/cs.yml +0 -11
  48. data/lib/carrierwave/locale/de.yml +0 -11
  49. data/lib/carrierwave/locale/nl.yml +0 -11
  50. data/lib/carrierwave/locale/sk.yml +0 -11
  51. data/lib/carrierwave/processing/mime_types.rb +0 -73
@@ -1,7 +1,3 @@
1
- # encoding: utf-8
2
-
3
- require "fog"
4
-
5
1
  module CarrierWave
6
2
  module Storage
7
3
 
@@ -20,6 +16,8 @@ module CarrierWave
20
16
  # [:fog_authenticated_url_expiration] (optional) time (in seconds) that authenticated urls
21
17
  # will be valid, when fog_public is false and provider is AWS or Google, defaults to 600
22
18
  # [:fog_use_ssl_for_aws] (optional) #public_url will use https for the AWS generated URL]
19
+ # [:fog_aws_accelerate] (optional) #public_url will use s3-accelerate subdomain
20
+ # instead of s3, defaults to false
23
21
  #
24
22
  #
25
23
  # AWS credentials contain the following keys:
@@ -27,7 +25,7 @@ module CarrierWave
27
25
  # [:aws_access_key_id]
28
26
  # [:aws_secret_access_key]
29
27
  # [:region] (optional) defaults to 'us-east-1'
30
- # :region should be one of ['eu-west-1', 'us-east-1', 'ap-southeast-1', 'us-west-1', 'ap-northeast-1']
28
+ # :region should be one of ['eu-west-1', 'us-east-1', 'ap-southeast-1', 'us-west-1', 'ap-northeast-1', 'eu-central-1']
31
29
  #
32
30
  #
33
31
  # Google credentials contain the following keys:
@@ -62,6 +60,14 @@ module CarrierWave
62
60
  def connection_cache
63
61
  @connection_cache ||= {}
64
62
  end
63
+
64
+ def eager_load
65
+ # see #1198. This will hopefully no longer be necessary in future release of fog
66
+ fog_credentials = CarrierWave::Uploader::Base.fog_credentials
67
+ if fog_credentials.present?
68
+ CarrierWave::Storage::Fog.connection_cache[fog_credentials] ||= ::Fog::Storage.new(fog_credentials)
69
+ end
70
+ end
65
71
  end
66
72
 
67
73
  ##
@@ -96,6 +102,57 @@ module CarrierWave
96
102
  CarrierWave::Storage::Fog::File.new(uploader, self, uploader.store_path(identifier))
97
103
  end
98
104
 
105
+ ##
106
+ # Stores given file to cache directory.
107
+ #
108
+ # === Parameters
109
+ #
110
+ # [new_file (File, IOString, Tempfile)] any kind of file object
111
+ #
112
+ # === Returns
113
+ #
114
+ # [CarrierWave::SanitizedFile] a sanitized file
115
+ #
116
+ def cache!(new_file)
117
+ f = CarrierWave::Storage::Fog::File.new(uploader, self, uploader.cache_path)
118
+ f.store(new_file)
119
+ f
120
+ end
121
+
122
+ ##
123
+ # Retrieves the file with the given cache_name from the cache.
124
+ #
125
+ # === Parameters
126
+ #
127
+ # [cache_name (String)] uniquely identifies a cache file
128
+ #
129
+ # === Raises
130
+ #
131
+ # [CarrierWave::InvalidParameter] if the cache_name is incorrectly formatted.
132
+ #
133
+ def retrieve_from_cache!(identifier)
134
+ CarrierWave::Storage::Fog::File.new(uploader, self, uploader.cache_path(identifier))
135
+ end
136
+
137
+ ##
138
+ # Deletes a cache dir
139
+ #
140
+ def delete_dir!(path)
141
+ # do nothing, because there's no such things as 'empty directory'
142
+ end
143
+
144
+ def clean_cache!(seconds)
145
+ connection.directories.new(
146
+ :key => uploader.fog_directory,
147
+ :public => uploader.fog_public
148
+ ).files.all(:prefix => uploader.cache_dir).each do |file|
149
+ # generate_cache_id returns key formated TIMEINT-PID(-COUNTER)-RND
150
+ time = file.key.scan(/(\d+)-\d+-\d+(?:-\d+)?/).first.map { |t| t.to_i }
151
+ time = Time.at(*time)
152
+ file.destroy if time < (Time.now.utc - seconds)
153
+ end
154
+ end
155
+
99
156
  def connection
100
157
  @connection ||= begin
101
158
  options = credentials = uploader.fog_credentials
@@ -104,6 +161,8 @@ module CarrierWave
104
161
  end
105
162
 
106
163
  class File
164
+ DEFAULT_S3_REGION = 'us-east-1'
165
+
107
166
  include CarrierWave::Utilities::Uri
108
167
 
109
168
  ##
@@ -128,7 +187,7 @@ module CarrierWave
128
187
 
129
188
  ##
130
189
  # Return a temporary authenticated url to a private file, if available
131
- # Only supported for AWS, Rackspace and Google providers
190
+ # Only supported for AWS, Rackspace, Google, AzureRM and Aliyun providers
132
191
  #
133
192
  # === Returns
134
193
  #
@@ -137,19 +196,28 @@ module CarrierWave
137
196
  # [NilClass] no authenticated url available
138
197
  #
139
198
  def authenticated_url(options = {})
140
- if ['AWS', 'Google', 'Rackspace'].include?(@uploader.fog_credentials[:provider])
199
+ if ['AWS', 'Google', 'Rackspace', 'OpenStack', 'AzureRM', 'Aliyun', 'backblaze'].include?(@uploader.fog_credentials[:provider])
141
200
  # avoid a get by using local references
142
201
  local_directory = connection.directories.new(:key => @uploader.fog_directory)
143
202
  local_file = local_directory.files.new(:key => path)
144
- if @uploader.fog_credentials[:provider] == "AWS"
145
- local_file.url(::Fog::Time.now + @uploader.fog_authenticated_url_expiration, options)
146
- elsif @uploader.fog_credentials[:provider] == "Rackspace"
147
- connection.get_object_https_url(@uploader.fog_directory, path, ::Fog::Time.now + @uploader.fog_authenticated_url_expiration)
148
- else
149
- local_file.url(::Fog::Time.now + @uploader.fog_authenticated_url_expiration)
203
+ expire_at = options[:expire_at] || ::Fog::Time.now + @uploader.fog_authenticated_url_expiration
204
+ case @uploader.fog_credentials[:provider]
205
+ when 'AWS', 'Google'
206
+ # Older versions of fog-google do not support options as a parameter
207
+ if url_options_supported?(local_file)
208
+ local_file.url(expire_at, options)
209
+ else
210
+ warn "Options hash not supported in #{local_file.class}. You may need to upgrade your Fog provider."
211
+ local_file.url(expire_at)
212
+ end
213
+ when 'Rackspace', 'OpenStack'
214
+ connection.get_object_https_url(@uploader.fog_directory, path, expire_at, options)
215
+ when 'Aliyun'
216
+ expire_at = expire_at - Time.now
217
+ local_file.url(expire_at)
218
+ else
219
+ local_file.url(expire_at)
150
220
  end
151
- else
152
- nil
153
221
  end
154
222
  end
155
223
 
@@ -161,7 +229,7 @@ module CarrierWave
161
229
  # [String] value of content-type
162
230
  #
163
231
  def content_type
164
- @content_type || file.content_type
232
+ @content_type || file.try(:content_type)
165
233
  end
166
234
 
167
235
  ##
@@ -184,7 +252,9 @@ module CarrierWave
184
252
  #
185
253
  def delete
186
254
  # avoid a get by just using local reference
187
- directory.files.new(:key => path).destroy
255
+ directory.files.new(:key => path).destroy.tap do |result|
256
+ @file = nil if result
257
+ end
188
258
  end
189
259
 
190
260
  ##
@@ -192,10 +262,11 @@ module CarrierWave
192
262
  #
193
263
  # === Returns
194
264
  #
195
- # [String] extension of file
265
+ # [String] extension of file or nil if the file has no extension
196
266
  #
197
267
  def extension
198
- path.split('.').last
268
+ path_elements = path.split('.')
269
+ path_elements.last if path_elements.size > 1
199
270
  end
200
271
 
201
272
  ##
@@ -214,7 +285,7 @@ module CarrierWave
214
285
  end
215
286
 
216
287
  def initialize(uploader, base, path)
217
- @uploader, @base, @path = uploader, base, path
288
+ @uploader, @base, @path, @content_type = uploader, base, path, nil
218
289
  end
219
290
 
220
291
  ##
@@ -224,6 +295,16 @@ module CarrierWave
224
295
  #
225
296
  # [String] contents of file
226
297
  def read
298
+ file_body = file.body
299
+
300
+ return if file_body.nil?
301
+ return file_body unless file_body.is_a?(::File)
302
+
303
+ # Fog::Storage::XXX::File#body could return the source file which was upoloaded to the remote server.
304
+ read_source_file(file_body) if ::File.exist?(file_body.path)
305
+
306
+ # If the source file doesn't exist, the remote content is read
307
+ @file = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables
227
308
  file.body
228
309
  end
229
310
 
@@ -235,7 +316,7 @@ module CarrierWave
235
316
  # [Integer] size of file body
236
317
  #
237
318
  def size
238
- file.content_length
319
+ file.nil? ? 0 : file.content_length
239
320
  end
240
321
 
241
322
  ##
@@ -245,7 +326,7 @@ module CarrierWave
245
326
  #
246
327
  # [Boolean] true if file exists or false
247
328
  def exists?
248
- !!directory.files.head(path)
329
+ !!file
249
330
  end
250
331
 
251
332
  ##
@@ -255,15 +336,19 @@ module CarrierWave
255
336
  #
256
337
  # [Boolean] true on success or raises error
257
338
  def store(new_file)
258
- fog_file = new_file.to_file
259
- @content_type ||= new_file.content_type
260
- @file = directory.files.create({
261
- :body => fog_file ? fog_file : new_file.read,
262
- :content_type => @content_type,
263
- :key => path,
264
- :public => @uploader.fog_public
265
- }.merge(@uploader.fog_attributes))
266
- fog_file.close if fog_file && !fog_file.closed?
339
+ if new_file.is_a?(self.class)
340
+ new_file.copy_to(path)
341
+ else
342
+ fog_file = new_file.to_file
343
+ @content_type ||= new_file.content_type
344
+ @file = directory.files.create({
345
+ :body => fog_file ? fog_file : new_file.read,
346
+ :content_type => @content_type,
347
+ :key => path,
348
+ :public => @uploader.fog_public
349
+ }.merge(@uploader.fog_attributes))
350
+ fog_file.close if fog_file && !fog_file.closed?
351
+ end
267
352
  true
268
353
  end
269
354
 
@@ -286,23 +371,35 @@ module CarrierWave
286
371
  end
287
372
  else
288
373
  # AWS/Google optimized for speed over correctness
289
- case @uploader.fog_credentials[:provider]
374
+ case fog_provider
290
375
  when 'AWS'
291
376
  # check if some endpoint is set in fog_credentials
292
377
  if @uploader.fog_credentials.has_key?(:endpoint)
293
378
  "#{@uploader.fog_credentials[:endpoint]}/#{@uploader.fog_directory}/#{encoded_path}"
294
379
  else
295
380
  protocol = @uploader.fog_use_ssl_for_aws ? "https" : "http"
381
+
382
+ subdomain_regex = /^(?:[a-z]|\d(?!\d{0,2}(?:\d{1,3}){3}$))(?:[a-z0-9\.]|(?![\-])|\-(?![\.])){1,61}[a-z0-9]$/
383
+ valid_subdomain = @uploader.fog_directory.to_s =~ subdomain_regex && !(protocol == 'https' && @uploader.fog_directory =~ /\./)
384
+
296
385
  # if directory is a valid subdomain, use that style for access
297
- if @uploader.fog_directory.to_s =~ /^(?:[a-z]|\d(?!\d{0,2}(?:\d{1,3}){3}$))(?:[a-z0-9\.]|(?![\-])|\-(?![\.])){1,61}[a-z0-9]$/
298
- "#{protocol}://#{@uploader.fog_directory}.s3.amazonaws.com/#{encoded_path}"
299
- else
300
- # directory is not a valid subdomain, so use path style for access
301
- "#{protocol}://s3.amazonaws.com/#{@uploader.fog_directory}/#{encoded_path}"
386
+ if valid_subdomain
387
+ s3_subdomain = @uploader.fog_aws_accelerate ? "s3-accelerate" : "s3"
388
+ "#{protocol}://#{@uploader.fog_directory}.#{s3_subdomain}.amazonaws.com/#{encoded_path}"
389
+ else # directory is not a valid subdomain, so use path style for access
390
+ region = @uploader.fog_credentials[:region].to_s
391
+ host = case region
392
+ when DEFAULT_S3_REGION, ''
393
+ 's3.amazonaws.com'
394
+ else
395
+ "s3.#{region}.amazonaws.com"
396
+ end
397
+ "#{protocol}://#{host}/#{@uploader.fog_directory}/#{encoded_path}"
302
398
  end
303
399
  end
304
400
  when 'Google'
305
- "https://commondatastorage.googleapis.com/#{@uploader.fog_directory}/#{encoded_path}"
401
+ # https://cloud.google.com/storage/docs/access-public-data
402
+ "https://storage.googleapis.com/#{@uploader.fog_directory}/#{encoded_path}"
306
403
  else
307
404
  # avoid a get by just using local reference
308
405
  directory.files.new(:key => path).public_url
@@ -337,9 +434,24 @@ module CarrierWave
337
434
  # [NilClass] no file name available
338
435
  #
339
436
  def filename(options = {})
340
- if file_url = url(options)
341
- URI.decode(file_url).gsub(/.*\/(.*?$)/, '\1')
342
- end
437
+ return unless file_url = url(options)
438
+ CGI.unescape(file_url.split('?').first).gsub(/.*\/(.*?$)/, '\1')
439
+ end
440
+
441
+ ##
442
+ # Creates a copy of this file and returns it.
443
+ #
444
+ # === Parameters
445
+ #
446
+ # [new_path (String)] The path where the file should be copied to.
447
+ #
448
+ # === Returns
449
+ #
450
+ # @return [CarrierWave::Storage::Fog::File] the location where the file will be stored.
451
+ #
452
+ def copy_to(new_path)
453
+ connection.copy_object(@uploader.fog_directory, file.key, @uploader.fog_directory, new_path, acl_header)
454
+ CarrierWave::Storage::Fog::File.new(@uploader, @base, new_path)
343
455
  end
344
456
 
345
457
  private
@@ -382,6 +494,33 @@ module CarrierWave
382
494
  @file ||= directory.files.head(path)
383
495
  end
384
496
 
497
+ def acl_header
498
+ if fog_provider == 'AWS'
499
+ { 'x-amz-acl' => @uploader.fog_public ? 'public-read' : 'private' }
500
+ else
501
+ {}
502
+ end
503
+ end
504
+
505
+ def fog_provider
506
+ @uploader.fog_credentials[:provider].to_s
507
+ end
508
+
509
+ def read_source_file(file_body)
510
+ return unless ::File.exist?(file_body.path)
511
+
512
+ begin
513
+ file_body = ::File.open(file_body.path) if file_body.closed? # Reopen if it's already closed
514
+ file_body.read
515
+ ensure
516
+ file_body.close
517
+ end
518
+ end
519
+
520
+ def url_options_supported?(local_file)
521
+ parameters = local_file.method(:url).parameters
522
+ parameters.count == 2 && parameters[1].include?(:options)
523
+ end
385
524
  end
386
525
 
387
526
  end # Fog
@@ -1,9 +1,3 @@
1
1
  require "carrierwave/storage/abstract"
2
2
  require "carrierwave/storage/file"
3
-
4
- begin
5
- require "fog"
6
- rescue LoadError
7
- end
8
-
9
- require "carrierwave/storage/fog" if defined?(Fog)
3
+ require "carrierwave/storage/fog"
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  module CarrierWave
4
2
  module Test
5
3
 
@@ -23,13 +21,16 @@ module CarrierWave
23
21
  "expected #{@actual.inspect} to be identical to #{@expected.inspect}"
24
22
  end
25
23
 
26
- def negative_failure_message
24
+ def failure_message_when_negated
27
25
  "expected #{@actual.inspect} to not be identical to #{@expected.inspect}"
28
26
  end
29
27
 
30
28
  def description
31
29
  "be identical to #{@expected.inspect}"
32
30
  end
31
+
32
+ # RSpec 2 compatibility:
33
+ alias_method :negative_failure_message, :failure_message_when_negated
33
34
  end
34
35
 
35
36
  def be_identical_to(expected)
@@ -51,13 +52,16 @@ module CarrierWave
51
52
  "expected #{@actual.current_path.inspect} to have permissions #{@expected.to_s(8)}, but they were #{(File.stat(@actual.path).mode & 0777).to_s(8)}"
52
53
  end
53
54
 
54
- def negative_failure_message
55
+ def failure_message_when_negated
55
56
  "expected #{@actual.current_path.inspect} not to have permissions #{@expected.to_s(8)}, but it did"
56
57
  end
57
58
 
58
59
  def description
59
60
  "have permissions #{@expected.to_s(8)}"
60
61
  end
62
+
63
+ # RSpec 2 compatibility:
64
+ alias_method :negative_failure_message, :failure_message_when_negated
61
65
  end
62
66
 
63
67
  def have_permissions(expected)
@@ -79,13 +83,16 @@ module CarrierWave
79
83
  "expected #{File.dirname @actual.current_path.inspect} to have permissions #{@expected.to_s(8)}, but they were #{(File.stat(@actual.path).mode & 0777).to_s(8)}"
80
84
  end
81
85
 
82
- def negative_failure_message
86
+ def failure_message_when_negated
83
87
  "expected #{File.dirname @actual.current_path.inspect} not to have permissions #{@expected.to_s(8)}, but it did"
84
88
  end
85
89
 
86
90
  def description
87
91
  "have permissions #{@expected.to_s(8)}"
88
92
  end
93
+
94
+ # RSpec 2 compatibility:
95
+ alias_method :negative_failure_message, :failure_message_when_negated
89
96
  end
90
97
 
91
98
  def have_directory_permissions(expected)
@@ -110,13 +117,16 @@ module CarrierWave
110
117
  "expected #{@actual.current_path.inspect} to be no larger than #{@width} by #{@height}, but it was #{@actual_width} by #{@actual_height}."
111
118
  end
112
119
 
113
- def negative_failure_message
120
+ def failure_message_when_negated
114
121
  "expected #{@actual.current_path.inspect} to be larger than #{@width} by #{@height}, but it wasn't."
115
122
  end
116
123
 
117
124
  def description
118
125
  "be no larger than #{@width} by #{@height}"
119
126
  end
127
+
128
+ # RSpec 2 compatibility:
129
+ alias_method :negative_failure_message, :failure_message_when_negated
120
130
  end
121
131
 
122
132
  def be_no_larger_than(width, height)
@@ -141,13 +151,16 @@ module CarrierWave
141
151
  "expected #{@actual.current_path.inspect} to have an exact size of #{@width} by #{@height}, but it was #{@actual_width} by #{@actual_height}."
142
152
  end
143
153
 
144
- def negative_failure_message
154
+ def failure_message_when_negated
145
155
  "expected #{@actual.current_path.inspect} not to have an exact size of #{@width} by #{@height}, but it did."
146
156
  end
147
157
 
148
158
  def description
149
159
  "have an exact size of #{@width} by #{@height}"
150
160
  end
161
+
162
+ # RSpec 2 compatibility:
163
+ alias_method :negative_failure_message, :failure_message_when_negated
151
164
  end
152
165
 
153
166
  def have_dimensions(width, height)
@@ -171,13 +184,16 @@ module CarrierWave
171
184
  "expected #{@actual.current_path.inspect} to have an exact size of #{@height}, but it was #{@actual_height}."
172
185
  end
173
186
 
174
- def negative_failure_message
187
+ def failure_message_when_negated
175
188
  "expected #{@actual.current_path.inspect} not to have an exact size of #{@height}, but it did."
176
189
  end
177
190
 
178
191
  def description
179
192
  "have an exact height of #{@height}"
180
193
  end
194
+
195
+ # RSpec 2 compatibility:
196
+ alias_method :negative_failure_message, :failure_message_when_negated
181
197
  end
182
198
 
183
199
  def have_height(height)
@@ -201,13 +217,16 @@ module CarrierWave
201
217
  "expected #{@actual.current_path.inspect} to have an exact size of #{@width}, but it was #{@actual_width}."
202
218
  end
203
219
 
204
- def negative_failure_message
220
+ def failure_message_when_negated
205
221
  "expected #{@actual.current_path.inspect} not to have an exact size of #{@width}, but it did."
206
222
  end
207
223
 
208
224
  def description
209
225
  "have an exact width of #{@width}"
210
226
  end
227
+
228
+ # RSpec 2 compatibility:
229
+ alias_method :negative_failure_message, :failure_message_when_negated
211
230
  end
212
231
 
213
232
  def have_width(width)
@@ -231,13 +250,16 @@ module CarrierWave
231
250
  "expected #{@actual.current_path.inspect} to be no wider than #{@width}, but it was #{@actual_width}."
232
251
  end
233
252
 
234
- def negative_failure_message
253
+ def failure_message_when_negated
235
254
  "expected #{@actual.current_path.inspect} not to be wider than #{@width}, but it is."
236
255
  end
237
256
 
238
257
  def description
239
258
  "have a width less than or equal to #{@width}"
240
259
  end
260
+
261
+ # RSpec 2 compatibility:
262
+ alias_method :negative_failure_message, :failure_message_when_negated
241
263
  end
242
264
 
243
265
  def be_no_wider_than(width)
@@ -261,19 +283,55 @@ module CarrierWave
261
283
  "expected #{@actual.current_path.inspect} to be no taller than #{@height}, but it was #{@actual_height}."
262
284
  end
263
285
 
264
- def negative_failure_message
286
+ def failure_message_when_negated
265
287
  "expected #{@actual.current_path.inspect} not to be taller than #{@height}, but it is."
266
288
  end
267
289
 
268
290
  def description
269
291
  "have a height less than or equal to #{@height}"
270
292
  end
293
+
294
+ # RSpec 2 compatibility:
295
+ alias_method :negative_failure_message, :failure_message_when_negated
271
296
  end
272
297
 
273
298
  def be_no_taller_than(height)
274
299
  BeNoTallerThan.new(height)
275
300
  end
276
301
 
302
+ class BeFormat # :nodoc:
303
+ def initialize(expected)
304
+ @expected = expected
305
+ end
306
+
307
+ def matches?(actual)
308
+ @actual = actual
309
+ # Satisfy expectation here. Return false or raise an error if it's not met.
310
+ image = ImageLoader.load_image(@actual.current_path)
311
+ @actual_expected = image.format
312
+ !@expected.nil? && @actual_expected.casecmp(@expected).zero?
313
+ end
314
+
315
+ def failure_message
316
+ "expected #{@actual.current_path.inspect} to have #{@expected} format, but it was #{@actual_expected}."
317
+ end
318
+
319
+ def failure_message_when_negated
320
+ "expected #{@actual.current_path.inspect} not to have #{@expected} format, but it did."
321
+ end
322
+
323
+ def description
324
+ "have #{@expected} format"
325
+ end
326
+
327
+ # RSpec 2 compatibility:
328
+ alias_method :negative_failure_message, :failure_message_when_negated
329
+ end
330
+
331
+ def be_format(expected)
332
+ BeFormat.new(expected)
333
+ end
334
+
277
335
  class ImageLoader # :nodoc:
278
336
  def self.load_image(filename)
279
337
  if defined? ::MiniMagick
@@ -303,6 +361,10 @@ module CarrierWave
303
361
  image.rows
304
362
  end
305
363
 
364
+ def format
365
+ image.format
366
+ end
367
+
306
368
  def initialize(filename)
307
369
  @image = ::Magick::Image.read(filename).first
308
370
  end
@@ -318,6 +380,10 @@ module CarrierWave
318
380
  image[:height]
319
381
  end
320
382
 
383
+ def format
384
+ image[:format]
385
+ end
386
+
321
387
  def initialize(filename)
322
388
  @image = ::MiniMagick::Image.open(filename)
323
389
  end
@@ -326,4 +392,3 @@ module CarrierWave
326
392
  end # Matchers
327
393
  end # Test
328
394
  end # CarrierWave
329
-