shrine 2.19.4 → 3.0.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of shrine might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +299 -11
- data/README.md +9 -3
- data/doc/advantages.md +1 -1
- data/doc/carrierwave.md +4 -4
- data/doc/creating_persistence_plugins.md +172 -0
- data/doc/creating_plugins.md +1 -1
- data/doc/creating_storages.md +3 -1
- data/doc/design.md +2 -2
- data/doc/direct_s3.md +0 -22
- data/doc/paperclip.md +3 -3
- data/doc/plugins/activerecord.md +211 -42
- data/doc/plugins/atomic_helpers.md +153 -0
- data/doc/plugins/column.md +90 -0
- data/doc/plugins/derivation_endpoint.md +54 -62
- data/doc/plugins/derivatives.md +752 -0
- data/doc/plugins/entity.md +204 -0
- data/doc/plugins/infer_extension.md +8 -8
- data/doc/plugins/instrumentation.md +33 -13
- data/doc/plugins/keep_files.md +5 -15
- data/doc/plugins/model.md +157 -0
- data/doc/plugins/presign_endpoint.md +2 -1
- data/doc/plugins/refresh_metadata.md +44 -7
- data/doc/plugins/sequel.md +190 -33
- data/doc/plugins/{default_url_options.md → url_options.md} +5 -5
- data/doc/processing.md +1 -1
- data/doc/release_notes/1.1.0.md +2 -2
- data/doc/release_notes/2.15.0.md +1 -1
- data/doc/storage/s3.md +2 -2
- data/doc/testing.md +1 -1
- data/lib/shrine.rb +72 -138
- data/lib/shrine/attacher.rb +272 -176
- data/lib/shrine/attachment.rb +2 -42
- data/lib/shrine/plugins/activerecord.rb +103 -26
- data/lib/shrine/plugins/add_metadata.rb +9 -10
- data/lib/shrine/plugins/atomic_helpers.rb +111 -0
- data/lib/shrine/plugins/attacher_options.rb +55 -0
- data/lib/shrine/plugins/backgrounding.rb +147 -115
- data/lib/shrine/plugins/cached_attachment_data.rb +6 -9
- data/lib/shrine/plugins/column.rb +104 -0
- data/lib/shrine/plugins/data_uri.rb +35 -38
- data/lib/shrine/plugins/default_storage.rb +18 -12
- data/lib/shrine/plugins/default_url.rb +11 -21
- data/lib/shrine/plugins/default_url_options.rb +3 -30
- data/lib/shrine/plugins/delete_raw.rb +9 -13
- data/lib/shrine/plugins/derivation_endpoint.rb +75 -114
- data/lib/shrine/plugins/derivatives.rb +576 -0
- data/lib/shrine/plugins/determine_mime_type.rb +3 -15
- data/lib/shrine/plugins/download_endpoint.rb +83 -131
- data/lib/shrine/plugins/dynamic_storage.rb +4 -8
- data/lib/shrine/plugins/entity.rb +128 -0
- data/lib/shrine/plugins/form_assign.rb +107 -0
- data/lib/shrine/plugins/included.rb +4 -3
- data/lib/shrine/plugins/infer_extension.rb +10 -17
- data/lib/shrine/plugins/instrumentation.rb +45 -25
- data/lib/shrine/plugins/keep_files.rb +2 -12
- data/lib/shrine/plugins/metadata_attributes.rb +15 -14
- data/lib/shrine/plugins/model.rb +137 -0
- data/lib/shrine/plugins/module_include.rb +2 -0
- data/lib/shrine/plugins/presign_endpoint.rb +1 -15
- data/lib/shrine/plugins/pretty_location.rb +5 -5
- data/lib/shrine/plugins/processing.rb +21 -6
- data/lib/shrine/plugins/rack_file.rb +1 -39
- data/lib/shrine/plugins/rack_response.rb +14 -7
- data/lib/shrine/plugins/recache.rb +5 -2
- data/lib/shrine/plugins/refresh_metadata.rb +12 -8
- data/lib/shrine/plugins/remote_url.rb +44 -53
- data/lib/shrine/plugins/remove_attachment.rb +7 -2
- data/lib/shrine/plugins/remove_invalid.rb +8 -4
- data/lib/shrine/plugins/restore_cached_data.rb +12 -4
- data/lib/shrine/plugins/sequel.rb +115 -27
- data/lib/shrine/plugins/signature.rb +2 -7
- data/lib/shrine/plugins/store_dimensions.rb +13 -27
- data/lib/shrine/plugins/upload_endpoint.rb +14 -15
- data/lib/shrine/plugins/upload_options.rb +9 -8
- data/lib/shrine/plugins/url_options.rb +33 -0
- data/lib/shrine/plugins/validation.rb +87 -0
- data/lib/shrine/plugins/validation_helpers.rb +33 -54
- data/lib/shrine/plugins/versions.rb +106 -84
- data/lib/shrine/storage/file_system.rb +32 -57
- data/lib/shrine/storage/linter.rb +9 -1
- data/lib/shrine/storage/memory.rb +42 -0
- data/lib/shrine/storage/s3.rb +38 -146
- data/lib/shrine/uploaded_file.rb +22 -29
- data/lib/shrine/version.rb +4 -4
- data/shrine.gemspec +2 -3
- metadata +27 -54
- data/doc/plugins/backup.md +0 -31
- data/doc/plugins/copy.md +0 -24
- data/doc/plugins/delete_promoted.md +0 -12
- data/doc/plugins/direct_upload.md +0 -172
- data/doc/plugins/hooks.md +0 -58
- data/doc/plugins/logging.md +0 -42
- data/doc/plugins/migration_helpers.md +0 -60
- data/doc/plugins/moving.md +0 -19
- data/doc/plugins/multi_delete.md +0 -20
- data/doc/plugins/parallelize.md +0 -16
- data/doc/plugins/parsed_json.md +0 -23
- data/lib/shrine/plugins/background_helpers.rb +0 -5
- data/lib/shrine/plugins/backup.rb +0 -90
- data/lib/shrine/plugins/copy.rb +0 -50
- data/lib/shrine/plugins/delete_promoted.rb +0 -20
- data/lib/shrine/plugins/direct_upload.rb +0 -217
- data/lib/shrine/plugins/hooks.rb +0 -90
- data/lib/shrine/plugins/logging.rb +0 -142
- data/lib/shrine/plugins/migration_helpers.rb +0 -70
- data/lib/shrine/plugins/moving.rb +0 -57
- data/lib/shrine/plugins/multi_delete.rb +0 -32
- data/lib/shrine/plugins/parallelize.rb +0 -78
- data/lib/shrine/plugins/parsed_json.rb +0 -29
@@ -6,11 +6,6 @@ class Shrine
|
|
6
6
|
#
|
7
7
|
# [doc/plugins/validation_helpers.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/validation_helpers.md
|
8
8
|
module ValidationHelpers
|
9
|
-
def self.configure(uploader, opts = {})
|
10
|
-
uploader.opts[:validation_default_messages] ||= {}
|
11
|
-
uploader.opts[:validation_default_messages].merge!(opts[:default_messages] || {})
|
12
|
-
end
|
13
|
-
|
14
9
|
DEFAULT_MESSAGES = {
|
15
10
|
max_size: -> (max) { "size must not be greater than #{PRETTY_FILESIZE.call(max)}" },
|
16
11
|
min_size: -> (min) { "size must not be less than #{PRETTY_FILESIZE.call(min)}" },
|
@@ -24,7 +19,7 @@ class Shrine
|
|
24
19
|
mime_type_exclusion: -> (list) { "type must not be one of: #{list.join(", ")}" },
|
25
20
|
extension_inclusion: -> (list) { "extension must be one of: #{list.join(", ")}" },
|
26
21
|
extension_exclusion: -> (list) { "extension must not be one of: #{list.join(", ")}" },
|
27
|
-
}
|
22
|
+
}.freeze
|
28
23
|
|
29
24
|
FILESIZE_UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"].freeze
|
30
25
|
|
@@ -39,10 +34,18 @@ class Shrine
|
|
39
34
|
"%.1f %s" % [bytes.to_f / 1024 ** exp, FILESIZE_UNITS[exp]]
|
40
35
|
end
|
41
36
|
|
37
|
+
def self.load_dependencies(uploader, *)
|
38
|
+
uploader.plugin :validation
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.configure(uploader, default_messages: {}, **opts)
|
42
|
+
uploader.opts[:validation_helpers] ||= { default_messages: DEFAULT_MESSAGES.dup }
|
43
|
+
uploader.opts[:validation_helpers][:default_messages].merge!(default_messages)
|
44
|
+
end
|
45
|
+
|
42
46
|
module AttacherClassMethods
|
43
47
|
def default_validation_messages
|
44
|
-
|
45
|
-
shrine_class.opts[:validation_default_messages])
|
48
|
+
shrine_class.opts[:validation_helpers][:default_messages]
|
46
49
|
end
|
47
50
|
end
|
48
51
|
|
@@ -51,14 +54,14 @@ class Shrine
|
|
51
54
|
#
|
52
55
|
# validate_max_size 5*1024*1024
|
53
56
|
def validate_max_size(max, message: nil)
|
54
|
-
validate_result(
|
57
|
+
validate_result(file.size <= max, :max_size, message, max)
|
55
58
|
end
|
56
59
|
|
57
60
|
# Validates that the `size` metadata is not smaller than `min`.
|
58
61
|
#
|
59
62
|
# validate_min_size 1024
|
60
63
|
def validate_min_size(min, message: nil)
|
61
|
-
validate_result(
|
64
|
+
validate_result(file.size >= min, :min_size, message, min)
|
62
65
|
end
|
63
66
|
|
64
67
|
# Validates that the `size` metadata is in the given range.
|
@@ -76,12 +79,9 @@ class Shrine
|
|
76
79
|
#
|
77
80
|
# validate_max_width 5000
|
78
81
|
def validate_max_width(max, message: nil)
|
79
|
-
fail Error, "
|
80
|
-
|
81
|
-
|
82
|
-
else
|
83
|
-
Shrine.deprecation("Width of the uploaded file is nil, and Shrine skipped the validation. In Shrine 3 the validation will fail if width is nil.")
|
84
|
-
end
|
82
|
+
fail Error, "width metadata is missing" unless file["width"]
|
83
|
+
|
84
|
+
validate_result(file["width"] <= max, :max_width, message, max)
|
85
85
|
end
|
86
86
|
|
87
87
|
# Validates that the `width` metadata is not smaller than `min`.
|
@@ -89,12 +89,9 @@ class Shrine
|
|
89
89
|
#
|
90
90
|
# validate_min_width 100
|
91
91
|
def validate_min_width(min, message: nil)
|
92
|
-
fail Error, "
|
93
|
-
|
94
|
-
|
95
|
-
else
|
96
|
-
Shrine.deprecation("Width of the uploaded file is nil, and Shrine skipped the validation. In Shrine 3 the validation will fail if width is nil.")
|
97
|
-
end
|
92
|
+
fail Error, "width metadata is missing" unless file["width"]
|
93
|
+
|
94
|
+
validate_result(file["width"] >= min, :min_width, message, min)
|
98
95
|
end
|
99
96
|
|
100
97
|
# Validates that the `width` metadata is in the given range.
|
@@ -112,12 +109,9 @@ class Shrine
|
|
112
109
|
#
|
113
110
|
# validate_max_height 5000
|
114
111
|
def validate_max_height(max, message: nil)
|
115
|
-
fail Error, "
|
116
|
-
|
117
|
-
|
118
|
-
else
|
119
|
-
Shrine.deprecation("Height of the uploaded file is nil, and Shrine skipped the validation. In Shrine 3 the validation will fail if height is nil.")
|
120
|
-
end
|
112
|
+
fail Error, "height metadata is missing" unless file["height"]
|
113
|
+
|
114
|
+
validate_result(file["height"] <= max, :max_height, message, max)
|
121
115
|
end
|
122
116
|
|
123
117
|
# Validates that the `height` metadata is not smaller than `min`.
|
@@ -125,12 +119,9 @@ class Shrine
|
|
125
119
|
#
|
126
120
|
# validate_min_height 100
|
127
121
|
def validate_min_height(min, message: nil)
|
128
|
-
fail Error, "
|
129
|
-
|
130
|
-
|
131
|
-
else
|
132
|
-
Shrine.deprecation("Height of the uploaded file is nil, and Shrine skipped the validation. In Shrine 3 the validation will fail if height is nil.")
|
133
|
-
end
|
122
|
+
fail Error, "height metadata is missing" unless file["height"]
|
123
|
+
|
124
|
+
validate_result(file["height"] >= min, :min_height, message, min)
|
134
125
|
end
|
135
126
|
|
136
127
|
# Validates that the `height` metadata is in the given range.
|
@@ -146,11 +137,10 @@ class Shrine
|
|
146
137
|
#
|
147
138
|
# validate_max_dimensions [5000, 5000]
|
148
139
|
def validate_max_dimensions((max_width, max_height), message: nil)
|
149
|
-
fail Error, "
|
150
|
-
fail Error, "width or height metadata is nil" unless get.width && get.height
|
140
|
+
fail Error, "width and/or height metadata is missing" unless file["width"] && file["height"]
|
151
141
|
|
152
142
|
validate_result(
|
153
|
-
|
143
|
+
file["width"] <= max_width && file["height"] <= max_height,
|
154
144
|
:max_dimensions, message, [max_width, max_height]
|
155
145
|
)
|
156
146
|
end
|
@@ -159,11 +149,10 @@ class Shrine
|
|
159
149
|
#
|
160
150
|
# validate_max_dimensions [100, 100]
|
161
151
|
def validate_min_dimensions((min_width, min_height), message: nil)
|
162
|
-
fail Error, "
|
163
|
-
fail Error, "width or height metadata is nil" unless get.width && get.height
|
152
|
+
fail Error, "width and/or height metadata is missing" unless file["width"] && file["height"]
|
164
153
|
|
165
154
|
validate_result(
|
166
|
-
|
155
|
+
file["width"] >= min_width && file["height"] >= min_height,
|
167
156
|
:min_dimensions, message, [min_width, min_height]
|
168
157
|
)
|
169
158
|
end
|
@@ -184,7 +173,7 @@ class Shrine
|
|
184
173
|
# validate_mime_type_inclusion %w[audio/mp3 audio/flac]
|
185
174
|
def validate_mime_type_inclusion(types, message: nil)
|
186
175
|
validate_result(
|
187
|
-
types.
|
176
|
+
types.include?(file.mime_type),
|
188
177
|
:mime_type_inclusion, message, types
|
189
178
|
)
|
190
179
|
end
|
@@ -196,7 +185,7 @@ class Shrine
|
|
196
185
|
# validate_mime_type_exclusion %w[text/x-php]
|
197
186
|
def validate_mime_type_exclusion(types, message: nil)
|
198
187
|
validate_result(
|
199
|
-
types.
|
188
|
+
!types.include?(file.mime_type),
|
200
189
|
:mime_type_exclusion, message, types
|
201
190
|
)
|
202
191
|
end
|
@@ -207,7 +196,7 @@ class Shrine
|
|
207
196
|
# validate_extension_inclusion %w[jpg jpeg png gif]
|
208
197
|
def validate_extension_inclusion(extensions, message: nil)
|
209
198
|
validate_result(
|
210
|
-
extensions.any? { |extension|
|
199
|
+
extensions.any? { |extension| extension.casecmp(file.extension.to_s) == 0 },
|
211
200
|
:extension_inclusion, message, extensions
|
212
201
|
)
|
213
202
|
end
|
@@ -219,7 +208,7 @@ class Shrine
|
|
219
208
|
# validate_extension_exclusion %[php jar]
|
220
209
|
def validate_extension_exclusion(extensions, message: nil)
|
221
210
|
validate_result(
|
222
|
-
extensions.none? { |extension|
|
211
|
+
extensions.none? { |extension| extension.casecmp(file.extension.to_s) == 0 },
|
223
212
|
:extension_exclusion, message, extensions
|
224
213
|
)
|
225
214
|
end
|
@@ -236,16 +225,6 @@ class Shrine
|
|
236
225
|
end
|
237
226
|
end
|
238
227
|
|
239
|
-
# Converts a string to a regex.
|
240
|
-
def regex(value)
|
241
|
-
if value.is_a?(Regexp)
|
242
|
-
Shrine.deprecation("Passing regexes to type/extension whitelists/blacklists in validation_helpers plugin is deprecated and will be removed in Shrine 3. Use strings instead.")
|
243
|
-
value
|
244
|
-
else
|
245
|
-
/\A#{Regexp.escape(value)}\z/i
|
246
|
-
end
|
247
|
-
end
|
248
|
-
|
249
228
|
# Generates an error message and appends it to errors array.
|
250
229
|
def add_error(*args)
|
251
230
|
errors << error_message(*args)
|
@@ -1,113 +1,72 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
Shrine.deprecation("The versions plugin is deprecated and will be removed in Shrine 4. Use the new derivatives plugin instead.")
|
4
|
+
|
3
5
|
class Shrine
|
4
6
|
module Plugins
|
5
7
|
# Documentation lives in [doc/plugins/versions.md] on GitHub.
|
6
8
|
#
|
7
9
|
# [doc/plugins/versions.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/versions.md
|
8
10
|
module Versions
|
9
|
-
def self.load_dependencies(uploader,
|
11
|
+
def self.load_dependencies(uploader, **)
|
12
|
+
uploader.plugin :processing
|
10
13
|
uploader.plugin :default_url
|
11
14
|
end
|
12
15
|
|
13
|
-
def self.configure(uploader, opts
|
14
|
-
|
15
|
-
|
16
|
-
uploader.opts[:version_names] = opts.fetch(:names, uploader.opts[:version_names])
|
17
|
-
uploader.opts[:version_fallbacks] = opts.fetch(:fallbacks, uploader.opts.fetch(:version_fallbacks, {}))
|
18
|
-
uploader.opts[:versions_fallback_to_original] = opts.fetch(:fallback_to_original, uploader.opts.fetch(:versions_fallback_to_original, true))
|
16
|
+
def self.configure(uploader, **opts)
|
17
|
+
uploader.opts[:versions] ||= { fallbacks: {}, fallback_to_original: true }
|
18
|
+
uploader.opts[:versions].merge!(opts)
|
19
19
|
end
|
20
20
|
|
21
21
|
module ClassMethods
|
22
|
-
def version_names
|
23
|
-
Shrine.deprecation("Shrine.version_names is deprecated and will be removed in Shrine 3.")
|
24
|
-
opts[:version_names]
|
25
|
-
end
|
26
|
-
|
27
22
|
def version_fallbacks
|
28
|
-
opts[:
|
29
|
-
end
|
30
|
-
|
31
|
-
# Checks that the identifier is a registered version.
|
32
|
-
def version?(name)
|
33
|
-
Shrine.deprecation("Shrine.version? is deprecated and will be removed in Shrine 3.")
|
34
|
-
version_names.nil? || version_names.map(&:to_s).include?(name.to_s)
|
23
|
+
opts[:versions][:fallbacks]
|
35
24
|
end
|
36
25
|
|
37
26
|
# Converts a hash of data into a hash of versions.
|
38
|
-
def uploaded_file(object
|
39
|
-
|
40
|
-
|
41
|
-
|
27
|
+
def uploaded_file(object)
|
28
|
+
object = JSON.parse(object) if object.is_a?(String)
|
29
|
+
|
30
|
+
Utils.deep_map(object, transform_keys: :to_sym) do |path, value|
|
31
|
+
if value.is_a?(Hash) && (value["id"].is_a?(String) || value[:id].is_a?(String))
|
32
|
+
file = super(value)
|
33
|
+
elsif value.is_a?(UploadedFile)
|
34
|
+
file = value
|
35
|
+
end
|
36
|
+
|
37
|
+
if file
|
38
|
+
yield file if block_given?
|
39
|
+
file
|
42
40
|
end
|
43
|
-
elsif object.is_a?(Array)
|
44
|
-
object.map { |value| uploaded_file(value, &block) }
|
45
|
-
else
|
46
|
-
super
|
47
41
|
end
|
48
42
|
end
|
49
43
|
end
|
50
44
|
|
51
45
|
module InstanceMethods
|
52
|
-
|
53
|
-
|
54
|
-
if object.is_a?(Hash)
|
55
|
-
object.all? { |name, version| uploaded?(version) }
|
56
|
-
elsif object.is_a?(Array)
|
57
|
-
object.all? { |version| uploaded?(version) }
|
58
|
-
else
|
59
|
-
super
|
60
|
-
end
|
61
|
-
end
|
46
|
+
def upload(io, **options)
|
47
|
+
files = process(io, **options) || io
|
62
48
|
|
63
|
-
|
64
|
-
|
65
|
-
# Stores each version individually. It asserts that all versions are
|
66
|
-
# known, because later the versions will be silently filtered, so
|
67
|
-
# we want to let the user know that they forgot to register a new
|
68
|
-
# version.
|
69
|
-
def _store(io, context)
|
70
|
-
if (hash = io).is_a?(Hash)
|
71
|
-
raise Error, ":location is not applicable to versions" if context.key?(:location)
|
72
|
-
raise Error, "detected multiple versions that point to the same IO object: given versions: #{hash.keys}, unique versions: #{hash.invert.invert.keys}" if hash.invert.invert != hash
|
73
|
-
|
74
|
-
hash.inject({}) do |result, (name, value)|
|
75
|
-
result.merge!(name.to_sym => _store(value, context.merge(version: name.to_sym){|_, v1, v2| Array(v1) + Array(v2)}))
|
76
|
-
end
|
77
|
-
elsif (array = io).is_a?(Array)
|
78
|
-
array.map.with_index { |value, idx| _store(value, context.merge(version: idx){|_, v1, v2| Array(v1) + Array(v2)}) }
|
79
|
-
else
|
80
|
-
super
|
81
|
-
end
|
82
|
-
end
|
49
|
+
Utils.map_file(files) do |name, version|
|
50
|
+
options.merge!(version: name.one? ? name.first : name) if name
|
83
51
|
|
84
|
-
|
85
|
-
def _delete(uploaded_file, context)
|
86
|
-
if (hash = uploaded_file).is_a?(Hash)
|
87
|
-
hash.each do |name, value|
|
88
|
-
_delete(value, context)
|
89
|
-
end
|
90
|
-
elsif (array = uploaded_file).is_a?(Array)
|
91
|
-
array.each do |value|
|
92
|
-
_delete(value, context)
|
93
|
-
end
|
94
|
-
else
|
95
|
-
super
|
52
|
+
super(version, **options, process: false)
|
96
53
|
end
|
97
54
|
end
|
98
55
|
end
|
99
56
|
|
100
57
|
module AttacherMethods
|
58
|
+
def destroy(*)
|
59
|
+
Utils.each_file(self.file) { |_, file| file.delete }
|
60
|
+
end
|
61
|
+
|
101
62
|
# Smart versioned URLs, which include the version name in the default
|
102
63
|
# URL, and properly forwards any options to the underlying storage.
|
103
64
|
def url(version = nil, **options)
|
104
|
-
|
105
|
-
|
106
|
-
if attachment.is_a?(Hash)
|
65
|
+
if file.is_a?(Hash)
|
107
66
|
if version
|
108
67
|
version = version.to_sym
|
109
|
-
if
|
110
|
-
|
68
|
+
if file.key?(version)
|
69
|
+
file[version].url(**options)
|
111
70
|
elsif fallback = shrine_class.version_fallbacks[version]
|
112
71
|
url(fallback, **options)
|
113
72
|
else
|
@@ -118,8 +77,8 @@ class Shrine
|
|
118
77
|
end
|
119
78
|
else
|
120
79
|
if version
|
121
|
-
if
|
122
|
-
|
80
|
+
if file && shrine_class.opts[:versions][:fallback_to_original]
|
81
|
+
file.url(**options)
|
123
82
|
else
|
124
83
|
default_url(**options, version: version)
|
125
84
|
end
|
@@ -129,22 +88,85 @@ class Shrine
|
|
129
88
|
end
|
130
89
|
end
|
131
90
|
|
91
|
+
# Converts the Hash/Array of UploadedFile objects into a Hash/Array of data.
|
92
|
+
def data
|
93
|
+
Utils.map_file(file, transform_keys: :to_s) do |_, version|
|
94
|
+
version.data
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def file=(file)
|
99
|
+
if file.is_a?(Hash) || file.is_a?(Array)
|
100
|
+
@file = file
|
101
|
+
else
|
102
|
+
super
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def uploaded_file(value, &block)
|
107
|
+
shrine_class.uploaded_file(value, &block)
|
108
|
+
end
|
109
|
+
|
132
110
|
private
|
133
111
|
|
134
|
-
def
|
135
|
-
|
112
|
+
def uploaded?(file, storage_key)
|
113
|
+
if file.is_a?(Hash) || file.is_a?(Array)
|
114
|
+
Utils.each_file(file).all? { |_, f| f.storage_key == storage_key }
|
115
|
+
else
|
116
|
+
super
|
117
|
+
end
|
136
118
|
end
|
119
|
+
end
|
137
120
|
|
138
|
-
|
139
|
-
|
121
|
+
module Utils
|
122
|
+
module_function
|
123
|
+
|
124
|
+
def each_file(object)
|
125
|
+
return enum_for(__method__, object) unless block_given?
|
126
|
+
|
127
|
+
map_file(object) do |path, file|
|
128
|
+
yield path, file
|
129
|
+
file
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def map_file(object, transform_keys: :to_sym)
|
134
|
+
if object.is_a?(Hash) || object.is_a?(Array)
|
135
|
+
deep_map(object, transform_keys: transform_keys) do |path, value|
|
136
|
+
yield path, value unless value.is_a?(Hash) || value.is_a?(Array)
|
137
|
+
end
|
138
|
+
elsif object
|
139
|
+
yield nil, object
|
140
|
+
else
|
141
|
+
object
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def deep_map(object, path = [], transform_keys:, &block)
|
140
146
|
if object.is_a?(Hash)
|
141
|
-
|
142
|
-
|
147
|
+
result = yield path, object
|
148
|
+
|
149
|
+
return result if result
|
150
|
+
|
151
|
+
object.inject({}) do |hash, (key, value)|
|
152
|
+
key = key.send(transform_keys)
|
153
|
+
result = yield [*path, key], value
|
154
|
+
|
155
|
+
hash.merge! key => (result || deep_map(value, [*path, key], transform_keys: transform_keys, &block))
|
143
156
|
end
|
144
157
|
elsif object.is_a?(Array)
|
145
|
-
|
158
|
+
result = yield path, object
|
159
|
+
|
160
|
+
return result if result
|
161
|
+
|
162
|
+
object.map.with_index do |value, idx|
|
163
|
+
result = yield [*path, idx], value
|
164
|
+
|
165
|
+
result || deep_map(value, [*path, idx], transform_keys: transform_keys, &block)
|
166
|
+
end
|
146
167
|
else
|
147
|
-
|
168
|
+
result = yield path, object
|
169
|
+
result or fail Shrine::Error, "leaf reached"
|
148
170
|
end
|
149
171
|
end
|
150
172
|
end
|
@@ -6,7 +6,7 @@ require "pathname"
|
|
6
6
|
class Shrine
|
7
7
|
module Storage
|
8
8
|
class FileSystem
|
9
|
-
attr_reader :directory, :prefix, :
|
9
|
+
attr_reader :directory, :prefix, :permissions, :directory_permissions
|
10
10
|
|
11
11
|
# Initializes a storage for uploading to the filesystem.
|
12
12
|
#
|
@@ -28,9 +28,7 @@ class Shrine
|
|
28
28
|
# : By default empty folders inside the directory are automatically
|
29
29
|
# deleted, but if it happens that it causes too much load on the
|
30
30
|
# filesystem, you can set this option to `false`.
|
31
|
-
def initialize(directory, prefix: nil,
|
32
|
-
Shrine.deprecation("The :host option to Shrine::Storage::FileSystem#initialize is deprecated and will be removed in Shrine 3. Pass :host to FileSystem#url instead, you can also use default_url_options plugin.") if host
|
33
|
-
|
31
|
+
def initialize(directory, prefix: nil, clean: true, permissions: 0644, directory_permissions: 0755)
|
34
32
|
if prefix
|
35
33
|
@prefix = Pathname(relative(prefix))
|
36
34
|
@directory = Pathname(directory).join(@prefix).expand_path
|
@@ -38,7 +36,6 @@ class Shrine
|
|
38
36
|
@directory = Pathname(directory).expand_path
|
39
37
|
end
|
40
38
|
|
41
|
-
@host = host
|
42
39
|
@permissions = permissions
|
43
40
|
@directory_permissions = directory_permissions
|
44
41
|
@clean = clean
|
@@ -51,39 +48,21 @@ class Shrine
|
|
51
48
|
|
52
49
|
# Copies the file into the given location.
|
53
50
|
def upload(io, id, move: false, **)
|
54
|
-
if move && movable?(io
|
55
|
-
move(io, id)
|
51
|
+
if move && movable?(io)
|
52
|
+
move(io, path!(id))
|
56
53
|
else
|
57
54
|
IO.copy_stream(io, path!(id))
|
58
|
-
|
59
|
-
path(id).chmod(permissions) if permissions
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
# Moves the file to the given location. This gets called by the `moving`
|
64
|
-
# plugin.
|
65
|
-
def move(io, id, **)
|
66
|
-
if io.respond_to?(:path)
|
67
|
-
FileUtils.mv io.path, path!(id)
|
68
|
-
else
|
69
|
-
FileUtils.mv io.storage.path(io.id), path!(id)
|
70
|
-
io.storage.clean(io.storage.path(io.id)) if io.storage.clean?
|
71
55
|
end
|
72
56
|
|
73
57
|
path(id).chmod(permissions) if permissions
|
74
58
|
end
|
75
59
|
|
76
|
-
# Returns true if the file is a `File` or a UploadedFile uploaded by the
|
77
|
-
# FileSystem storage.
|
78
|
-
def movable?(io, id)
|
79
|
-
io.respond_to?(:path) ||
|
80
|
-
(io.is_a?(UploadedFile) && io.storage.is_a?(Storage::FileSystem))
|
81
|
-
end
|
82
|
-
|
83
60
|
# Opens the file on the given location in read mode. Accepts additional
|
84
61
|
# `File.open` arguments.
|
85
|
-
def open(id, **options
|
86
|
-
path(id).open(binmode: true, **options
|
62
|
+
def open(id, **options)
|
63
|
+
path(id).open(binmode: true, **options)
|
64
|
+
rescue Errno::ENOENT
|
65
|
+
raise Shrine::FileNotFound, "file #{id.inspect} not found on storage"
|
87
66
|
end
|
88
67
|
|
89
68
|
# Returns true if the file exists on the filesystem.
|
@@ -105,7 +84,7 @@ class Shrine
|
|
105
84
|
# from the returned path (e.g. #directory can be set to "public" folder).
|
106
85
|
# Both cases accept a `:host` value which will be prefixed to the
|
107
86
|
# generated path.
|
108
|
-
def url(id, host:
|
87
|
+
def url(id, host: nil, **options)
|
109
88
|
path = (prefix ? relative_path(id) : path(id)).to_s
|
110
89
|
host ? host + path : path
|
111
90
|
end
|
@@ -115,15 +94,10 @@ class Shrine
|
|
115
94
|
#
|
116
95
|
# file_system.clear! # deletes all files and subdirectories in the storage directory
|
117
96
|
# file_system.clear! { |path| path.mtime < Time.now - 7*24*60*60 } # deletes only files older than 1 week
|
118
|
-
def clear!(
|
119
|
-
if
|
97
|
+
def clear!(&condition)
|
98
|
+
if condition
|
120
99
|
list_files(directory) do |path|
|
121
|
-
|
122
|
-
Shrine.deprecation("The :older_than option to FileSystem#clear! is deprecated and will be removed in Shrine 3. You should use a block instead, e.g. `storage.clear! { |path| path.mtime < Time.now - 7*24*60*60 }`.")
|
123
|
-
next unless path.mtime < older_than
|
124
|
-
else
|
125
|
-
next unless condition.call(path)
|
126
|
-
end
|
100
|
+
next unless condition.call(path)
|
127
101
|
path.delete
|
128
102
|
clean(path) if clean?
|
129
103
|
end
|
@@ -137,15 +111,6 @@ class Shrine
|
|
137
111
|
directory.join(id.gsub("/", File::SEPARATOR))
|
138
112
|
end
|
139
113
|
|
140
|
-
# Catches the deprecated `#download` method.
|
141
|
-
def method_missing(name, *args, &block)
|
142
|
-
case name
|
143
|
-
when :download then deprecated_download(*args, &block)
|
144
|
-
else
|
145
|
-
super
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
114
|
protected
|
150
115
|
|
151
116
|
# Cleans all empty subdirectories up the hierarchy.
|
@@ -165,6 +130,24 @@ class Shrine
|
|
165
130
|
|
166
131
|
private
|
167
132
|
|
133
|
+
# Moves the file to the given location. This gets called by the `moving`
|
134
|
+
# plugin.
|
135
|
+
def move(io, path)
|
136
|
+
if io.respond_to?(:path)
|
137
|
+
FileUtils.mv io.path, path
|
138
|
+
else
|
139
|
+
FileUtils.mv io.storage.path(io.id), path
|
140
|
+
io.storage.clean(io.storage.path(io.id)) if io.storage.clean?
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Returns true if the file is a `File` or a UploadedFile uploaded by the
|
145
|
+
# FileSystem storage.
|
146
|
+
def movable?(io)
|
147
|
+
io.respond_to?(:path) ||
|
148
|
+
(io.is_a?(UploadedFile) && io.storage.is_a?(Storage::FileSystem))
|
149
|
+
end
|
150
|
+
|
168
151
|
# Creates all intermediate directories for that location.
|
169
152
|
def path!(id)
|
170
153
|
path = path(id)
|
@@ -191,20 +174,12 @@ class Shrine
|
|
191
174
|
Dir.empty?(path)
|
192
175
|
end
|
193
176
|
else
|
177
|
+
# :nocov:
|
194
178
|
def dir_empty?(path)
|
195
179
|
Dir.foreach(path) { |x| return false unless [".", ".."].include?(x) }
|
196
180
|
true
|
197
181
|
end
|
198
|
-
|
199
|
-
|
200
|
-
def deprecated_download(id, **options)
|
201
|
-
Shrine.deprecation("Shrine::Storage::FileSystem#download is deprecated and will be removed in Shrine 3.")
|
202
|
-
tempfile = Tempfile.new(["shrine-filesystem", File.extname(id)], binmode: true)
|
203
|
-
open(id, **options) { |file| IO.copy_stream(file, tempfile) }
|
204
|
-
tempfile.tap(&:open)
|
205
|
-
rescue
|
206
|
-
tempfile.close! if tempfile
|
207
|
-
raise
|
182
|
+
# :nocov:
|
208
183
|
end
|
209
184
|
end
|
210
185
|
end
|