carrierwave 3.1.4 → 4.0.0.rc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,38 @@ module CarrierWave
6
6
 
7
7
  include CarrierWave::Mount
8
8
 
9
+ ##
10
+ # See +CarrierWave::Mount#mount_uploader+ for documentation
11
+ #
12
+ def mount_uploader(column, uploader=nil, options={}, &block)
13
+ super
14
+
15
+ mod = Module.new
16
+ prepend mod
17
+ mod.class_eval <<-RUBY, __FILE__, __LINE__+1
18
+ def remote_#{column}_url=(url)
19
+ super
20
+ mark_#{column}_changed_on_error
21
+ end
22
+ RUBY
23
+ end
24
+
25
+ ##
26
+ # See +CarrierWave::Mount#mount_uploaders+ for documentation
27
+ #
28
+ def mount_uploaders(column, uploader=nil, options={}, &block)
29
+ super
30
+
31
+ mod = Module.new
32
+ prepend mod
33
+ mod.class_eval <<-RUBY, __FILE__, __LINE__+1
34
+ def remote_#{column}_urls=(urls)
35
+ super
36
+ mark_#{column}_changed_on_error
37
+ end
38
+ RUBY
39
+ end
40
+
9
41
  private
10
42
 
11
43
  def mount_base(column, uploader=nil, options={}, &block)
@@ -52,6 +84,9 @@ module CarrierWave
52
84
  @_mounters[:"#{column}"] = nil
53
85
  # The attribute needs to be cleared to prevent it from picked up as identifier
54
86
  write_uploader(_mounter(:#{column}).serialization_column, nil)
87
+ if (metadata_column = _mounter(:#{column}).metadata_column)
88
+ write_uploader(metadata_column, nil)
89
+ end
55
90
  _mounter(:"#{column}").cache(old_uploaders)
56
91
  end
57
92
 
@@ -59,6 +94,22 @@ module CarrierWave
59
94
  return unless has_attribute?(_mounter(:#{column}).serialization_column)
60
95
  super
61
96
  end
97
+
98
+ def #{column}=(new_file)
99
+ super
100
+ mark_#{column}_changed_on_error
101
+ end
102
+
103
+ private
104
+
105
+ # A refusal leaves the column as it was, and a record saved through an
106
+ # association is only validated when it changed, so say it changed
107
+ def mark_#{column}_changed_on_error
108
+ return if #{column}_integrity_errors.empty? && #{column}_processing_errors.empty? &&
109
+ #{column}_download_errors.empty?
110
+
111
+ __send__(:"\#{_mounter(:#{column}).serialization_column}_will_change!")
112
+ end
62
113
  RUBY
63
114
  end
64
115
 
@@ -96,7 +96,7 @@ module CarrierWave
96
96
  #
97
97
  # === Yields
98
98
  #
99
- # [MiniMagick::Image] additional manipulations to perform
99
+ # [MiniMagick::Image] additional manipulations to perform (deprecated, use #manipulate!)
100
100
  #
101
101
  # === Examples
102
102
  #
@@ -124,7 +124,7 @@ module CarrierWave
124
124
  #
125
125
  # === Yields
126
126
  #
127
- # [MiniMagick::Image] additional manipulations to perform
127
+ # [MiniMagick::Image] additional manipulations to perform (deprecated, use #manipulate!)
128
128
  #
129
129
  def resize_to_limit(width, height, combine_options: {}, &block)
130
130
  width, height = resolve_dimensions(width, height)
@@ -148,7 +148,7 @@ module CarrierWave
148
148
  #
149
149
  # === Yields
150
150
  #
151
- # [MiniMagick::Image] additional manipulations to perform
151
+ # [MiniMagick::Image] additional manipulations to perform (deprecated, use #manipulate!)
152
152
  #
153
153
  def resize_to_fit(width, height, combine_options: {}, &block)
154
154
  width, height = resolve_dimensions(width, height)
@@ -173,7 +173,7 @@ module CarrierWave
173
173
  #
174
174
  # === Yields
175
175
  #
176
- # [MiniMagick::Image] additional manipulations to perform
176
+ # [MiniMagick::Image] additional manipulations to perform (deprecated, use #manipulate!)
177
177
  #
178
178
  def resize_to_fill(width, height, gravity = 'Center', combine_options: {}, &block)
179
179
  width, height = resolve_dimensions(width, height)
@@ -203,7 +203,7 @@ module CarrierWave
203
203
  #
204
204
  # === Yields
205
205
  #
206
- # [MiniMagick::Image] additional manipulations to perform
206
+ # [MiniMagick::Image] additional manipulations to perform (deprecated, use #manipulate!)
207
207
  #
208
208
  def resize_and_pad(width, height, background=:transparent, gravity='Center', combine_options: {}, &block)
209
209
  width, height = resolve_dimensions(width, height)
@@ -228,7 +228,7 @@ module CarrierWave
228
228
  #
229
229
  # === Yields
230
230
  #
231
- # [MiniMagick::Image] additional manipulations to perform
231
+ # [MiniMagick::Image] additional manipulations to perform (deprecated, use #manipulate!)
232
232
  #
233
233
  def crop(left, top, width, height, combine_options: {}, &block)
234
234
  width, height = resolve_dimensions(width, height)
@@ -290,6 +290,7 @@ module CarrierWave
290
290
 
291
291
  image = yield(image)
292
292
  FileUtils.mv image.path, current_path
293
+ follow_extension_of image.path
293
294
 
294
295
  ::MiniMagick::Image.new(current_path).identify
295
296
  rescue ::MiniMagick::Error, ::MiniMagick::Invalid => e
@@ -325,20 +326,20 @@ module CarrierWave
325
326
  result = builder.call
326
327
  result.close
327
328
 
328
- # backwards compatibility (we want to eventually move away from MiniMagick::Image)
329
329
  if block
330
+ unless @minimagick_image_block_warned
331
+ @minimagick_image_block_warned = true
332
+ CarrierWave.deprecator.warn "Passing a block to receive a MiniMagick::Image is deprecated. " \
333
+ "Use #manipulate! to work with a MiniMagick::Image, or #minimagick! to build on the ImageProcessing builder."
334
+ end
335
+
330
336
  image = ::MiniMagick::Image.new(result.path, result)
331
337
  image = block.call(image)
332
338
  result = image.instance_variable_get(:@tempfile)
333
339
  end
334
340
 
335
341
  FileUtils.mv result.path, current_path
336
-
337
- if File.extname(result.path) != File.extname(current_path)
338
- move_to = current_path.chomp(File.extname(current_path)) + File.extname(result.path)
339
- file.content_type = Marcel::Magic.by_path(move_to).try(:type)
340
- file.move_to(move_to, permissions, directory_permissions)
341
- end
342
+ follow_extension_of result.path
342
343
  rescue ::MiniMagick::Error, ::MiniMagick::Invalid => e
343
344
  raise e if e.message =~ /(You must have .+ installed|is not installed|executable not found)/
344
345
  message = I18n.translate(:"errors.messages.processing_error")
@@ -347,6 +348,16 @@ module CarrierWave
347
348
 
348
349
  private
349
350
 
351
+ # The processing may have changed the format, in which case the file is renamed so
352
+ # that its extension keeps telling what it actually holds
353
+ def follow_extension_of(processed_path)
354
+ return if File.extname(processed_path) == File.extname(current_path)
355
+
356
+ move_to = current_path.chomp(File.extname(current_path)) + File.extname(processed_path)
357
+ file.content_type = Marcel::Magic.by_path(move_to).try(:type)
358
+ file.move_to(move_to, permissions, directory_permissions)
359
+ end
360
+
350
361
  def resolve_dimensions(*dimensions)
351
362
  dimensions.map do |value|
352
363
  next value unless value.instance_of?(Proc)
@@ -383,6 +383,11 @@ module CarrierWave
383
383
  frame = yield(*[frame, index, options].take(block.arity)) if block_given?
384
384
  frames << frame if frame
385
385
  end
386
+
387
+ if frames.none?
388
+ raise CarrierWave::ProcessingError, I18n.translate(:"errors.messages.unsupported_image_format_error")
389
+ end
390
+
386
391
  frames.append(true) if block_given?
387
392
 
388
393
  write_block = create_info_block(options[:write])
@@ -93,10 +93,6 @@ module CarrierWave
93
93
  #
94
94
  # [format (#to_s)] an abbreviation of the format
95
95
  #
96
- # === Yields
97
- #
98
- # [Vips::Image] additional manipulations to perform
99
- #
100
96
  # === Examples
101
97
  #
102
98
  # image.convert(:png)
@@ -121,10 +117,6 @@ module CarrierWave
121
117
  # [height (Integer)] the height to scale the image to
122
118
  # [combine_options (Hash)] additional Vips options to apply before resizing
123
119
  #
124
- # === Yields
125
- #
126
- # [Vips::Image] additional manipulations to perform
127
- #
128
120
  def resize_to_limit(width, height, combine_options: {})
129
121
  width, height = resolve_dimensions(width, height)
130
122
 
@@ -145,10 +137,6 @@ module CarrierWave
145
137
  # [height (Integer)] the height to scale the image to
146
138
  # [combine_options (Hash)] additional Vips options to apply before resizing
147
139
  #
148
- # === Yields
149
- #
150
- # [Vips::Image] additional manipulations to perform
151
- #
152
140
  def resize_to_fit(width, height, combine_options: {})
153
141
  width, height = resolve_dimensions(width, height)
154
142
 
@@ -169,10 +157,6 @@ module CarrierWave
169
157
  # [height (Integer)] the height to scale the image to
170
158
  # [combine_options (Hash)] additional vips options to apply before resizing
171
159
  #
172
- # === Yields
173
- #
174
- # [Vips::Image] additional manipulations to perform
175
- #
176
160
  def resize_to_fill(width, height, _gravity = nil, combine_options: {})
177
161
  width, height = resolve_dimensions(width, height)
178
162
 
@@ -199,10 +183,6 @@ module CarrierWave
199
183
  # [alpha (Boolean, nil)] pad the image with the alpha channel if supported
200
184
  # [combine_options (Hash)] additional vips options to apply before resizing
201
185
  #
202
- # === Yields
203
- #
204
- # [Vips::Image] additional manipulations to perform
205
- #
206
186
  def resize_and_pad(width, height, background=nil, gravity='centre', alpha=nil, combine_options: {})
207
187
  width, height = resolve_dimensions(width, height)
208
188
 
@@ -224,10 +204,6 @@ module CarrierWave
224
204
  # [width (Integer)] width of area to extract
225
205
  # [height (Integer)] height of area to extract
226
206
  #
227
- # === Yields
228
- #
229
- # [Vips::Image] additional manipulations to perform
230
- #
231
207
  def crop(left, top, width, height, combine_options: {})
232
208
  width, height = resolve_dimensions(width, height)
233
209
  width = vips_image.width - left if width + left > vips_image.width
@@ -53,7 +53,7 @@ module CarrierWave
53
53
  # [String] the sanitized filename
54
54
  #
55
55
  def filename
56
- sanitize(original_filename) if original_filename
56
+ sanitize(original_filename.to_s)
57
57
  end
58
58
 
59
59
  alias_method :identifier, :filename
@@ -305,7 +305,7 @@ module CarrierWave
305
305
 
306
306
  # Sanitize the filename, to prevent hacking
307
307
  def sanitize(name)
308
- name = name.scrub
308
+ name = name.scrub.delete("\0") # a NUL byte cuts the path short, and File.basename refuses one
309
309
  name = name.tr("\\", "/") # work-around for IE
310
310
  name = File.basename(name)
311
311
  name = name.gsub(sanitize_regexp, "_")
@@ -31,6 +31,15 @@ module CarrierWave
31
31
  raise NotImplementedError, "Need to implement #retrieve_from_cache! if you want to use #{self.class.name} as a cache storage."
32
32
  end
33
33
 
34
+ # Storage engines which defer writing to the cache have to flush it here
35
+ def materialize_cache!(file)
36
+ file
37
+ end
38
+
39
+ def direct_upload_url(path, expires_in:, headers: {})
40
+ raise NotImplementedError, "Need to implement #direct_upload_url if you want to use #{self.class.name} for direct uploads."
41
+ end
42
+
34
43
  def delete_dir!(path)
35
44
  raise NotImplementedError, "Need to implement #delete_dir! if you want to use #{self.class.name} as a cache storage."
36
45
  end
@@ -110,10 +110,8 @@ module CarrierWave
110
110
 
111
111
  def clean_cache!(seconds)
112
112
  Dir.glob(::File.expand_path(::File.join(uploader.cache_dir, '*'), uploader.root)).each do |dir|
113
- # generate_cache_id returns key formatted TIMEINT-PID(-COUNTER)-RND
114
- matched = dir.scan(/(\d+)-\d+-\d+(?:-\d+)?/).first
115
- next unless matched
116
- time = Time.at(matched[0].to_i)
113
+ time = uploader.parse_cache_id(::File.basename(dir))
114
+ next unless time
117
115
  if time < (Time.now.utc - seconds)
118
116
  FileUtils.rm_rf(dir)
119
117
  end
@@ -12,6 +12,13 @@ module CarrierWave
12
12
  # [:fog_directory] specifies name of directory to store data in, assumed to already exist
13
13
  #
14
14
  # [:fog_attributes] (optional) additional attributes to set on files
15
+ # [:fog_acl] (optional) overrides the ACL set on stored/copied files.
16
+ # When nil (default), ACL is derived from fog_public (:public attribute passed to fog).
17
+ # When false, no ACL is set at all (omits :public; use for buckets/containers with ACLs disabled).
18
+ # When a String, used as the explicit ACL value: passed as x-amz-acl for AWS
19
+ # (e.g. 'bucket-owner-full-control') or destination_predefined_acl for Google
20
+ # (e.g. 'bucketOwnerFullControl'). For store operations, pair with fog_attributes to
21
+ # set the provider-specific header.
15
22
  # [:fog_public] (optional) public readability, defaults to true
16
23
  # [:fog_authenticated_url_expiration] (optional) time (in seconds) that authenticated urls
17
24
  # will be valid, when fog_public is false and provider is AWS or Google, defaults to 600
@@ -105,7 +112,8 @@ module CarrierWave
105
112
  end
106
113
 
107
114
  ##
108
- # Stores given file to cache directory.
115
+ # Stages given file to be cached, deferring the upload until #materialize_cache!,
116
+ # as a cached file usually goes straight to the store within the same request.
109
117
  #
110
118
  # === Parameters
111
119
  #
@@ -113,12 +121,35 @@ module CarrierWave
113
121
  #
114
122
  # === Returns
115
123
  #
116
- # [CarrierWave::SanitizedFile] a sanitized file
124
+ # [CarrierWave::SanitizedFile] the staged file
125
+ # or
126
+ # [CarrierWave::Storage::Fog::File] the uploaded file, with cache_only
117
127
  #
118
128
  def cache!(new_file)
119
- f = CarrierWave::Storage::Fog::File.new(uploader, self, uploader.cache_path)
120
- f.store(new_file)
121
- f
129
+ # With cache_only there's no store to move to, so the cache is the final location
130
+ return upload_to_cache(new_file) if uploader.cache_only
131
+
132
+ local_storage.cache!(new_file)
133
+ end
134
+
135
+ ##
136
+ # Uploads the staged file to the cache directory.
137
+ #
138
+ # === Parameters
139
+ #
140
+ # [file (CarrierWave::SanitizedFile)] the staged file
141
+ #
142
+ # === Returns
143
+ #
144
+ # [CarrierWave::Storage::Fog::File] the uploaded file
145
+ #
146
+ def materialize_cache!(file)
147
+ return file if file.is_a?(CarrierWave::Storage::Fog::File)
148
+
149
+ upload_to_cache(file).tap do
150
+ file.delete
151
+ local_storage.delete_dir!(uploader.cache_path(nil))
152
+ end
122
153
  end
123
154
 
124
155
  ##
@@ -140,22 +171,36 @@ module CarrierWave
140
171
  # Deletes a cache dir
141
172
  #
142
173
  def delete_dir!(path)
143
- # do nothing, because there's no such things as 'empty directory'
174
+ # Only the local cache has dirs, as there's no such things as 'empty directory'
175
+ local_storage.delete_dir!(path)
176
+ end
177
+
178
+ ##
179
+ # Returns a URL a client can PUT the file to, to cache it without the bytes
180
+ # passing through the application.
181
+ #
182
+ def direct_upload_url(path, expires_in:, headers: {})
183
+ connection.put_object_url(uploader.fog_directory, path, ::Fog::Time.now.since(expires_in), headers)
144
184
  end
145
185
 
146
186
  def clean_cache!(seconds)
147
- connection.directories.new(
148
- :key => uploader.fog_directory,
149
- :public => uploader.fog_public
150
- ).files.all(:prefix => uploader.cache_dir).each do |file|
151
- # generate_cache_id returns key formatted TIMEINT-PID(-COUNTER)-RND
152
- matched = file.key.match(/(\d+)-\d+-\d+(?:-\d+)?/)
153
- next unless matched
154
- time = Time.at(matched[1].to_i)
187
+ local_storage.clean_cache!(seconds)
188
+ directory = connection.directories.new(fog_public_attrs.merge(:key => uploader.fog_directory))
189
+ prefix = "#{uploader.cache_dir.to_s.chomp('/')}/"
190
+ directory.files.all(:prefix => prefix).each do |file|
191
+ # the prefix is checked again, as some providers may not honor it when listing
192
+ next unless file.key.start_with?(prefix)
193
+
194
+ time = uploader.parse_cache_id(file.key.delete_prefix(prefix).split('/').first)
195
+ next unless time
155
196
  file.destroy if time < (Time.now.utc - seconds)
156
197
  end
157
198
  end
158
199
 
200
+ def fog_public_attrs
201
+ uploader.fog_acl.nil? ? { :public => uploader.fog_public } : {}
202
+ end
203
+
159
204
  def connection
160
205
  @connection ||= begin
161
206
  options = credentials = uploader.fog_credentials
@@ -296,7 +341,7 @@ module CarrierWave
296
341
  return read_source_file if ::File.exist?(file_body.path)
297
342
 
298
343
  # If the source file doesn't exist, the remote content is read
299
- @file = nil
344
+ remove_instance_variable(:@file)
300
345
  file.body
301
346
  end
302
347
 
@@ -308,7 +353,7 @@ module CarrierWave
308
353
  # [Integer] size of file body
309
354
  #
310
355
  def size
311
- file.nil? ? 0 : file.content_length
356
+ file&.content_length || 0
312
357
  end
313
358
 
314
359
  ##
@@ -342,12 +387,11 @@ module CarrierWave
342
387
  else
343
388
  fog_file = new_file.to_file
344
389
  @content_type ||= new_file.content_type
345
- @file = directory.files.create({
390
+ @file = directory.files.create(fog_public_attrs.merge(
346
391
  :body => fog_file || new_file.read,
347
392
  :content_type => @content_type,
348
- :key => path,
349
- :public => @uploader.fog_public
350
- }.merge(@uploader.fog_attributes))
393
+ :key => path
394
+ ).merge(@uploader.fog_attributes))
351
395
  fog_file.close if fog_file && !fog_file.closed?
352
396
  end
353
397
  true
@@ -439,11 +483,12 @@ module CarrierWave
439
483
  #
440
484
  def filename(options = {})
441
485
  return unless (file_url = url(options))
442
- CGI.unescape(file_url.split('?').first).gsub(/.*\/(.*?$)/, '\1')
486
+ decode_path(file_url.split('?').first).gsub(/.*\/(.*?$)/, '\1')
443
487
  end
444
488
 
445
489
  ##
446
- # Creates a copy of this file and returns it.
490
+ # Creates a copy of this file and returns it. Copying to where the file already is
491
+ # leaves it alone, as S3 refuses a copy onto the same key.
447
492
  #
448
493
  # === Parameters
449
494
  #
@@ -454,7 +499,7 @@ module CarrierWave
454
499
  # @return [CarrierWave::Storage::Fog::File] the location where the file will be stored.
455
500
  #
456
501
  def copy_to(new_path)
457
- file.copy(@uploader.fog_directory, new_path, copy_options)
502
+ file.copy(@uploader.fog_directory, new_path, copy_options) unless new_path == path
458
503
  CarrierWave::Storage::Fog::File.new(@uploader, @base, new_path)
459
504
  end
460
505
 
@@ -498,10 +543,7 @@ module CarrierWave
498
543
  # [Fog::#{provider}::Directory] containing directory
499
544
  #
500
545
  def directory
501
- @directory ||= connection.directories.new(
502
- :key => @uploader.fog_directory,
503
- :public => @uploader.fog_public
504
- )
546
+ @directory ||= connection.directories.new(fog_public_attrs.merge(:key => @uploader.fog_directory))
505
547
  end
506
548
 
507
549
  ##
@@ -512,7 +554,9 @@ module CarrierWave
512
554
  # [Fog::#{provider}::File] file data from remote service
513
555
  #
514
556
  def file
515
- @file ||= directory.files.head(path)
557
+ return @file if defined?(@file)
558
+
559
+ @file = directory.files.head(path)
516
560
  end
517
561
 
518
562
  def copy_options
@@ -523,11 +567,19 @@ module CarrierWave
523
567
  end
524
568
 
525
569
  def acl_header
570
+ fog_acl = @uploader.fog_acl
571
+ return {} if fog_acl == false
572
+
526
573
  case fog_provider
527
574
  when 'AWS'
528
- { 'x-amz-acl' => @uploader.fog_public ? 'public-read' : 'private' }
575
+ acl_value = fog_acl || (@uploader.fog_public ? 'public-read' : 'private')
576
+ { 'x-amz-acl' => acl_value }
529
577
  when "Google"
530
- @uploader.fog_public ? { destination_predefined_acl: "publicRead" } : {}
578
+ if fog_acl
579
+ { destination_predefined_acl: fog_acl }
580
+ else
581
+ @uploader.fog_public ? { destination_predefined_acl: "publicRead" } : {}
582
+ end
531
583
  else
532
584
  {}
533
585
  end
@@ -537,6 +589,10 @@ module CarrierWave
537
589
  @uploader.fog_credentials[:provider].to_s
538
590
  end
539
591
 
592
+ def fog_public_attrs
593
+ @uploader.fog_acl.nil? ? { :public => @uploader.fog_public } : {}
594
+ end
595
+
540
596
  def read_source_file
541
597
  source_file = to_file
542
598
  return unless source_file
@@ -554,6 +610,18 @@ module CarrierWave
554
610
  end
555
611
  end
556
612
 
613
+ private
614
+
615
+ def upload_to_cache(new_file)
616
+ f = CarrierWave::Storage::Fog::File.new(uploader, self, uploader.cache_path)
617
+ f.store(new_file)
618
+ f
619
+ end
620
+
621
+ def local_storage
622
+ @local_storage ||= CarrierWave::Storage::File.new(uploader)
623
+ end
624
+
557
625
  end # Fog
558
626
 
559
627
  end # Storage
@@ -336,6 +336,8 @@ module CarrierWave
336
336
  def self.load_image(filename)
337
337
  if defined? ::MiniMagick
338
338
  MiniMagickWrapper.new(filename)
339
+ elsif defined? ::Vips
340
+ VipsWrapper.new(filename)
339
341
  else
340
342
  unless defined? ::Magick
341
343
  begin
@@ -393,6 +395,28 @@ module CarrierWave
393
395
  end
394
396
  end
395
397
 
398
+ class VipsWrapper # :nodoc:
399
+ attr_reader :image
400
+
401
+ def width
402
+ image.width
403
+ end
404
+
405
+ def height
406
+ image.height
407
+ end
408
+
409
+ def format
410
+ # This returns the name of the vips loader (e.g. pngload) as Vips
411
+ # doesn't do content detection.
412
+ image.get("vips-loader").delete_suffix("load")
413
+ end
414
+
415
+ def initialize(filename)
416
+ @image = ::Vips::Image.new_from_file(filename)
417
+ end
418
+ end
419
+
396
420
  end # Matchers
397
421
  end # Test
398
422
  end # CarrierWave
@@ -81,10 +81,9 @@ module CarrierWave
81
81
  end
82
82
 
83
83
  ##
84
- # Caches the remotely stored file
85
- #
86
- # This is useful when about to process images. Most processing solutions
87
- # require the file to be stored on the local filesystem.
84
+ # Caches the stored file, so that it can be processed. Most processing solutions
85
+ # require the file to be on the local filesystem. It is staged as any assigned
86
+ # file is, so the next save stores the result; use #read for a copy only to read.
88
87
  #
89
88
  def cache_stored_file!
90
89
  cache!
@@ -103,7 +102,29 @@ module CarrierWave
103
102
  # [String] a cache name, in the format TIMEINT-PID-COUNTER-RND/filename.txt
104
103
  #
105
104
  def cache_name
106
- File.join(cache_id, original_filename) if cache_id && original_filename
105
+ File.join(*[cache_id, original_filename].compact) if cache_id
106
+ end
107
+
108
+ ##
109
+ # Tells when the given cache id was created, and by returning nil, that it is
110
+ # not one. Override to take cache ids which were made elsewhere.
111
+ #
112
+ # === Parameters
113
+ #
114
+ # [cache_id (String)] the cache id to look at
115
+ #
116
+ # === Returns
117
+ #
118
+ # [Time] when it was created
119
+ # or
120
+ # [NilClass] if it isn't a cache id
121
+ #
122
+ def parse_cache_id(cache_id)
123
+ # Earlier version used 3 part cache_id. Thus we should allow for
124
+ # the cache_id to have both 3 part and 4 part formats.
125
+ return unless cache_id.to_s =~ /\A(-?\d+)-\d+(-\d{4})?-\d{4}\z/
126
+
127
+ Time.at($1.to_i)
107
128
  end
108
129
 
109
130
  ##
@@ -146,11 +167,24 @@ module CarrierWave
146
167
  with_callbacks(:cache, @file) do
147
168
  @file = cache_storage.cache!(@file)
148
169
  end
170
+ rescue Errno::ENAMETOOLONG
171
+ # How long a name may be is the filesystem's to say, so let it say it
172
+ raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.filename_too_long_error")
149
173
  ensure
150
174
  FileUtils.rm_rf(workfile_path(''))
151
175
  end
152
176
  end
153
177
 
178
+ ##
179
+ # Makes sure the cached file is written to the cache storage, which may have
180
+ # deferred it. Needed when the file has to outlive the current request.
181
+ #
182
+ def materialize_cache!
183
+ with_callbacks(:materialize_cache) do
184
+ @file = cache_storage.materialize_cache!(@file) if @file
185
+ end
186
+ end
187
+
154
188
  ##
155
189
  # Retrieves the file with the given cache_name from the cache.
156
190
  #
@@ -199,9 +233,7 @@ module CarrierWave
199
233
  attr_reader :original_filename
200
234
 
201
235
  def cache_id=(cache_id)
202
- # Earlier version used 3 part cache_id. Thus we should allow for
203
- # the cache_id to have both 3 part and 4 part formats.
204
- raise CarrierWave::InvalidParameter, "invalid cache id" unless cache_id =~ /\A(-)?[\d]+\-[\d]+(\-[\d]{4})?\-[\d]{4}\z/
236
+ raise CarrierWave::InvalidParameter, "invalid cache id" unless parse_cache_id(cache_id)
205
237
  @cache_id = cache_id
206
238
  end
207
239
 
@@ -211,7 +243,8 @@ module CarrierWave
211
243
  end
212
244
 
213
245
  def cache_storage
214
- @cache_storage ||= (self.class.cache_storage || self.class.storage).new(self)
246
+ # Falls back to this uploader's storage, which may not be the configured one
247
+ @cache_storage ||= self.class.cache_storage&.new(self) || storage
215
248
  end
216
249
 
217
250
  # We can override the full_original_filename method in other modules