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.
- checksums.yaml +4 -4
- data/README.md +180 -3
- data/lib/carrierwave/direct_upload.rb +20 -0
- data/lib/carrierwave/downloader/base.rb +38 -9
- data/lib/carrierwave/downloader/remote_file.rb +3 -1
- data/lib/carrierwave/error.rb +1 -0
- data/lib/carrierwave/locale/en.yml +2 -0
- data/lib/carrierwave/mount.rb +10 -0
- data/lib/carrierwave/mounter.rb +79 -11
- data/lib/carrierwave/orm/activerecord.rb +51 -0
- data/lib/carrierwave/processing/mini_magick.rb +24 -13
- data/lib/carrierwave/processing/rmagick.rb +5 -0
- data/lib/carrierwave/processing/vips.rb +0 -24
- data/lib/carrierwave/sanitized_file.rb +2 -2
- data/lib/carrierwave/storage/abstract.rb +9 -0
- data/lib/carrierwave/storage/file.rb +2 -4
- data/lib/carrierwave/storage/fog.rb +98 -30
- data/lib/carrierwave/test/matchers.rb +24 -0
- data/lib/carrierwave/uploader/cache.rb +42 -9
- data/lib/carrierwave/uploader/configuration.rb +4 -0
- data/lib/carrierwave/uploader/direct_upload.rb +51 -0
- data/lib/carrierwave/uploader/metadata.rb +100 -0
- data/lib/carrierwave/uploader/processing.rb +18 -6
- data/lib/carrierwave/uploader/proxy.rb +18 -2
- data/lib/carrierwave/uploader/store.rb +48 -1
- data/lib/carrierwave/uploader/url.rb +3 -0
- data/lib/carrierwave/uploader/versions.rb +93 -50
- data/lib/carrierwave/uploader.rb +4 -0
- data/lib/carrierwave/utilities/uri.rb +32 -7
- data/lib/carrierwave/version.rb +1 -1
- data/lib/carrierwave.rb +1 -0
- metadata +9 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 663f843bb0d228ae33a1afa9bf60c466ba068f7f13e5bd8cfa505de59029a34d
|
|
4
|
+
data.tar.gz: d6cdd19287a021a50268c48e2d4be4acf9bf1ed14680ff12c83c29226d7a7652
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
@@ -378,6 +411,32 @@ class MyUploader < CarrierWave::Uploader::Base
|
|
|
378
411
|
end
|
|
379
412
|
```
|
|
380
413
|
|
|
414
|
+
`process convert: format` cannot be given a condition. The file extension follows the
|
|
415
|
+
conversion, and it has to be worked out again when the file is retrieved, where whether
|
|
416
|
+
the condition held is not known. Convert unconditionally, or give the format a version
|
|
417
|
+
of its own:
|
|
418
|
+
|
|
419
|
+
```ruby
|
|
420
|
+
version :webp, if: :convert_to_webp? do
|
|
421
|
+
process convert: :webp # unconditional within the version
|
|
422
|
+
end
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
If you need to convert on a condition and keep it on the uploader itself, take the
|
|
426
|
+
naming over so that it still follows from the identifier:
|
|
427
|
+
|
|
428
|
+
```ruby
|
|
429
|
+
process :to_jpeg, if: :heic?
|
|
430
|
+
|
|
431
|
+
def to_jpeg
|
|
432
|
+
minimagick! { |builder| builder.convert('jpg') }
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def full_filename(for_file)
|
|
436
|
+
for_file.sub(/\.heic\z/i, '.jpg')
|
|
437
|
+
end
|
|
438
|
+
```
|
|
439
|
+
|
|
381
440
|
### Nested versions
|
|
382
441
|
|
|
383
442
|
It is possible to nest versions within versions:
|
|
@@ -424,6 +483,13 @@ end
|
|
|
424
483
|
```
|
|
425
484
|
|
|
426
485
|
The `model` variable points to the instance object the uploader is attached to.
|
|
486
|
+
The condition is evaluated when the version is accessed, so it isn't called at all
|
|
487
|
+
as long as you only use the original file.
|
|
488
|
+
|
|
489
|
+
Note that `present?` on a version only tells whether a file is assigned, not whether
|
|
490
|
+
that version was actually created. Use `uploader.thumb.exists?` to ask the storage,
|
|
491
|
+
which costs a request when the storage is a remote one, or
|
|
492
|
+
[record what was stored](#recording-what-was-stored) to have the answer at hand.
|
|
427
493
|
|
|
428
494
|
### Create versions from existing versions
|
|
429
495
|
|
|
@@ -466,7 +532,44 @@ end
|
|
|
466
532
|
|
|
467
533
|
Please note that `#full_filename` mustn't be constructed based on a dynamic value
|
|
468
534
|
that can change from the time of store and time of retrieval, since it will result in
|
|
469
|
-
being unable to retrieve a file previously stored.
|
|
535
|
+
being unable to retrieve a file previously stored. Storing refuses to leave a file
|
|
536
|
+
somewhere a later request would not look, so this is caught rather than silent.
|
|
537
|
+
|
|
538
|
+
## Recording what was stored
|
|
539
|
+
|
|
540
|
+
By default CarrierWave persists the identifier alone, and works out everything else on
|
|
541
|
+
retrieval by re-running the uploader's definition and asking the storage. Which versions
|
|
542
|
+
a conditional creates is decided when the file is stored, so re-deciding it later can
|
|
543
|
+
give a different answer than what is actually there, and asking a remote storage for
|
|
544
|
+
things like the size costs a request per record.
|
|
545
|
+
|
|
546
|
+
Give the mount a column to record it in, and it stops guessing:
|
|
547
|
+
|
|
548
|
+
```ruby
|
|
549
|
+
class Event < ActiveRecord::Base
|
|
550
|
+
mount_uploader :image, ImageUploader, metadata_column: :image_metadata
|
|
551
|
+
end
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
```ruby
|
|
555
|
+
add_column :events, :image_metadata, :json
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
The column can be a `json`/`jsonb` one, a serialized one, or plain text holding JSON.
|
|
559
|
+
For `mount_uploaders` it holds one set of facts per file.
|
|
560
|
+
|
|
561
|
+
Records stored before the column was added have nothing recorded, and keep working as
|
|
562
|
+
they always have, so no backfill is needed.
|
|
563
|
+
|
|
564
|
+
Override `#build_metadata` to record your own facts:
|
|
565
|
+
|
|
566
|
+
```ruby
|
|
567
|
+
class ImageUploader < CarrierWave::Uploader::Base
|
|
568
|
+
def build_metadata
|
|
569
|
+
super.merge('width' => width, 'height' => height)
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
```
|
|
470
573
|
|
|
471
574
|
## Making uploads work across form redisplays
|
|
472
575
|
|
|
@@ -500,6 +603,11 @@ case of images, a small thumbnail would be a good indicator:
|
|
|
500
603
|
<% end %>
|
|
501
604
|
```
|
|
502
605
|
|
|
606
|
+
When the cache storage is a remote one, uploading the cached file to it is deferred until
|
|
607
|
+
it turns out to be needed beyond the current request, which reading `avatar_cache` or
|
|
608
|
+
`avatar_url` tells CarrierWave. If you carry the cache name over by other means, call
|
|
609
|
+
`user.avatar.materialize_cache!` before handing it out.
|
|
610
|
+
|
|
503
611
|
## Removing uploaded files
|
|
504
612
|
|
|
505
613
|
If you want to remove a previously uploaded file on a mounted uploader, you can
|
|
@@ -562,6 +670,57 @@ CarrierWave.configure do |config|
|
|
|
562
670
|
end
|
|
563
671
|
```
|
|
564
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
|
+
|
|
565
724
|
## Providing a default URL
|
|
566
725
|
|
|
567
726
|
In many cases, especially when working with images, it might be a good idea to
|
|
@@ -946,6 +1105,10 @@ end
|
|
|
946
1105
|
If you're uploading images, you'll probably want to manipulate them in some way,
|
|
947
1106
|
you might want to create thumbnail images for example.
|
|
948
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
|
+
|
|
949
1112
|
### Using MiniMagick
|
|
950
1113
|
|
|
951
1114
|
MiniMagick performs all the operations using the 'convert' CLI which is part of the standard ImageMagick kit.
|
|
@@ -1024,10 +1187,24 @@ class ImageFileUploader < CarrierWave::Uploader::Base
|
|
|
1024
1187
|
end
|
|
1025
1188
|
````
|
|
1026
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
|
+
|
|
1027
1204
|
### List of available processing methods:
|
|
1028
1205
|
|
|
1029
1206
|
> [!NOTE]
|
|
1030
|
-
> 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>.
|
|
1031
1208
|
|
|
1032
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.
|
|
1033
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require 'open-uri'
|
|
2
2
|
require 'ssrf_filter'
|
|
3
|
-
require 'addressable'
|
|
3
|
+
require 'addressable/uri'
|
|
4
4
|
require 'carrierwave/downloader/remote_file'
|
|
5
5
|
|
|
6
6
|
module CarrierWave
|
|
@@ -8,6 +8,14 @@ module CarrierWave
|
|
|
8
8
|
class Base
|
|
9
9
|
include CarrierWave::Utilities::Uri
|
|
10
10
|
|
|
11
|
+
# Leftovers from pasting, removed before parsing as in the first steps of
|
|
12
|
+
# https://url.spec.whatwg.org/#concept-basic-url-parser
|
|
13
|
+
LEADING_TRAILING_JUNK = /\A[\x00-\x20]+|[\x00-\x20]+\z/.freeze
|
|
14
|
+
EMBEDDED_JUNK = /[\t\n\r]/.freeze
|
|
15
|
+
# Matches a host name with an optional port, but not a scheme like 'http://' or 'data:'
|
|
16
|
+
HOSTISH = /\A[[:alnum:]][[:alnum:]\-._]*(?::\d+)?(?:[\/?#]|\z)/.freeze
|
|
17
|
+
DEFAULT_SCHEME = 'https'.freeze
|
|
18
|
+
|
|
11
19
|
attr_reader :uploader
|
|
12
20
|
|
|
13
21
|
def initialize(uploader)
|
|
@@ -29,15 +37,16 @@ module CarrierWave
|
|
|
29
37
|
uri = process_uri(url.to_s)
|
|
30
38
|
begin
|
|
31
39
|
if skip_ssrf_protection?(uri)
|
|
32
|
-
response = OpenURI.open_uri(
|
|
40
|
+
response = OpenURI.open_uri(uri, headers)
|
|
33
41
|
else
|
|
34
42
|
request = nil
|
|
43
|
+
ssrf_options = @uploader.ssrf_filter_options || {}
|
|
35
44
|
if ::SsrfFilter::VERSION.to_f < 1.1
|
|
36
|
-
response = SsrfFilter.get(uri, headers: headers) do |req|
|
|
45
|
+
response = SsrfFilter.get(uri, headers: headers, **ssrf_options) do |req|
|
|
37
46
|
request = req
|
|
38
47
|
end
|
|
39
48
|
else
|
|
40
|
-
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|
|
|
41
50
|
res.body # ensure to read body
|
|
42
51
|
end
|
|
43
52
|
end
|
|
@@ -59,22 +68,42 @@ module CarrierWave
|
|
|
59
68
|
##
|
|
60
69
|
# Processes the given URL by parsing it, and escaping if necessary. Public to allow overriding.
|
|
61
70
|
#
|
|
71
|
+
# Never decodes, as that would make %2F and '/', %2B and '+' indistinguishable.
|
|
72
|
+
#
|
|
62
73
|
# === Parameters
|
|
63
74
|
#
|
|
64
75
|
# [url (String)] The URL where the remote file is stored
|
|
65
76
|
#
|
|
66
77
|
def process_uri(source)
|
|
67
|
-
uri = Addressable::URI.parse(source)
|
|
78
|
+
uri = Addressable::URI.parse(normalize_input(source))
|
|
68
79
|
uri.host = uri.normalized_host
|
|
69
|
-
|
|
70
|
-
uri.
|
|
71
|
-
uri.
|
|
72
|
-
uri.fragment = encode_non_ascii(uri.fragment) if uri.fragment
|
|
80
|
+
uri.path = sanitize_component(uri.path, SANITIZE_PATH) if uri.path
|
|
81
|
+
uri.query = sanitize_component(uri.query, SANITIZE_QUERY) if uri.query
|
|
82
|
+
uri.fragment = sanitize_component(uri.fragment, SANITIZE_FRAGMENT) if uri.fragment
|
|
73
83
|
URI.parse(uri.to_s)
|
|
74
84
|
rescue URI::InvalidURIError, Addressable::URI::InvalidURIError
|
|
75
85
|
raise CarrierWave::DownloadError, "couldn't parse URL: #{source}"
|
|
76
86
|
end
|
|
77
87
|
|
|
88
|
+
##
|
|
89
|
+
# Cleans up a URL as pasted by an end user, since `remote_#{column}_url=` is meant to be
|
|
90
|
+
# fed from a text field. Public to allow overriding.
|
|
91
|
+
#
|
|
92
|
+
# === Parameters
|
|
93
|
+
#
|
|
94
|
+
# [source (String)] The URL given by the user
|
|
95
|
+
#
|
|
96
|
+
def normalize_input(source)
|
|
97
|
+
source = source.to_s.gsub(LEADING_TRAILING_JUNK, '').gsub(EMBEDDED_JUNK, '')
|
|
98
|
+
|
|
99
|
+
return "#{DEFAULT_SCHEME}:#{source}" if source.start_with?('//')
|
|
100
|
+
# Don't promote a local path like '/etc/passwd' into a URL
|
|
101
|
+
return source if source.start_with?('/')
|
|
102
|
+
return "#{DEFAULT_SCHEME}://#{source}" if source.match?(HOSTISH)
|
|
103
|
+
|
|
104
|
+
source
|
|
105
|
+
end
|
|
106
|
+
|
|
78
107
|
##
|
|
79
108
|
# If this returns true, SSRF protection will be bypassed.
|
|
80
109
|
# You can override this if you want to allow accessing specific local URIs that are not SSRF exploitable.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module CarrierWave
|
|
2
2
|
module Downloader
|
|
3
3
|
class RemoteFile
|
|
4
|
+
include CarrierWave::Utilities::Uri
|
|
5
|
+
|
|
4
6
|
attr_reader :file, :uri
|
|
5
7
|
|
|
6
8
|
def initialize(file)
|
|
@@ -53,7 +55,7 @@ module CarrierWave
|
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
def filename_from_uri
|
|
56
|
-
|
|
58
|
+
decode_path(File.basename(uri.path))
|
|
57
59
|
end
|
|
58
60
|
|
|
59
61
|
def method_missing(*args, &block)
|
data/lib/carrierwave/error.rb
CHANGED
|
@@ -9,6 +9,8 @@ en:
|
|
|
9
9
|
content_type_allowlist_error: "You are not allowed to upload %{content_type} files, allowed types: %{allowed_types}"
|
|
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
|
+
unsupported_image_format_error: "Failed to manipulate, file format is not supported"
|
|
13
|
+
filename_too_long_error: "File name is too long"
|
|
12
14
|
min_size_error: "File size should be greater than %{min_size}"
|
|
13
15
|
max_size_error: "File size should be less than %{max_size}"
|
|
14
16
|
min_width_error: "Image width should be greater than %{min_width}px"
|
data/lib/carrierwave/mount.rb
CHANGED
|
@@ -102,6 +102,7 @@ module CarrierWave
|
|
|
102
102
|
# === Options
|
|
103
103
|
#
|
|
104
104
|
# [:mount_on => Symbol] if the name of the column to be serialized to differs you can override it using this option
|
|
105
|
+
# [:metadata_column => Symbol] the column to record what was stored in, so that it does not have to be worked out again
|
|
105
106
|
# [:ignore_integrity_errors => Boolean] if set to true, integrity errors will result in caching failing silently
|
|
106
107
|
# [:ignore_processing_errors => Boolean] if set to true, processing errors will result in caching failing silently
|
|
107
108
|
#
|
|
@@ -174,6 +175,10 @@ module CarrierWave
|
|
|
174
175
|
_mounter(:#{column}).read_identifiers[0]
|
|
175
176
|
end
|
|
176
177
|
|
|
178
|
+
def #{column}_identifier=(identifier)
|
|
179
|
+
_mounter(:#{column}).identifiers = [identifier]
|
|
180
|
+
end
|
|
181
|
+
|
|
177
182
|
def #{column}_integrity_error
|
|
178
183
|
#{column}_integrity_errors.last
|
|
179
184
|
end
|
|
@@ -247,6 +252,7 @@ module CarrierWave
|
|
|
247
252
|
# === Options
|
|
248
253
|
#
|
|
249
254
|
# [:mount_on => Symbol] if the name of the column to be serialized to differs you can override it using this option
|
|
255
|
+
# [:metadata_column => Symbol] the column to record what was stored in, so that it does not have to be worked out again
|
|
250
256
|
# [:ignore_integrity_errors => Boolean] if set to true, integrity errors will result in caching failing silently
|
|
251
257
|
# [:ignore_processing_errors => Boolean] if set to true, processing errors will result in caching failing silently
|
|
252
258
|
#
|
|
@@ -319,6 +325,10 @@ module CarrierWave
|
|
|
319
325
|
def #{column}_identifiers
|
|
320
326
|
_mounter(:#{column}).read_identifiers
|
|
321
327
|
end
|
|
328
|
+
|
|
329
|
+
def #{column}_identifiers=(identifiers)
|
|
330
|
+
_mounter(:#{column}).identifiers = identifiers
|
|
331
|
+
end
|
|
322
332
|
RUBY
|
|
323
333
|
end
|
|
324
334
|
|
data/lib/carrierwave/mounter.rb
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "active_support/core_ext/array/wrap"
|
|
3
|
+
|
|
1
4
|
module CarrierWave
|
|
2
5
|
|
|
3
6
|
# this is an internal class, used by CarrierWave::Mount so that
|
|
@@ -8,6 +11,10 @@ module CarrierWave
|
|
|
8
11
|
uploaders.first&.identifier
|
|
9
12
|
end
|
|
10
13
|
|
|
14
|
+
def metadata
|
|
15
|
+
uploaders.first&.build_metadata
|
|
16
|
+
end
|
|
17
|
+
|
|
11
18
|
def temporary_identifier
|
|
12
19
|
temporary_identifiers.first
|
|
13
20
|
end
|
|
@@ -18,6 +25,10 @@ module CarrierWave
|
|
|
18
25
|
uploaders.map(&:identifier).presence
|
|
19
26
|
end
|
|
20
27
|
|
|
28
|
+
def metadata
|
|
29
|
+
uploaders.map(&:build_metadata).presence
|
|
30
|
+
end
|
|
31
|
+
|
|
21
32
|
def temporary_identifier
|
|
22
33
|
temporary_identifiers.presence
|
|
23
34
|
end
|
|
@@ -63,18 +74,35 @@ module CarrierWave
|
|
|
63
74
|
[record.read_uploader(serialization_column)].flatten.reject(&:blank?)
|
|
64
75
|
end
|
|
65
76
|
|
|
77
|
+
##
|
|
78
|
+
# The stored metadata, as many as there are identifiers. Whatever the column gives
|
|
79
|
+
# is accepted, so that it can be a json column, a serialized one, or plain text.
|
|
80
|
+
#
|
|
81
|
+
def read_metadata
|
|
82
|
+
return [] unless metadata_column
|
|
83
|
+
|
|
84
|
+
value = record.read_uploader(metadata_column)
|
|
85
|
+
value = JSON.parse(value) rescue nil if value.is_a?(String)
|
|
86
|
+
Array.wrap(value)
|
|
87
|
+
end
|
|
88
|
+
|
|
66
89
|
def uploaders
|
|
67
|
-
@uploaders ||=
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
90
|
+
@uploaders ||= begin
|
|
91
|
+
metadata = read_metadata
|
|
92
|
+
read_identifiers.each_with_index.map do |identifier, index|
|
|
93
|
+
uploader = blank_uploader
|
|
94
|
+
# Has to come first, as retrieval derives the path from what was recorded
|
|
95
|
+
uploader.metadata = metadata[index]
|
|
96
|
+
uploader.retrieve_from_store!(identifier)
|
|
97
|
+
uploader
|
|
98
|
+
end
|
|
71
99
|
end
|
|
72
100
|
end
|
|
73
101
|
|
|
74
102
|
def cache(new_files)
|
|
75
103
|
return if !new_files.is_a?(Array) && new_files.blank?
|
|
76
104
|
old_uploaders = uploaders
|
|
77
|
-
|
|
105
|
+
attempts = new_files.map do |new_file|
|
|
78
106
|
handle_error do
|
|
79
107
|
if new_file.is_a?(String)
|
|
80
108
|
if (uploader = old_uploaders.detect { |old_uploader| old_uploader.identifier == new_file })
|
|
@@ -95,12 +123,36 @@ module CarrierWave
|
|
|
95
123
|
uploader
|
|
96
124
|
end
|
|
97
125
|
end
|
|
98
|
-
end
|
|
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
|
|
99
149
|
@removed_uploaders += (old_uploaders - @uploaders)
|
|
100
150
|
write_temporary_identifier
|
|
101
151
|
end
|
|
102
152
|
|
|
103
153
|
def cache_names
|
|
154
|
+
# The names are carried over to the next request, which the files have to survive
|
|
155
|
+
uploaders.each(&:materialize_cache!)
|
|
104
156
|
uploaders.map(&:cache_name).compact
|
|
105
157
|
end
|
|
106
158
|
|
|
@@ -128,14 +180,20 @@ module CarrierWave
|
|
|
128
180
|
@download_errors.clear
|
|
129
181
|
@remote_urls = urls
|
|
130
182
|
|
|
131
|
-
|
|
132
|
-
@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|
|
|
133
185
|
handle_error do
|
|
134
186
|
uploader = blank_uploader
|
|
135
187
|
uploader.download!(url, header || {})
|
|
136
|
-
|
|
188
|
+
uploader
|
|
137
189
|
end
|
|
138
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)
|
|
139
197
|
write_temporary_identifier
|
|
140
198
|
end
|
|
141
199
|
|
|
@@ -157,6 +215,7 @@ module CarrierWave
|
|
|
157
215
|
@added_uploaders += additions
|
|
158
216
|
|
|
159
217
|
record.write_uploader(serialization_column, identifier)
|
|
218
|
+
record.write_uploader(metadata_column, metadata) if metadata_column
|
|
160
219
|
end
|
|
161
220
|
|
|
162
221
|
def urls(*args)
|
|
@@ -178,7 +237,8 @@ module CarrierWave
|
|
|
178
237
|
end
|
|
179
238
|
|
|
180
239
|
def remove!
|
|
181
|
-
|
|
240
|
+
# The reader leaves a blank uploader in place, which has nothing to remove
|
|
241
|
+
uploaders.reject { |uploader| uploader.file.nil? }.each(&:remove!)
|
|
182
242
|
clear!
|
|
183
243
|
end
|
|
184
244
|
|
|
@@ -197,10 +257,14 @@ module CarrierWave
|
|
|
197
257
|
option(:mount_on) || column
|
|
198
258
|
end
|
|
199
259
|
|
|
260
|
+
def metadata_column
|
|
261
|
+
option(:metadata_column)
|
|
262
|
+
end
|
|
263
|
+
|
|
200
264
|
def remove_previous
|
|
201
265
|
current_paths = uploaders.map(&:path)
|
|
202
266
|
@removed_uploaders
|
|
203
|
-
.reject {|uploader| current_paths.include?(uploader.path) }
|
|
267
|
+
.reject {|uploader| uploader.file.nil? || current_paths.include?(uploader.path) }
|
|
204
268
|
.each { |uploader| uploader.remove! if uploader.remove_previously_stored_files_after_update }
|
|
205
269
|
reset_changes!
|
|
206
270
|
end
|
|
@@ -227,17 +291,21 @@ module CarrierWave
|
|
|
227
291
|
@removed_uploaders += unstaged
|
|
228
292
|
end
|
|
229
293
|
|
|
294
|
+
# Records the error and gives it back, unless the mount wants it raised
|
|
230
295
|
def handle_error
|
|
231
296
|
yield
|
|
232
297
|
rescue CarrierWave::DownloadError => e
|
|
233
298
|
@download_errors << e
|
|
234
299
|
raise e unless option(:ignore_download_errors)
|
|
300
|
+
e
|
|
235
301
|
rescue CarrierWave::ProcessingError => e
|
|
236
302
|
@processing_errors << e
|
|
237
303
|
raise e unless option(:ignore_processing_errors)
|
|
304
|
+
e
|
|
238
305
|
rescue CarrierWave::IntegrityError => e
|
|
239
306
|
@integrity_errors << e
|
|
240
307
|
raise e unless option(:ignore_integrity_errors)
|
|
308
|
+
e
|
|
241
309
|
end
|
|
242
310
|
|
|
243
311
|
def write_temporary_identifier
|