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.

Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +299 -11
  3. data/README.md +9 -3
  4. data/doc/advantages.md +1 -1
  5. data/doc/carrierwave.md +4 -4
  6. data/doc/creating_persistence_plugins.md +172 -0
  7. data/doc/creating_plugins.md +1 -1
  8. data/doc/creating_storages.md +3 -1
  9. data/doc/design.md +2 -2
  10. data/doc/direct_s3.md +0 -22
  11. data/doc/paperclip.md +3 -3
  12. data/doc/plugins/activerecord.md +211 -42
  13. data/doc/plugins/atomic_helpers.md +153 -0
  14. data/doc/plugins/column.md +90 -0
  15. data/doc/plugins/derivation_endpoint.md +54 -62
  16. data/doc/plugins/derivatives.md +752 -0
  17. data/doc/plugins/entity.md +204 -0
  18. data/doc/plugins/infer_extension.md +8 -8
  19. data/doc/plugins/instrumentation.md +33 -13
  20. data/doc/plugins/keep_files.md +5 -15
  21. data/doc/plugins/model.md +157 -0
  22. data/doc/plugins/presign_endpoint.md +2 -1
  23. data/doc/plugins/refresh_metadata.md +44 -7
  24. data/doc/plugins/sequel.md +190 -33
  25. data/doc/plugins/{default_url_options.md → url_options.md} +5 -5
  26. data/doc/processing.md +1 -1
  27. data/doc/release_notes/1.1.0.md +2 -2
  28. data/doc/release_notes/2.15.0.md +1 -1
  29. data/doc/storage/s3.md +2 -2
  30. data/doc/testing.md +1 -1
  31. data/lib/shrine.rb +72 -138
  32. data/lib/shrine/attacher.rb +272 -176
  33. data/lib/shrine/attachment.rb +2 -42
  34. data/lib/shrine/plugins/activerecord.rb +103 -26
  35. data/lib/shrine/plugins/add_metadata.rb +9 -10
  36. data/lib/shrine/plugins/atomic_helpers.rb +111 -0
  37. data/lib/shrine/plugins/attacher_options.rb +55 -0
  38. data/lib/shrine/plugins/backgrounding.rb +147 -115
  39. data/lib/shrine/plugins/cached_attachment_data.rb +6 -9
  40. data/lib/shrine/plugins/column.rb +104 -0
  41. data/lib/shrine/plugins/data_uri.rb +35 -38
  42. data/lib/shrine/plugins/default_storage.rb +18 -12
  43. data/lib/shrine/plugins/default_url.rb +11 -21
  44. data/lib/shrine/plugins/default_url_options.rb +3 -30
  45. data/lib/shrine/plugins/delete_raw.rb +9 -13
  46. data/lib/shrine/plugins/derivation_endpoint.rb +75 -114
  47. data/lib/shrine/plugins/derivatives.rb +576 -0
  48. data/lib/shrine/plugins/determine_mime_type.rb +3 -15
  49. data/lib/shrine/plugins/download_endpoint.rb +83 -131
  50. data/lib/shrine/plugins/dynamic_storage.rb +4 -8
  51. data/lib/shrine/plugins/entity.rb +128 -0
  52. data/lib/shrine/plugins/form_assign.rb +107 -0
  53. data/lib/shrine/plugins/included.rb +4 -3
  54. data/lib/shrine/plugins/infer_extension.rb +10 -17
  55. data/lib/shrine/plugins/instrumentation.rb +45 -25
  56. data/lib/shrine/plugins/keep_files.rb +2 -12
  57. data/lib/shrine/plugins/metadata_attributes.rb +15 -14
  58. data/lib/shrine/plugins/model.rb +137 -0
  59. data/lib/shrine/plugins/module_include.rb +2 -0
  60. data/lib/shrine/plugins/presign_endpoint.rb +1 -15
  61. data/lib/shrine/plugins/pretty_location.rb +5 -5
  62. data/lib/shrine/plugins/processing.rb +21 -6
  63. data/lib/shrine/plugins/rack_file.rb +1 -39
  64. data/lib/shrine/plugins/rack_response.rb +14 -7
  65. data/lib/shrine/plugins/recache.rb +5 -2
  66. data/lib/shrine/plugins/refresh_metadata.rb +12 -8
  67. data/lib/shrine/plugins/remote_url.rb +44 -53
  68. data/lib/shrine/plugins/remove_attachment.rb +7 -2
  69. data/lib/shrine/plugins/remove_invalid.rb +8 -4
  70. data/lib/shrine/plugins/restore_cached_data.rb +12 -4
  71. data/lib/shrine/plugins/sequel.rb +115 -27
  72. data/lib/shrine/plugins/signature.rb +2 -7
  73. data/lib/shrine/plugins/store_dimensions.rb +13 -27
  74. data/lib/shrine/plugins/upload_endpoint.rb +14 -15
  75. data/lib/shrine/plugins/upload_options.rb +9 -8
  76. data/lib/shrine/plugins/url_options.rb +33 -0
  77. data/lib/shrine/plugins/validation.rb +87 -0
  78. data/lib/shrine/plugins/validation_helpers.rb +33 -54
  79. data/lib/shrine/plugins/versions.rb +106 -84
  80. data/lib/shrine/storage/file_system.rb +32 -57
  81. data/lib/shrine/storage/linter.rb +9 -1
  82. data/lib/shrine/storage/memory.rb +42 -0
  83. data/lib/shrine/storage/s3.rb +38 -146
  84. data/lib/shrine/uploaded_file.rb +22 -29
  85. data/lib/shrine/version.rb +4 -4
  86. data/shrine.gemspec +2 -3
  87. metadata +27 -54
  88. data/doc/plugins/backup.md +0 -31
  89. data/doc/plugins/copy.md +0 -24
  90. data/doc/plugins/delete_promoted.md +0 -12
  91. data/doc/plugins/direct_upload.md +0 -172
  92. data/doc/plugins/hooks.md +0 -58
  93. data/doc/plugins/logging.md +0 -42
  94. data/doc/plugins/migration_helpers.md +0 -60
  95. data/doc/plugins/moving.md +0 -19
  96. data/doc/plugins/multi_delete.md +0 -20
  97. data/doc/plugins/parallelize.md +0 -16
  98. data/doc/plugins/parsed_json.md +0 -23
  99. data/lib/shrine/plugins/background_helpers.rb +0 -5
  100. data/lib/shrine/plugins/backup.rb +0 -90
  101. data/lib/shrine/plugins/copy.rb +0 -50
  102. data/lib/shrine/plugins/delete_promoted.rb +0 -20
  103. data/lib/shrine/plugins/direct_upload.rb +0 -217
  104. data/lib/shrine/plugins/hooks.rb +0 -90
  105. data/lib/shrine/plugins/logging.rb +0 -142
  106. data/lib/shrine/plugins/migration_helpers.rb +0 -70
  107. data/lib/shrine/plugins/moving.rb +0 -57
  108. data/lib/shrine/plugins/multi_delete.rb +0 -32
  109. data/lib/shrine/plugins/parallelize.rb +0 -78
  110. 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
- @default_validation_messages ||= DEFAULT_MESSAGES.merge(
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(get.size <= max, :max_size, message, max)
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(get.size >= min, :min_size, message, min)
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, ":store_dimensions plugin is required" unless get.respond_to?(:width)
80
- if get.width
81
- validate_result(get.width <= max, :max_width, message, max)
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, ":store_dimensions plugin is required" unless get.respond_to?(:width)
93
- if get.width
94
- validate_result(get.width >= min, :min_width, message, min)
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, ":store_dimensions plugin is required" unless get.respond_to?(:height)
116
- if get.height
117
- validate_result(get.height <= max, :max_height, message, max)
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, ":store_dimensions plugin is required" unless get.respond_to?(:height)
129
- if get.height
130
- validate_result(get.height >= min, :min_height, message, min)
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, ":store_dimensions plugin is required" unless get.respond_to?(:width) && get.respond_to?(:height)
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
- get.width <= max_width && get.height <= max_height,
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, ":store_dimensions plugin is required" unless get.respond_to?(:width) && get.respond_to?(:height)
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
- get.width >= min_width && get.height >= min_height,
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.any? { |type| regex(type) =~ get.mime_type.to_s },
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.none? { |type| regex(type) =~ get.mime_type.to_s },
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| regex(extension) =~ get.extension.to_s },
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| regex(extension) =~ get.extension.to_s },
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
- Shrine.deprecation("The versions Shrine plugin doesn't need the :names option anymore, you can safely remove it.") if opts.key?(:names)
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[:version_fallbacks]
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, &block)
39
- if object.is_a?(Hash) && object.values.none? { |value| value.is_a?(String) }
40
- object.inject({}) do |result, (name, value)|
41
- result.merge!(name.to_sym => uploaded_file(value, &block))
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
- # Checks whether all versions are uploaded by this uploader.
53
- def uploaded?(object)
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
- private
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
- # Deletes each file individually
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
- attachment = get
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 attachment.key?(version)
110
- attachment[version].url(**options)
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 attachment && fallback_to_original?
122
- attachment.url(**options)
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 fallback_to_original?
135
- shrine_class.opts[:versions_fallback_to_original]
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
- # Converts the Hash/Array of UploadedFile objects into a Hash/Array of data.
139
- def convert_to_data(object)
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
- object.inject({}) do |hash, (name, value)|
142
- hash.merge!(name => convert_to_data(value))
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
- object.map { |value| convert_to_data(value) }
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
- super
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, :host, :permissions, :directory_permissions
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, host: nil, clean: true, permissions: 0644, directory_permissions: 0755)
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, id)
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, &block)
86
- path(id).open(binmode: true, **options, &block)
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: self.host, **options)
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!(older_than: nil, &condition)
119
- if older_than || condition
97
+ def clear!(&condition)
98
+ if condition
120
99
  list_files(directory) do |path|
121
- if older_than
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
- end
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