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 +4 -4
- data/README.md +104 -2
- data/lib/carrierwave/direct_upload.rb +20 -0
- data/lib/carrierwave/downloader/base.rb +3 -2
- data/lib/carrierwave/locale/en.yml +1 -0
- data/lib/carrierwave/mount.rb +8 -0
- data/lib/carrierwave/mounter.rb +40 -7
- data/lib/carrierwave/orm/activerecord.rb +48 -0
- data/lib/carrierwave/processing/mini_magick.rb +24 -13
- data/lib/carrierwave/processing/vips.rb +0 -24
- data/lib/carrierwave/sanitized_file.rb +2 -2
- data/lib/carrierwave/storage/abstract.rb +4 -0
- data/lib/carrierwave/storage/file.rb +2 -4
- data/lib/carrierwave/storage/fog.rb +48 -20
- data/lib/carrierwave/uploader/cache.rb +32 -9
- data/lib/carrierwave/uploader/configuration.rb +3 -0
- data/lib/carrierwave/uploader/direct_upload.rb +51 -0
- data/lib/carrierwave/uploader/processing.rb +9 -1
- data/lib/carrierwave/uploader/store.rb +25 -0
- data/lib/carrierwave/uploader/versions.rb +19 -2
- data/lib/carrierwave/uploader.rb +2 -0
- data/lib/carrierwave/utilities/uri.rb +5 -2
- data/lib/carrierwave/version.rb +1 -1
- data/lib/carrierwave.rb +1 -0
- metadata +3 -1
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
|
|
@@ -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"
|
data/lib/carrierwave/mount.rb
CHANGED
|
@@ -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
|
|
data/lib/carrierwave/mounter.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
575
|
+
acl_value = fog_acl || (@uploader.fog_public ? 'public-read' : 'private')
|
|
576
|
+
{ 'x-amz-acl' => acl_value }
|
|
557
577
|
when "Google"
|
|
558
|
-
|
|
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
|
|
85
|
-
#
|
|
86
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
data/lib/carrierwave/uploader.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
35
|
+
RFC2396_PARSER.unescape(str)
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
# Escapes only what cannot appear in the component, leaving existing %XX alone.
|
data/lib/carrierwave/version.rb
CHANGED
data/lib/carrierwave.rb
CHANGED
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.
|
|
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
|