carrierwave 4.0.0.beta → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44d2471aedac47f453dcf2e0b0b3a36657ff89a1cd521875528a118cdfc465f4
4
- data.tar.gz: 4de4a19f9c7b959d8bfd3658c8ee672b03227a5c74d96ec86cbca64ab692d726
3
+ metadata.gz: 663f843bb0d228ae33a1afa9bf60c466ba068f7f13e5bd8cfa505de59029a34d
4
+ data.tar.gz: d6cdd19287a021a50268c48e2d4be4acf9bf1ed14680ff12c83c29226d7a7652
5
5
  SHA512:
6
- metadata.gz: 5f15fe1cc5ca10c246398c23bfa6d882494434dde71f40c4d044d949fae30e0929ade0d928ae951b028d2c9821538ae3d1e18a71c43f17e7a5256ecc19f4b8ea
7
- data.tar.gz: 675258a32e180790066ea27d8d12aad87e78c607e51125afc0c24fef5eab42588f7a2dba4d54967ee28b52ae7d4921bf408801ee6ab504ebac05d91ef907cdbd
6
+ metadata.gz: 4eee63b300dfe7ce3d4f2061bb8681b0c23c6b434ab45fe19e015a1cb6a8bfd19c7a84a30f0454c84d23df843ce51e71498cde0b4fef671f941d94ee8e1160f5
7
+ data.tar.gz: e77c6b1e21ecf67d074b8fc893b4d762c3fca35fe69014bb9504bb98992ca49858ee8ad060b9e8905d897e18dd9169be0ef12ad6f640e4a02abd4e854f414e14
data/README.md CHANGED
@@ -35,6 +35,23 @@ gem 'carrierwave', '~> 3.0'
35
35
 
36
36
  Finally, restart the server to apply the changes.
37
37
 
38
+ ### Supported Ruby and Rails versions
39
+
40
+ Each release is tested against the versions below. The gemspec only states a
41
+ minimum, so Bundler will happily install an old CarrierWave alongside a Rails
42
+ newer than it has ever seen — which is how a patch-level upgrade ends up failing
43
+ at boot. Check this table when upgrading Rails.
44
+
45
+ | CarrierWave | Rails | Ruby |
46
+ |-------------|------------|----------|
47
+ | 4.0 | 7.0 – 8.1 | >= 2.7 |
48
+ | 3.1 | 6.0 – 8.0 | >= 2.5 |
49
+ | 3.0 | 6.0 – 7.1 | >= 2.5 |
50
+ | 2.2 | 5.0 – 6.1 | >= 2.2.2 |
51
+
52
+ Only the latest release gets fixes; older lines are listed so you can tell
53
+ whether your combination was ever supported.
54
+
38
55
  ## Upgrading from 2.x or earlier
39
56
 
40
57
  CarrierWave 3.0 comes with a change in the way of handling the file extension on conversion. This results in following issues if you use `process convert: :format` to change the file format:
@@ -159,6 +176,14 @@ example, create a migration like this:
159
176
 
160
177
  __Note__: JSON datatype doesn't exists in SQLite adapter, that's why you can use a string datatype which will be serialized in model.
161
178
 
179
+ Leave the column nullable: `NULL` is what it holds when no file is mounted, so a `default: []` or `null: false` on it doesn't hold — the `NULL` is written on save. The uploader's own readers answer an empty array either way, and are what to read:
180
+
181
+ ```ruby
182
+ user.avatars # => [] when there is no file
183
+ user.avatars_identifiers # => []
184
+ user[:avatars] # => nil, the column as it is stored
185
+ ```
186
+
162
187
  Open your model file and mount the uploader:
163
188
 
164
189
 
@@ -223,7 +248,7 @@ end
223
248
  This works for the file storage as well as Amazon S3 and Rackspace Cloud Files.
224
249
  Define `store_dir` as `nil` if you'd like to store files at the root level.
225
250
 
226
- If you store files outside the project root folder, you may want to define `cache_dir` in the same way:
251
+ With the `:file` storage, if you store files outside the project root folder, you may want to define `cache_dir` in the same way:
227
252
 
228
253
  ```ruby
229
254
  class MyUploader < CarrierWave::Uploader::Base
@@ -245,6 +270,14 @@ class MyUploader < CarrierWave::Uploader::Base
245
270
  end
246
271
  ```
247
272
 
273
+ The name is worked out again whenever the stored file is located, so it has to come out the same every time. Memoise anything that would otherwise differ per call, in an instance variable of your own, as `@filename` is the uploader's:
274
+
275
+ ```ruby
276
+ def filename
277
+ @stable_filename ||= "#{SecureRandom.uuid}.#{file.extension}"
278
+ end
279
+ ```
280
+
248
281
  Some old documentations (like [this](https://stackoverflow.com/a/5865117)) may instruct you to safeguard the filename value with `if original_filename`, but it's no longer necessary with CarrierWave 3.0 or later.
249
282
 
250
283
  ## Securing uploads
@@ -637,6 +670,57 @@ CarrierWave.configure do |config|
637
670
  end
638
671
  ```
639
672
 
673
+ ## Uploading directly to the storage
674
+
675
+ When the file is stored on a cloud service, it can be uploaded straight to it,
676
+ without the bytes passing through your application. `#direct_upload` tells the
677
+ client where to put the file, and under which name to hand it back afterwards:
678
+
679
+ ```ruby
680
+ class UploadsController < ApplicationController
681
+ def create
682
+ upload = User.new.avatar.direct_upload(filename: params[:filename])
683
+ render json: upload.to_h
684
+ # => { url: "https://mybucket.s3.amazonaws.com/uploads/tmp/1758…-4829/cat.jpg?X-Amz-…",
685
+ # method: "PUT", headers: {}, cache_name: "1758…-4829/cat.jpg" }
686
+ end
687
+ end
688
+ ```
689
+
690
+ The file goes where the cache expects it, so the upload is finished by assigning
691
+ the returned name to the `avatar_cache` field you already have for form
692
+ redisplays. In the browser:
693
+
694
+ ```js
695
+ const upload = await (await fetch(`/uploads?filename=${encodeURIComponent(file.name)}`)).json();
696
+ await fetch(upload.url, { method: upload.method, headers: upload.headers, body: file });
697
+ document.querySelector('#user_avatar_cache').value = upload.cache_name;
698
+ // then submit the form as usual
699
+ ```
700
+
701
+ The file is stored where it belongs when the record is saved, as any cached file
702
+ is. Libraries which upload for you take the same three values: with
703
+ [Uppy](https://uppy.io/docs/aws-s3/), return `upload.to_h` from
704
+ `getUploadParameters`.
705
+
706
+ The cache has to be on the cloud service for this, which it is when `storage` is
707
+ set to `:fog` and `cache_storage` is left alone. A few more things to know:
708
+
709
+ * the filename is sanitized before it is signed into the URL, so the name the
710
+ file is stored under is the one in `cache_name`, not the one you passed;
711
+ * the upload stays possible for `fog_authenticated_url_expiration` seconds, which
712
+ `expires_in:` overrides;
713
+ * the size cannot be limited by the URL. Check it in the client and again with
714
+ `size_range` when the file comes back, and let `clean_cached_files!` or a
715
+ lifecycle rule on the cache directory collect what was uploaded and never used;
716
+ * pass `content_type:` to fix the type the client has to send. It is signed into
717
+ the URL and returned in `headers`, so the client has to send exactly that;
718
+ * uploaders which process what they store, or have versions, refuse to hand out
719
+ an upload: neither can be done to a file the application never sees.
720
+
721
+ Your storage service has to allow the upload to be made from the browser, which
722
+ for S3 means a CORS rule allowing `PUT` from your origin.
723
+
640
724
  ## Providing a default URL
641
725
 
642
726
  In many cases, especially when working with images, it might be a good idea to
@@ -1021,6 +1105,10 @@ end
1021
1105
  If you're uploading images, you'll probably want to manipulate them in some way,
1022
1106
  you might want to create thumbnail images for example.
1023
1107
 
1108
+ The processing library itself is not a dependency of CarrierWave, so add the one
1109
+ your uploaders use to your own Gemfile — `mini_magick`, `ruby-vips` or `rmagick`
1110
+ — along with the tool it drives (ImageMagick or libvips).
1111
+
1024
1112
  ### Using MiniMagick
1025
1113
 
1026
1114
  MiniMagick performs all the operations using the 'convert' CLI which is part of the standard ImageMagick kit.
@@ -1099,10 +1187,24 @@ class ImageFileUploader < CarrierWave::Uploader::Base
1099
1187
  end
1100
1188
  ````
1101
1189
 
1190
+ For anything the methods below don't cover, `vips!` yields the [ImageProcessing](https://github.com/janko/image_processing/blob/master/doc/vips.md) builder (`minimagick!` does the same for MiniMagick). For example, to strip the metadata and set the quality:
1191
+
1192
+ ````ruby
1193
+ class ImageFileUploader < CarrierWave::Uploader::Base
1194
+ include CarrierWave::Vips
1195
+
1196
+ process :optimize
1197
+
1198
+ def optimize
1199
+ vips! { |builder| builder.saver(strip: true, quality: 90) }
1200
+ end
1201
+ end
1202
+ ````
1203
+
1102
1204
  ### List of available processing methods:
1103
1205
 
1104
1206
  > [!NOTE]
1105
- > While the intention is to provide uniform interfaces to all three processing libraries the availability and implementation of processing methods can <a href="supported-processing-methods">vary slightly between them</a>.
1207
+ > While the intention is to provide uniform interfaces to all three processing libraries the availability and implementation of processing methods can <a href="#supported-processing-methods">vary slightly between them</a>.
1106
1208
 
1107
1209
  - `convert` - Changes the image encoding format to the given format (eg. jpg). This operation is treated specially to trigger the change of the file extension, so it matches with the format of the resulting file.
1108
1210
  - `resize_to_limit` - Resize the image to fit within the specified dimensions while retaining the original aspect ratio. Will only resize the image if it is larger than the specified dimensions. The resulting image may be shorter or narrower than specified in the smaller dimension but will not be larger than the specified values.
@@ -0,0 +1,20 @@
1
+ module CarrierWave
2
+ ##
3
+ # What a client needs to upload a file straight into the cache. See
4
+ # CarrierWave::Uploader::DirectUpload#direct_upload.
5
+ #
6
+ class DirectUpload
7
+ attr_reader :url, :method, :headers, :cache_name
8
+
9
+ def initialize(url:, cache_name:, headers: {}, method: 'PUT')
10
+ @url = url
11
+ @cache_name = cache_name
12
+ @headers = headers
13
+ @method = method
14
+ end
15
+
16
+ def to_h
17
+ { url: url, method: method, headers: headers, cache_name: cache_name }
18
+ end
19
+ end # DirectUpload
20
+ end # CarrierWave
@@ -40,12 +40,13 @@ module CarrierWave
40
40
  response = OpenURI.open_uri(uri, headers)
41
41
  else
42
42
  request = nil
43
+ ssrf_options = @uploader.ssrf_filter_options || {}
43
44
  if ::SsrfFilter::VERSION.to_f < 1.1
44
- response = SsrfFilter.get(uri, headers: headers) do |req|
45
+ response = SsrfFilter.get(uri, headers: headers, **ssrf_options) do |req|
45
46
  request = req
46
47
  end
47
48
  else
48
- response = SsrfFilter.get(uri, headers: headers, request_proc: ->(req) { request = req }) do |res|
49
+ response = SsrfFilter.get(uri, headers: headers, request_proc: ->(req) { request = req }, **ssrf_options) do |res|
49
50
  res.body # ensure to read body
50
51
  end
51
52
  end
@@ -10,6 +10,7 @@ en:
10
10
  content_type_denylist_error: "You are not allowed to upload %{content_type} files"
11
11
  processing_error: "Failed to manipulate, maybe it is not an image?"
12
12
  unsupported_image_format_error: "Failed to manipulate, file format is not supported"
13
+ filename_too_long_error: "File name is too long"
13
14
  min_size_error: "File size should be greater than %{min_size}"
14
15
  max_size_error: "File size should be less than %{max_size}"
15
16
  min_width_error: "Image width should be greater than %{min_width}px"
@@ -175,6 +175,10 @@ module CarrierWave
175
175
  _mounter(:#{column}).read_identifiers[0]
176
176
  end
177
177
 
178
+ def #{column}_identifier=(identifier)
179
+ _mounter(:#{column}).identifiers = [identifier]
180
+ end
181
+
178
182
  def #{column}_integrity_error
179
183
  #{column}_integrity_errors.last
180
184
  end
@@ -321,6 +325,10 @@ module CarrierWave
321
325
  def #{column}_identifiers
322
326
  _mounter(:#{column}).read_identifiers
323
327
  end
328
+
329
+ def #{column}_identifiers=(identifiers)
330
+ _mounter(:#{column}).identifiers = identifiers
331
+ end
324
332
  RUBY
325
333
  end
326
334
 
@@ -102,7 +102,7 @@ module CarrierWave
102
102
  def cache(new_files)
103
103
  return if !new_files.is_a?(Array) && new_files.blank?
104
104
  old_uploaders = uploaders
105
- @uploaders = new_files.map do |new_file|
105
+ attempts = new_files.map do |new_file|
106
106
  handle_error do
107
107
  if new_file.is_a?(String)
108
108
  if (uploader = old_uploaders.detect { |old_uploader| old_uploader.identifier == new_file })
@@ -123,7 +123,29 @@ module CarrierWave
123
123
  uploader
124
124
  end
125
125
  end
126
- end.reject(&:blank?)
126
+ end
127
+ errors, new_uploaders = attempts.partition { |attempt| attempt.is_a?(CarrierWave::UploadError) }
128
+ # A refusal didn't ask for the mounted file to go
129
+ return if errors.any? && new_uploaders.all?(&:blank?)
130
+
131
+ @uploaders = new_uploaders.reject(&:blank?)
132
+ @removed_uploaders += (old_uploaders - @uploaders)
133
+ write_temporary_identifier
134
+ end
135
+
136
+ ##
137
+ # Takes the stored files with the given identifiers as the mounted ones, for when
138
+ # they were put in the store by something other than CarrierWave.
139
+ #
140
+ def identifiers=(new_identifiers)
141
+ return if !new_identifiers.is_a?(Array) && new_identifiers.blank?
142
+
143
+ old_uploaders = uploaders
144
+ @uploaders = new_identifiers.map do |identifier|
145
+ next if identifier.blank?
146
+
147
+ blank_uploader.tap { |uploader| uploader.adopt!(identifier) }
148
+ end.compact
127
149
  @removed_uploaders += (old_uploaders - @uploaders)
128
150
  write_temporary_identifier
129
151
  end
@@ -158,14 +180,20 @@ module CarrierWave
158
180
  @download_errors.clear
159
181
  @remote_urls = urls
160
182
 
161
- clear_unstaged
162
- @remote_urls.zip(remote_request_headers || []) do |url, header|
183
+ old_uploaders = uploaders
184
+ attempts = @remote_urls.zip(remote_request_headers || []).map do |url, header|
163
185
  handle_error do
164
186
  uploader = blank_uploader
165
187
  uploader.download!(url, header || {})
166
- @uploaders << uploader
188
+ uploader
167
189
  end
168
190
  end
191
+ errors, new_uploaders = attempts.partition { |attempt| attempt.is_a?(CarrierWave::UploadError) }
192
+ # A refusal didn't ask for the mounted file to go
193
+ return if errors.any? && new_uploaders.all?(&:blank?)
194
+
195
+ @uploaders = old_uploaders.select(&:staged) + new_uploaders.reject(&:blank?)
196
+ @removed_uploaders += (old_uploaders - @uploaders)
169
197
  write_temporary_identifier
170
198
  end
171
199
 
@@ -209,7 +237,8 @@ module CarrierWave
209
237
  end
210
238
 
211
239
  def remove!
212
- uploaders.each(&:remove!)
240
+ # The reader leaves a blank uploader in place, which has nothing to remove
241
+ uploaders.reject { |uploader| uploader.file.nil? }.each(&:remove!)
213
242
  clear!
214
243
  end
215
244
 
@@ -235,7 +264,7 @@ module CarrierWave
235
264
  def remove_previous
236
265
  current_paths = uploaders.map(&:path)
237
266
  @removed_uploaders
238
- .reject {|uploader| current_paths.include?(uploader.path) }
267
+ .reject {|uploader| uploader.file.nil? || current_paths.include?(uploader.path) }
239
268
  .each { |uploader| uploader.remove! if uploader.remove_previously_stored_files_after_update }
240
269
  reset_changes!
241
270
  end
@@ -262,17 +291,21 @@ module CarrierWave
262
291
  @removed_uploaders += unstaged
263
292
  end
264
293
 
294
+ # Records the error and gives it back, unless the mount wants it raised
265
295
  def handle_error
266
296
  yield
267
297
  rescue CarrierWave::DownloadError => e
268
298
  @download_errors << e
269
299
  raise e unless option(:ignore_download_errors)
300
+ e
270
301
  rescue CarrierWave::ProcessingError => e
271
302
  @processing_errors << e
272
303
  raise e unless option(:ignore_processing_errors)
304
+ e
273
305
  rescue CarrierWave::IntegrityError => e
274
306
  @integrity_errors << e
275
307
  raise e unless option(:ignore_integrity_errors)
308
+ e
276
309
  end
277
310
 
278
311
  def write_temporary_identifier
@@ -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)
@@ -62,6 +94,22 @@ module CarrierWave
62
94
  return unless has_attribute?(_mounter(:#{column}).serialization_column)
63
95
  super
64
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
65
113
  RUBY
66
114
  end
67
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)
@@ -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, "_")
@@ -36,6 +36,10 @@ module CarrierWave
36
36
  file
37
37
  end
38
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
+
39
43
  def delete_dir!(path)
40
44
  raise NotImplementedError, "Need to implement #delete_dir! if you want to use #{self.class.name} as a cache storage."
41
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
@@ -168,20 +175,32 @@ module CarrierWave
168
175
  local_storage.delete_dir!(path)
169
176
  end
170
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)
184
+ end
185
+
171
186
  def clean_cache!(seconds)
172
187
  local_storage.clean_cache!(seconds)
173
- connection.directories.new(
174
- :key => uploader.fog_directory,
175
- :public => uploader.fog_public
176
- ).files.all(:prefix => uploader.cache_dir).each do |file|
177
- # generate_cache_id returns key formatted TIMEINT-PID(-COUNTER)-RND
178
- matched = file.key.match(/(\d+)-\d+-\d+(?:-\d+)?/)
179
- next unless matched
180
- time = Time.at(matched[1].to_i)
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
181
196
  file.destroy if time < (Time.now.utc - seconds)
182
197
  end
183
198
  end
184
199
 
200
+ def fog_public_attrs
201
+ uploader.fog_acl.nil? ? { :public => uploader.fog_public } : {}
202
+ end
203
+
185
204
  def connection
186
205
  @connection ||= begin
187
206
  options = credentials = uploader.fog_credentials
@@ -368,12 +387,11 @@ module CarrierWave
368
387
  else
369
388
  fog_file = new_file.to_file
370
389
  @content_type ||= new_file.content_type
371
- @file = directory.files.create({
390
+ @file = directory.files.create(fog_public_attrs.merge(
372
391
  :body => fog_file || new_file.read,
373
392
  :content_type => @content_type,
374
- :key => path,
375
- :public => @uploader.fog_public
376
- }.merge(@uploader.fog_attributes))
393
+ :key => path
394
+ ).merge(@uploader.fog_attributes))
377
395
  fog_file.close if fog_file && !fog_file.closed?
378
396
  end
379
397
  true
@@ -469,7 +487,8 @@ module CarrierWave
469
487
  end
470
488
 
471
489
  ##
472
- # 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.
473
492
  #
474
493
  # === Parameters
475
494
  #
@@ -480,7 +499,7 @@ module CarrierWave
480
499
  # @return [CarrierWave::Storage::Fog::File] the location where the file will be stored.
481
500
  #
482
501
  def copy_to(new_path)
483
- file.copy(@uploader.fog_directory, new_path, copy_options)
502
+ file.copy(@uploader.fog_directory, new_path, copy_options) unless new_path == path
484
503
  CarrierWave::Storage::Fog::File.new(@uploader, @base, new_path)
485
504
  end
486
505
 
@@ -524,10 +543,7 @@ module CarrierWave
524
543
  # [Fog::#{provider}::Directory] containing directory
525
544
  #
526
545
  def directory
527
- @directory ||= connection.directories.new(
528
- :key => @uploader.fog_directory,
529
- :public => @uploader.fog_public
530
- )
546
+ @directory ||= connection.directories.new(fog_public_attrs.merge(:key => @uploader.fog_directory))
531
547
  end
532
548
 
533
549
  ##
@@ -551,11 +567,19 @@ module CarrierWave
551
567
  end
552
568
 
553
569
  def acl_header
570
+ fog_acl = @uploader.fog_acl
571
+ return {} if fog_acl == false
572
+
554
573
  case fog_provider
555
574
  when 'AWS'
556
- { '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 }
557
577
  when "Google"
558
- @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
559
583
  else
560
584
  {}
561
585
  end
@@ -565,6 +589,10 @@ module CarrierWave
565
589
  @uploader.fog_credentials[:provider].to_s
566
590
  end
567
591
 
592
+ def fog_public_attrs
593
+ @uploader.fog_acl.nil? ? { :public => @uploader.fog_public } : {}
594
+ end
595
+
568
596
  def read_source_file
569
597
  source_file = to_file
570
598
  return unless source_file
@@ -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,6 +167,9 @@ 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
@@ -209,9 +233,7 @@ module CarrierWave
209
233
  attr_reader :original_filename
210
234
 
211
235
  def cache_id=(cache_id)
212
- # Earlier version used 3 part cache_id. Thus we should allow for
213
- # the cache_id to have both 3 part and 4 part formats.
214
- 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)
215
237
  @cache_id = cache_id
216
238
  end
217
239
 
@@ -221,7 +243,8 @@ module CarrierWave
221
243
  end
222
244
 
223
245
  def cache_storage
224
- @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
225
248
  end
226
249
 
227
250
  # We can override the full_original_filename method in other modules
@@ -28,6 +28,7 @@ module CarrierWave
28
28
 
29
29
  # fog
30
30
  add_deprecated_config :fog_provider
31
+ add_config :fog_acl
31
32
  add_config :fog_attributes
32
33
  add_config :fog_credentials
33
34
  add_config :fog_directory
@@ -50,6 +51,7 @@ module CarrierWave
50
51
  add_config :download_retry_count
51
52
  add_config :download_retry_wait_time
52
53
  add_config :skip_ssrf_protection
54
+ add_config :ssrf_filter_options
53
55
 
54
56
  # set default values
55
57
  reset_config
@@ -221,6 +223,7 @@ module CarrierWave
221
223
  config.download_retry_count = 0
222
224
  config.download_retry_wait_time = 5
223
225
  config.skip_ssrf_protection = false
226
+ config.ssrf_filter_options = {}
224
227
  end
225
228
  end
226
229
  end
@@ -0,0 +1,51 @@
1
+ module CarrierWave
2
+ module Uploader
3
+ module DirectUpload
4
+ extend ActiveSupport::Concern
5
+
6
+ include CarrierWave::Uploader::Callbacks
7
+ include CarrierWave::Uploader::Configuration
8
+ include CarrierWave::Uploader::Cache
9
+
10
+ ##
11
+ # Returns what a client needs to put a file into the cache itself, without the
12
+ # bytes passing through the application. Assign the returned #cache_name to
13
+ # #{column}_cache once the client is done, as with any cached file.
14
+ #
15
+ # === Parameters
16
+ #
17
+ # [filename (String)] the name the file is to be cached under
18
+ # [expires_in (Integer)] seconds the upload stays possible, defaults to
19
+ # fog_authenticated_url_expiration
20
+ # [content_type (String)] the type the client has to send, if it is to be fixed
21
+ #
22
+ # === Returns
23
+ #
24
+ # [CarrierWave::DirectUpload] where and how to upload, and the name to hand back
25
+ #
26
+ def direct_upload(filename:, expires_in: nil, content_type: nil)
27
+ raise CarrierWave::InvalidParameter, "invalid filename" if filename.blank?
28
+
29
+ filename = CarrierWave::SanitizedFile.new(filename).filename
30
+
31
+ upload = nil
32
+ with_callbacks(:direct_upload, filename) do
33
+ self.cache_id = CarrierWave.generate_cache_id
34
+ self.original_filename = filename
35
+ headers = content_type ? { 'Content-Type' => content_type } : {}
36
+
37
+ upload = ::CarrierWave::DirectUpload.new(
38
+ url: cache_storage.direct_upload_url(
39
+ cache_path,
40
+ expires_in: expires_in || fog_authenticated_url_expiration,
41
+ headers: headers.dup # the storage may add its own
42
+ ),
43
+ headers: headers,
44
+ cache_name: cache_name
45
+ )
46
+ end
47
+ upload
48
+ end
49
+ end # DirectUpload
50
+ end # Uploader
51
+ end # CarrierWave
@@ -10,6 +10,7 @@ module CarrierWave
10
10
  self.processors = []
11
11
 
12
12
  before :cache, :process!
13
+ before :direct_upload, :refuse_direct_upload_of_processed!
13
14
  end
14
15
 
15
16
  module ClassMethods
@@ -74,7 +75,7 @@ module CarrierWave
74
75
  # the condition held cannot be known, so a conditional one is refused.
75
76
  if condition
76
77
  raise ArgumentError, "`process convert:` cannot be given `:if` or `:unless`. The file extension follows the conversion, and it has to be worked out again when the file is retrieved, where whether the condition held is not known. " \
77
- "Convert unconditionally, or put the conversion in a version of its own, or take the naming over by overriding #full_filename."
78
+ "Convert unconditionally, or put the conversion in a version of its own, or convert in a processor of your own and have #filename answer the extension the file ended up with."
78
79
  end
79
80
 
80
81
  force_extension processor_args
@@ -120,6 +121,13 @@ module CarrierWave
120
121
 
121
122
  private
122
123
 
124
+ # Processing needs the file, which a direct upload never hands to the application
125
+ def refuse_direct_upload_of_processed!(filename)
126
+ return if !enable_processing || self.class.processors.empty?
127
+
128
+ raise "#{filename.inspect} can't be uploaded directly, as the processing of #{self.class} would be skipped"
129
+ end
130
+
123
131
  def forcing_extension(filename)
124
132
  if force_extension && filename
125
133
  Pathname.new(filename).sub_ext(".#{force_extension.to_s.delete_prefix('.')}").to_s
@@ -124,6 +124,27 @@ module CarrierWave
124
124
  end
125
125
  end
126
126
 
127
+ ##
128
+ # Takes the file already stored with the given identifier as its own, for when
129
+ # something other than CarrierWave put it there.
130
+ #
131
+ # === Parameters
132
+ #
133
+ # [identifier (String)] identifies the file in the store
134
+ #
135
+ # === Raises
136
+ #
137
+ # [CarrierWave::InvalidParameter] if the identifier points out of the store dir
138
+ #
139
+ def adopt!(identifier)
140
+ # The file gets removed when replaced, so it must not point out of the store dir
141
+ raise CarrierWave::InvalidParameter, "invalid identifier" if identifier.start_with?('/') || identifier.split('/').include?('..')
142
+
143
+ with_callbacks(:adopt, identifier) do
144
+ retrieve_from_store!(identifier)
145
+ end
146
+ end
147
+
127
148
  ##
128
149
  # Look for an identifier which doesn't collide with the given already-stored identifiers.
129
150
  # It is done by adding a index number as the suffix.
@@ -155,6 +176,10 @@ module CarrierWave
155
176
  # spared having to work the name out at all.
156
177
  #
157
178
  def ensure_the_filename_can_be_worked_out_again
179
+ if filename.blank?
180
+ raise CarrierWave::FilenameNotReproducible, "The file has no name to be stored under. " \
181
+ "#filename has to give one, as a cached file doesn't have to carry a name of its own."
182
+ end
158
183
  return if parent_version || metadata_recorded?
159
184
 
160
185
  for_file = deduplicated_filename
@@ -116,6 +116,8 @@ module CarrierWave
116
116
  after :remove, :remove_versions!
117
117
  after :retrieve_from_cache, :retrieve_versions_from_cache!
118
118
  after :retrieve_from_store, :retrieve_versions_from_store!
119
+ after :adopt, :adopt_versions!
120
+ before :direct_upload, :refuse_direct_upload_of_versions!
119
121
 
120
122
  prepend Module.new {
121
123
  def initialize(*)
@@ -251,7 +253,7 @@ module CarrierWave
251
253
  # that the cached files are generated by the single upload attempt.
252
254
  #
253
255
  def cache!(*args)
254
- self.cache_id = parent_version.cache_id if parent_version
256
+ self.cache_id = parent_version.cache_id if parent_version&.cached?
255
257
 
256
258
  super
257
259
  end
@@ -344,7 +346,8 @@ module CarrierWave
344
346
  end
345
347
 
346
348
  def full_original_filename
347
- [version_name, super].compact.join('_')
349
+ # nil stays nil, as a cached file doesn't have to carry a name of its own
350
+ [version_name, super].compact.join('_').presence
348
351
  end
349
352
 
350
353
  def cache_versions!(new_file)
@@ -373,6 +376,20 @@ module CarrierWave
373
376
  @deferred_version_retrieval = [:retrieve_from_store!, identifier]
374
377
  end
375
378
 
379
+ # Versions are created from a file the application has, which a direct upload never gives it
380
+ def refuse_direct_upload_of_versions!(filename)
381
+ return if versions.empty?
382
+
383
+ raise "#{filename.inspect} can't be uploaded directly, as nothing would create the versions #{versions.keys.join(', ')}"
384
+ end
385
+
386
+ # Versions are created only through the cache, which an adopted file never goes through
387
+ def adopt_versions!(identifier)
388
+ return if active_versions.empty?
389
+
390
+ raise "#{identifier.inspect} can't be adopted, as nothing would create the versions #{active_versions.keys.join(', ')}"
391
+ end
392
+
376
393
  end # Versions
377
394
  end # Uploader
378
395
  end # CarrierWave
@@ -5,6 +5,7 @@ require "carrierwave/uploader/url"
5
5
  require "carrierwave/uploader/mountable"
6
6
  require "carrierwave/uploader/cache"
7
7
  require "carrierwave/uploader/store"
8
+ require "carrierwave/uploader/direct_upload"
8
9
  require "carrierwave/uploader/download"
9
10
  require "carrierwave/uploader/remove"
10
11
  require "carrierwave/uploader/extension_allowlist"
@@ -52,6 +53,7 @@ module CarrierWave
52
53
  include CarrierWave::Uploader::Mountable
53
54
  include CarrierWave::Uploader::Cache
54
55
  include CarrierWave::Uploader::Store
56
+ include CarrierWave::Uploader::DirectUpload
55
57
  include CarrierWave::Uploader::Download
56
58
  include CarrierWave::Uploader::Remove
57
59
  include CarrierWave::Uploader::ExtensionAllowlist
@@ -19,17 +19,20 @@ module CarrierWave
19
19
 
20
20
  PERCENT_ENCODED = /%[0-9a-fA-F]{2}/.freeze
21
21
 
22
+ # URI::DEFAULT_PARSER#escape warns as obsolete, and URI::RFC2396_PARSER is missing in older uri gems
23
+ RFC2396_PARSER = URI::RFC2396_Parser.new
24
+
22
25
  module_function
23
26
 
24
27
  # Not idempotent, as '%' is escaped to '%25' every time
25
28
  def encode_path(path)
26
- URI::DEFAULT_PARSER.escape(path, PATH_UNSAFE)
29
+ RFC2396_PARSER.escape(path, PATH_UNSAFE)
27
30
  end
28
31
 
29
32
  # Only for strings not sent over the wire, like a filename to be shown to the user.
30
33
  # CGI.unescape is for form encoding and would turn '+' into a space.
31
34
  def decode_path(str)
32
- URI::DEFAULT_PARSER.unescape(str)
35
+ RFC2396_PARSER.unescape(str)
33
36
  end
34
37
 
35
38
  # Escapes only what cannot appear in the component, leaving existing %XX alone.
@@ -1,3 +1,3 @@
1
1
  module CarrierWave
2
- VERSION = "4.0.0.beta".freeze
2
+ VERSION = "4.0.0.rc".freeze
3
3
  end
data/lib/carrierwave.rb CHANGED
@@ -98,6 +98,7 @@ end
98
98
  require "carrierwave/utilities"
99
99
  require "carrierwave/error"
100
100
  require "carrierwave/sanitized_file"
101
+ require "carrierwave/direct_upload"
101
102
  require "carrierwave/mounter"
102
103
  require "carrierwave/mount"
103
104
  require "carrierwave/processing"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carrierwave
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0.beta
4
+ version: 4.0.0.rc
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonas Nicklas
@@ -347,6 +347,7 @@ files:
347
347
  - README.md
348
348
  - lib/carrierwave.rb
349
349
  - lib/carrierwave/compatibility/paperclip.rb
350
+ - lib/carrierwave/direct_upload.rb
350
351
  - lib/carrierwave/downloader/base.rb
351
352
  - lib/carrierwave/downloader/remote_file.rb
352
353
  - lib/carrierwave/error.rb
@@ -372,6 +373,7 @@ files:
372
373
  - lib/carrierwave/uploader/content_type_denylist.rb
373
374
  - lib/carrierwave/uploader/default_url.rb
374
375
  - lib/carrierwave/uploader/dimension.rb
376
+ - lib/carrierwave/uploader/direct_upload.rb
375
377
  - lib/carrierwave/uploader/download.rb
376
378
  - lib/carrierwave/uploader/extension_allowlist.rb
377
379
  - lib/carrierwave/uploader/extension_denylist.rb