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
|
@@ -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
|
|
@@ -45,10 +46,12 @@ module CarrierWave
|
|
|
45
46
|
add_config :validate_processing
|
|
46
47
|
add_config :validate_download
|
|
47
48
|
add_config :mount_on
|
|
49
|
+
add_config :metadata_column
|
|
48
50
|
add_config :cache_only
|
|
49
51
|
add_config :download_retry_count
|
|
50
52
|
add_config :download_retry_wait_time
|
|
51
53
|
add_config :skip_ssrf_protection
|
|
54
|
+
add_config :ssrf_filter_options
|
|
52
55
|
|
|
53
56
|
# set default values
|
|
54
57
|
reset_config
|
|
@@ -220,6 +223,7 @@ module CarrierWave
|
|
|
220
223
|
config.download_retry_count = 0
|
|
221
224
|
config.download_retry_wait_time = 5
|
|
222
225
|
config.skip_ssrf_protection = false
|
|
226
|
+
config.ssrf_filter_options = {}
|
|
223
227
|
end
|
|
224
228
|
end
|
|
225
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
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
require "active_support/core_ext/hash/keys"
|
|
2
|
+
|
|
3
|
+
module CarrierWave
|
|
4
|
+
module Uploader
|
|
5
|
+
module Metadata
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
include CarrierWave::Uploader::Versions
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
# Facts about the file which were determined when it was stored, as recorded in
|
|
12
|
+
# the column given to the +metadata+ option of the mount. They are the answer to
|
|
13
|
+
# questions the storage cannot answer, like whether a conditional version was
|
|
14
|
+
# created, and they save asking it the ones it can.
|
|
15
|
+
#
|
|
16
|
+
# Returns an empty Hash when nothing was recorded, in which case the facts are
|
|
17
|
+
# derived from the file and the uploader definition as they have always been.
|
|
18
|
+
#
|
|
19
|
+
# === Returns
|
|
20
|
+
#
|
|
21
|
+
# [Hash] the recorded facts
|
|
22
|
+
#
|
|
23
|
+
def metadata
|
|
24
|
+
return @metadata || {} unless parent_version
|
|
25
|
+
|
|
26
|
+
parent_version.metadata.dig('versions', self.class.version_names.last.to_s) || {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def metadata=(metadata)
|
|
30
|
+
@metadata = metadata&.deep_stringify_keys
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# Collects the facts to record about the file being stored. Override to record
|
|
35
|
+
# your own, merging them into the result of +super+.
|
|
36
|
+
#
|
|
37
|
+
# === Returns
|
|
38
|
+
#
|
|
39
|
+
# [Hash] the facts to record
|
|
40
|
+
#
|
|
41
|
+
def build_metadata
|
|
42
|
+
{
|
|
43
|
+
'filename' => full_filename(identifier),
|
|
44
|
+
'size' => size,
|
|
45
|
+
'content_type' => content_type,
|
|
46
|
+
'versions' => active_versions.map { |name, version| [name.to_s, version.build_metadata] }.to_h
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# === Returns
|
|
52
|
+
#
|
|
53
|
+
# [Boolean] Whether what is stored gets recorded, which the mount decides
|
|
54
|
+
#
|
|
55
|
+
def metadata_recorded?
|
|
56
|
+
model.class.respond_to?(:uploader_option) &&
|
|
57
|
+
model.class.uploaders.has_key?(mounted_as) &&
|
|
58
|
+
!!model.class.uploader_option(mounted_as, :metadata_column)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def size
|
|
62
|
+
metadata['size'] || super
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def content_type
|
|
66
|
+
metadata['content_type'] || super
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# The conditions decide which versions to create, so which ones were created is
|
|
71
|
+
# settled once the file is stored. Consult the record instead of asking them
|
|
72
|
+
# again, which is not only costly but can give a different answer.
|
|
73
|
+
#
|
|
74
|
+
def version_active?(name)
|
|
75
|
+
recorded_versions = metadata['versions']
|
|
76
|
+
return super unless recorded_versions
|
|
77
|
+
|
|
78
|
+
versions.has_key?(name.to_sym) && recorded_versions.has_key?(name.to_s)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def cache!(*)
|
|
82
|
+
@metadata = nil
|
|
83
|
+
super
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def recreate_versions!(*)
|
|
87
|
+
@metadata = nil
|
|
88
|
+
super
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def full_filename(for_file)
|
|
94
|
+
return super unless for_file == identifier
|
|
95
|
+
|
|
96
|
+
metadata['filename'] || super
|
|
97
|
+
end
|
|
98
|
+
end # Metadata
|
|
99
|
+
end # Uploader
|
|
100
|
+
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
|
|
@@ -67,13 +68,17 @@ module CarrierWave
|
|
|
67
68
|
new_processors.each do |processor, processor_args|
|
|
68
69
|
self.processors += [[processor, processor_args, condition, condition_type]]
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
next unless processor == :convert
|
|
72
|
+
|
|
73
|
+
# Treat :convert specially, since it should trigger the file extension change.
|
|
74
|
+
# That has to be worked out again when the file is retrieved, where whether
|
|
75
|
+
# the condition held cannot be known, so a conditional one is refused.
|
|
76
|
+
if condition
|
|
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. " \
|
|
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."
|
|
76
79
|
end
|
|
80
|
+
|
|
81
|
+
force_extension processor_args
|
|
77
82
|
end
|
|
78
83
|
end
|
|
79
84
|
end # ClassMethods
|
|
@@ -116,6 +121,13 @@ module CarrierWave
|
|
|
116
121
|
|
|
117
122
|
private
|
|
118
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
|
+
|
|
119
131
|
def forcing_extension(filename)
|
|
120
132
|
if force_extension && filename
|
|
121
133
|
Pathname.new(filename).sub_ext(".#{force_extension.to_s.delete_prefix('.')}").to_s
|
|
@@ -5,10 +5,26 @@ module CarrierWave
|
|
|
5
5
|
##
|
|
6
6
|
# === Returns
|
|
7
7
|
#
|
|
8
|
-
# [Boolean] Whether
|
|
8
|
+
# [Boolean] Whether a file is assigned. Use #exists? to ask the storage whether
|
|
9
|
+
# the file is actually there.
|
|
9
10
|
#
|
|
10
11
|
def blank?
|
|
11
|
-
file
|
|
12
|
+
return true unless file
|
|
13
|
+
# A cache name can point at a cache which is gone, so it is verified. It is
|
|
14
|
+
# local and thus cheap, unlike asking a remote store.
|
|
15
|
+
return file.empty? if cached?
|
|
16
|
+
|
|
17
|
+
false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# === Returns
|
|
22
|
+
#
|
|
23
|
+
# [Boolean] Whether the file exists in the storage. Asking costs a request when
|
|
24
|
+
# the storage is a remote one.
|
|
25
|
+
#
|
|
26
|
+
def exists?
|
|
27
|
+
!!file&.exists?
|
|
12
28
|
end
|
|
13
29
|
|
|
14
30
|
##
|
|
@@ -94,6 +94,7 @@ module CarrierWave
|
|
|
94
94
|
cache!(new_file) if new_file && !cached?
|
|
95
95
|
if !cache_only && @file && @cache_id
|
|
96
96
|
with_callbacks(:store, new_file) do
|
|
97
|
+
ensure_the_filename_can_be_worked_out_again
|
|
97
98
|
new_file = storage.store!(@file)
|
|
98
99
|
if delete_tmp_file_after_storage
|
|
99
100
|
@file.delete unless move_to_store
|
|
@@ -116,8 +117,31 @@ module CarrierWave
|
|
|
116
117
|
#
|
|
117
118
|
def retrieve_from_store!(identifier)
|
|
118
119
|
with_callbacks(:retrieve_from_store, identifier) do
|
|
119
|
-
|
|
120
|
+
# Has to come first, as the storage derives the path from what was recorded
|
|
121
|
+
# for the identifier
|
|
120
122
|
@identifier = identifier
|
|
123
|
+
@file = storage.retrieve!(identifier)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
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)
|
|
121
145
|
end
|
|
122
146
|
end
|
|
123
147
|
|
|
@@ -143,6 +167,29 @@ module CarrierWave
|
|
|
143
167
|
|
|
144
168
|
private
|
|
145
169
|
|
|
170
|
+
##
|
|
171
|
+
# A later request gets the identifier and nothing else, so the name the file is
|
|
172
|
+
# stored under has to follow from it. Ask a fresh uploader what it would work out,
|
|
173
|
+
# and refuse before anything is written rather than leave the file somewhere it
|
|
174
|
+
# would never be found. Versions are left out: as long as the original is found,
|
|
175
|
+
# they can be made again. So is a mount which records what it stored, which is
|
|
176
|
+
# spared having to work the name out at all.
|
|
177
|
+
#
|
|
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
|
|
183
|
+
return if parent_version || metadata_recorded?
|
|
184
|
+
|
|
185
|
+
for_file = deduplicated_filename
|
|
186
|
+
would_look_for = self.class.new(model, mounted_as).send(:full_filename, for_file)
|
|
187
|
+
return if would_look_for == full_filename(for_file)
|
|
188
|
+
|
|
189
|
+
raise CarrierWave::FilenameNotReproducible, "The file would be stored as #{full_filename(for_file).inspect}, but a later request given only the identifier #{for_file.inspect} would look for #{would_look_for.inspect}. " \
|
|
190
|
+
"#full_filename has to follow from the identifier, as that is all such a request gets."
|
|
191
|
+
end
|
|
192
|
+
|
|
146
193
|
def full_filename(for_file)
|
|
147
194
|
forcing_extension(for_file)
|
|
148
195
|
end
|
|
@@ -15,6 +15,9 @@ module CarrierWave
|
|
|
15
15
|
# [String] the location where this file is accessible via a url
|
|
16
16
|
#
|
|
17
17
|
def url(options = {})
|
|
18
|
+
# The URL is to be fetched by a browser, which the file has to survive this request for
|
|
19
|
+
materialize_cache! if cached?
|
|
20
|
+
|
|
18
21
|
if file.respond_to?(:url)
|
|
19
22
|
tmp_url = file.method(:url).arity.zero? ? file.url : file.url(options)
|
|
20
23
|
return tmp_url if tmp_url.present?
|
|
@@ -9,6 +9,7 @@ module CarrierWave
|
|
|
9
9
|
@options = {}
|
|
10
10
|
@blocks = []
|
|
11
11
|
@klass = nil
|
|
12
|
+
@mutex = Mutex.new
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def configure(options, &block)
|
|
@@ -18,55 +19,68 @@ module CarrierWave
|
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
def build(superclass)
|
|
22
|
+
# Double-checked locking pattern for thread-safe lazy initialization.
|
|
23
|
+
# Without synchronization, concurrent threads can observe a partially
|
|
24
|
+
# initialized @klass where the class is created but version_options
|
|
25
|
+
# is not yet set, causing NoMethodError in version_active?.
|
|
21
26
|
return @klass if @klass
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
|
|
28
|
+
@mutex.synchronize do
|
|
29
|
+
return @klass if @klass
|
|
30
|
+
|
|
31
|
+
klass = Class.new(superclass)
|
|
32
|
+
superclass.const_set("VersionUploader#{@name.to_s.camelize}", klass)
|
|
33
|
+
|
|
34
|
+
klass.version_names += [@name]
|
|
35
|
+
klass.versions = {}
|
|
36
|
+
klass.processors = []
|
|
37
|
+
klass.version_options = @options
|
|
38
|
+
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
39
|
+
# Define the enable_processing method for versions so they get the
|
|
40
|
+
# value from the parent class unless explicitly overwritten
|
|
41
|
+
def self.enable_processing(value=nil)
|
|
42
|
+
self.enable_processing = value if value
|
|
43
|
+
if defined?(@enable_processing) && !@enable_processing.nil?
|
|
44
|
+
@enable_processing
|
|
45
|
+
else
|
|
46
|
+
superclass.enable_processing
|
|
47
|
+
end
|
|
38
48
|
end
|
|
39
|
-
end
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
# Regardless of what is set in the parent uploader, do not enforce the
|
|
51
|
+
# move_to_cache config option on versions because it moves the original
|
|
52
|
+
# file to the version's target file.
|
|
53
|
+
#
|
|
54
|
+
# If you want to enforce this setting on versions, override this method
|
|
55
|
+
# in each version:
|
|
56
|
+
#
|
|
57
|
+
# version :thumb do
|
|
58
|
+
# def move_to_cache
|
|
59
|
+
# true
|
|
60
|
+
# end
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
def move_to_cache
|
|
64
|
+
false
|
|
65
|
+
end
|
|
57
66
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
# Need to rely on the parent version's identifier, as versions don't have its own one.
|
|
68
|
+
def identifier
|
|
69
|
+
parent_version.identifier
|
|
70
|
+
end
|
|
71
|
+
RUBY
|
|
72
|
+
@blocks.each { |block| klass.class_eval(&block) }
|
|
73
|
+
|
|
74
|
+
# Only assign to @klass after full initialization to ensure
|
|
75
|
+
# other threads never see a partially initialized class.
|
|
76
|
+
@klass = klass
|
|
77
|
+
end
|
|
65
78
|
end
|
|
66
79
|
|
|
67
80
|
def deep_dup
|
|
68
81
|
other = dup
|
|
69
82
|
other.instance_variable_set(:@blocks, @blocks.dup)
|
|
83
|
+
other.instance_variable_set(:@mutex, Mutex.new)
|
|
70
84
|
other
|
|
71
85
|
end
|
|
72
86
|
|
|
@@ -97,15 +111,18 @@ module CarrierWave
|
|
|
97
111
|
attr_accessor :parent_version
|
|
98
112
|
|
|
99
113
|
after :cache, :cache_versions!
|
|
114
|
+
after :materialize_cache, :materialize_versions_cache!
|
|
100
115
|
after :store, :store_versions!
|
|
101
116
|
after :remove, :remove_versions!
|
|
102
117
|
after :retrieve_from_cache, :retrieve_versions_from_cache!
|
|
103
118
|
after :retrieve_from_store, :retrieve_versions_from_store!
|
|
119
|
+
after :adopt, :adopt_versions!
|
|
120
|
+
before :direct_upload, :refuse_direct_upload_of_versions!
|
|
104
121
|
|
|
105
122
|
prepend Module.new {
|
|
106
123
|
def initialize(*)
|
|
107
124
|
super
|
|
108
|
-
@versions = nil
|
|
125
|
+
@versions = @deferred_version_retrieval = nil
|
|
109
126
|
end
|
|
110
127
|
}
|
|
111
128
|
end
|
|
@@ -170,11 +187,16 @@ module CarrierWave
|
|
|
170
187
|
# [Hash{Symbol => CarrierWave::Uploader}] a list of uploader instances
|
|
171
188
|
#
|
|
172
189
|
def versions
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
190
|
+
unless @versions
|
|
191
|
+
@versions = {}
|
|
192
|
+
self.class.versions.each do |name, version|
|
|
193
|
+
@versions[name] = version.build(self.class).new(model, mounted_as)
|
|
194
|
+
@versions[name].parent_version = self
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
if (retrieval = @deferred_version_retrieval)
|
|
198
|
+
@deferred_version_retrieval = nil
|
|
199
|
+
active_versions.each_value { |v| v.public_send(*retrieval) }
|
|
178
200
|
end
|
|
179
201
|
@versions
|
|
180
202
|
end
|
|
@@ -231,7 +253,7 @@ module CarrierWave
|
|
|
231
253
|
# that the cached files are generated by the single upload attempt.
|
|
232
254
|
#
|
|
233
255
|
def cache!(*args)
|
|
234
|
-
self.cache_id = parent_version.cache_id if parent_version
|
|
256
|
+
self.cache_id = parent_version.cache_id if parent_version&.cached?
|
|
235
257
|
|
|
236
258
|
super
|
|
237
259
|
end
|
|
@@ -324,13 +346,18 @@ module CarrierWave
|
|
|
324
346
|
end
|
|
325
347
|
|
|
326
348
|
def full_original_filename
|
|
327
|
-
|
|
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
|
|
328
351
|
end
|
|
329
352
|
|
|
330
353
|
def cache_versions!(new_file)
|
|
331
354
|
derived_versions.each_value { |v| v.cache!(new_file) }
|
|
332
355
|
end
|
|
333
356
|
|
|
357
|
+
def materialize_versions_cache!
|
|
358
|
+
versions.each_value(&:materialize_cache!)
|
|
359
|
+
end
|
|
360
|
+
|
|
334
361
|
def store_versions!(new_file)
|
|
335
362
|
active_versions.each_value { |v| v.store!(new_file) }
|
|
336
363
|
end
|
|
@@ -339,12 +366,28 @@ module CarrierWave
|
|
|
339
366
|
versions.each_value { |v| v.remove! }
|
|
340
367
|
end
|
|
341
368
|
|
|
369
|
+
# Retrieval is deferred until #versions is accessed, as evaluating the versions'
|
|
370
|
+
# conditions can be costly and is pointless when no version is used
|
|
342
371
|
def retrieve_versions_from_cache!(cache_name)
|
|
343
|
-
|
|
372
|
+
@deferred_version_retrieval = [:retrieve_from_cache!, cache_name]
|
|
344
373
|
end
|
|
345
374
|
|
|
346
375
|
def retrieve_versions_from_store!(identifier)
|
|
347
|
-
|
|
376
|
+
@deferred_version_retrieval = [:retrieve_from_store!, identifier]
|
|
377
|
+
end
|
|
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(', ')}"
|
|
348
391
|
end
|
|
349
392
|
|
|
350
393
|
end # Versions
|
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"
|
|
@@ -15,6 +16,7 @@ require "carrierwave/uploader/file_size"
|
|
|
15
16
|
require "carrierwave/uploader/dimension"
|
|
16
17
|
require "carrierwave/uploader/processing"
|
|
17
18
|
require "carrierwave/uploader/versions"
|
|
19
|
+
require "carrierwave/uploader/metadata"
|
|
18
20
|
require "carrierwave/uploader/default_url"
|
|
19
21
|
|
|
20
22
|
require "carrierwave/uploader/serialization"
|
|
@@ -51,6 +53,7 @@ module CarrierWave
|
|
|
51
53
|
include CarrierWave::Uploader::Mountable
|
|
52
54
|
include CarrierWave::Uploader::Cache
|
|
53
55
|
include CarrierWave::Uploader::Store
|
|
56
|
+
include CarrierWave::Uploader::DirectUpload
|
|
54
57
|
include CarrierWave::Uploader::Download
|
|
55
58
|
include CarrierWave::Uploader::Remove
|
|
56
59
|
include CarrierWave::Uploader::ExtensionAllowlist
|
|
@@ -61,6 +64,7 @@ module CarrierWave
|
|
|
61
64
|
include CarrierWave::Uploader::Dimension
|
|
62
65
|
include CarrierWave::Uploader::Processing
|
|
63
66
|
include CarrierWave::Uploader::Versions
|
|
67
|
+
include CarrierWave::Uploader::Metadata
|
|
64
68
|
include CarrierWave::Uploader::DefaultUrl
|
|
65
69
|
include CarrierWave::Uploader::Serialization
|
|
66
70
|
end # Base
|
|
@@ -3,23 +3,48 @@ require 'uri'
|
|
|
3
3
|
module CarrierWave
|
|
4
4
|
module Utilities
|
|
5
5
|
module Uri
|
|
6
|
+
# For encoding raw data into a URI component, where '%' is data and becomes '%25'.
|
|
6
7
|
# based on Ruby < 2.0's URI.encode
|
|
7
8
|
PATH_SAFE = URI::RFC2396_REGEXP::PATTERN::UNRESERVED + '\/'
|
|
8
9
|
PATH_UNSAFE = Regexp.new("[^#{PATH_SAFE}]", false)
|
|
9
|
-
NON_ASCII = /[^[:ascii:]]/.freeze
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# For repairing a string which is already a URI, following RFC 3986 per component.
|
|
12
|
+
UNRESERVED = 'A-Za-z0-9\-._~'.freeze
|
|
13
|
+
SUB_DELIMS = "!$&'()*+,;=".freeze
|
|
14
|
+
PCHAR = "#{UNRESERVED}#{SUB_DELIMS}:@".freeze
|
|
15
|
+
SANITIZE_PATH = Regexp.new("[#{PCHAR}/]").freeze
|
|
16
|
+
# Also allows [ ] { } | \ ^, which browsers and URI.parse accept in a query
|
|
17
|
+
SANITIZE_QUERY = Regexp.new("[#{PCHAR}/?\\[\\]{}|\\\\^]").freeze
|
|
18
|
+
SANITIZE_FRAGMENT = Regexp.new("[#{PCHAR}/?]").freeze
|
|
12
19
|
|
|
20
|
+
PERCENT_ENCODED = /%[0-9a-fA-F]{2}/.freeze
|
|
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
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# Not idempotent, as '%' is escaped to '%25' every time
|
|
13
28
|
def encode_path(path)
|
|
14
|
-
|
|
29
|
+
RFC2396_PARSER.escape(path, PATH_UNSAFE)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Only for strings not sent over the wire, like a filename to be shown to the user.
|
|
33
|
+
# CGI.unescape is for form encoding and would turn '+' into a space.
|
|
34
|
+
def decode_path(str)
|
|
35
|
+
RFC2396_PARSER.unescape(str)
|
|
15
36
|
end
|
|
16
37
|
|
|
17
|
-
|
|
18
|
-
|
|
38
|
+
# Escapes only what cannot appear in the component, leaving existing %XX alone.
|
|
39
|
+
# Decoding first would make %2F and '/', %2B and '+' indistinguishable.
|
|
40
|
+
def sanitize_component(str, allowed)
|
|
41
|
+
str.gsub(/(#{PERCENT_ENCODED})|(.)/m) do
|
|
42
|
+
$1 || ($2.match?(allowed) ? $2 : escape_octets($2))
|
|
43
|
+
end
|
|
19
44
|
end
|
|
20
45
|
|
|
21
|
-
def
|
|
22
|
-
|
|
46
|
+
def escape_octets(str)
|
|
47
|
+
str.b.bytes.map { |byte| format('%%%02X', byte) }.join
|
|
23
48
|
end
|
|
24
49
|
end # Uri
|
|
25
50
|
end # Utilities
|
data/lib/carrierwave/version.rb
CHANGED
data/lib/carrierwave.rb
CHANGED