shrine 2.19.3 → 3.6.0

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.
Files changed (211) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +523 -41
  3. data/LICENSE.txt +1 -1
  4. data/README.md +83 -979
  5. data/doc/advantages.md +231 -204
  6. data/doc/attacher.md +304 -153
  7. data/doc/carrierwave.md +297 -226
  8. data/doc/changing_derivatives.md +308 -0
  9. data/doc/changing_location.md +103 -21
  10. data/doc/changing_storage.md +110 -0
  11. data/doc/creating_persistence_plugins.md +132 -0
  12. data/doc/creating_plugins.md +43 -23
  13. data/doc/creating_storages.md +19 -5
  14. data/doc/design.md +147 -97
  15. data/doc/direct_s3.md +38 -28
  16. data/doc/external/articles.md +63 -0
  17. data/doc/external/extensions.md +53 -0
  18. data/doc/external/misc.md +32 -0
  19. data/doc/getting_started.md +1156 -0
  20. data/doc/metadata.md +190 -109
  21. data/doc/multiple_files.md +93 -30
  22. data/doc/paperclip.md +384 -262
  23. data/doc/plugins/activerecord.md +177 -46
  24. data/doc/plugins/add_metadata.md +139 -38
  25. data/doc/plugins/atomic_helpers.md +217 -0
  26. data/doc/plugins/backgrounding.md +156 -98
  27. data/doc/plugins/cached_attachment_data.md +7 -5
  28. data/doc/plugins/column.md +121 -0
  29. data/doc/plugins/data_uri.md +23 -22
  30. data/doc/plugins/default_storage.md +36 -10
  31. data/doc/plugins/default_url.md +30 -13
  32. data/doc/plugins/delete_raw.md +4 -2
  33. data/doc/plugins/derivation_endpoint.md +186 -101
  34. data/doc/plugins/derivatives.md +839 -0
  35. data/doc/plugins/determine_mime_type.md +4 -2
  36. data/doc/plugins/download_endpoint.md +64 -8
  37. data/doc/plugins/dynamic_storage.md +5 -3
  38. data/doc/plugins/entity.md +263 -0
  39. data/doc/plugins/form_assign.md +55 -0
  40. data/doc/plugins/included.md +31 -8
  41. data/doc/plugins/infer_extension.md +21 -10
  42. data/doc/plugins/instrumentation.md +38 -16
  43. data/doc/plugins/keep_files.md +16 -17
  44. data/doc/plugins/metadata_attributes.md +42 -13
  45. data/doc/plugins/mirroring.md +118 -0
  46. data/doc/plugins/model.md +210 -0
  47. data/doc/plugins/module_include.md +4 -2
  48. data/doc/plugins/multi_cache.md +24 -0
  49. data/doc/plugins/persistence.md +101 -0
  50. data/doc/plugins/presign_endpoint.md +9 -4
  51. data/doc/plugins/pretty_location.md +16 -3
  52. data/doc/plugins/processing.md +4 -2
  53. data/doc/plugins/rack_file.md +8 -2
  54. data/doc/plugins/rack_response.md +6 -2
  55. data/doc/plugins/recache.md +4 -2
  56. data/doc/plugins/refresh_metadata.md +49 -9
  57. data/doc/plugins/remote_url.md +84 -47
  58. data/doc/plugins/remove_attachment.md +27 -6
  59. data/doc/plugins/remove_invalid.md +21 -6
  60. data/doc/plugins/restore_cached_data.md +11 -3
  61. data/doc/plugins/sequel.md +159 -35
  62. data/doc/plugins/signature.md +16 -5
  63. data/doc/plugins/store_dimensions.md +14 -2
  64. data/doc/plugins/tempfile.md +4 -2
  65. data/doc/plugins/type_predicates.md +96 -0
  66. data/doc/plugins/upload_endpoint.md +13 -13
  67. data/doc/plugins/upload_options.md +6 -4
  68. data/doc/plugins/{default_url_options.md → url_options.md} +9 -7
  69. data/doc/plugins/validation.md +97 -0
  70. data/doc/plugins/validation_helpers.md +16 -13
  71. data/doc/plugins/versions.md +15 -19
  72. data/doc/processing.md +438 -221
  73. data/doc/refile.md +188 -170
  74. data/doc/release_notes/1.0.0.md +4 -0
  75. data/doc/release_notes/1.1.0.md +6 -2
  76. data/doc/release_notes/1.2.0.md +4 -0
  77. data/doc/release_notes/1.3.0.md +4 -0
  78. data/doc/release_notes/1.4.0.md +4 -0
  79. data/doc/release_notes/1.4.1.md +4 -0
  80. data/doc/release_notes/1.4.2.md +4 -0
  81. data/doc/release_notes/2.0.0.md +4 -0
  82. data/doc/release_notes/2.0.1.md +4 -0
  83. data/doc/release_notes/2.1.0.md +5 -1
  84. data/doc/release_notes/2.1.1.md +4 -0
  85. data/doc/release_notes/2.10.0.md +4 -0
  86. data/doc/release_notes/2.10.1.md +4 -0
  87. data/doc/release_notes/2.11.0.md +4 -0
  88. data/doc/release_notes/2.12.0.md +4 -0
  89. data/doc/release_notes/2.13.0.md +4 -0
  90. data/doc/release_notes/2.14.0.md +5 -1
  91. data/doc/release_notes/2.15.0.md +11 -7
  92. data/doc/release_notes/2.16.0.md +4 -0
  93. data/doc/release_notes/2.17.0.md +4 -0
  94. data/doc/release_notes/2.18.0.md +4 -0
  95. data/doc/release_notes/2.19.0.md +6 -3
  96. data/doc/release_notes/2.2.0.md +4 -0
  97. data/doc/release_notes/2.3.0.md +4 -0
  98. data/doc/release_notes/2.3.1.md +4 -0
  99. data/doc/release_notes/2.4.0.md +4 -0
  100. data/doc/release_notes/2.4.1.md +4 -0
  101. data/doc/release_notes/2.5.0.md +4 -0
  102. data/doc/release_notes/2.6.0.md +4 -0
  103. data/doc/release_notes/2.6.1.md +4 -0
  104. data/doc/release_notes/2.7.0.md +4 -0
  105. data/doc/release_notes/2.8.0.md +4 -0
  106. data/doc/release_notes/2.9.0.md +4 -0
  107. data/doc/release_notes/3.0.0.md +981 -0
  108. data/doc/release_notes/3.0.1.md +22 -0
  109. data/doc/release_notes/3.1.0.md +73 -0
  110. data/doc/release_notes/3.2.0.md +96 -0
  111. data/doc/release_notes/3.2.1.md +31 -0
  112. data/doc/release_notes/3.2.2.md +14 -0
  113. data/doc/release_notes/3.3.0.md +105 -0
  114. data/doc/release_notes/3.4.0.md +35 -0
  115. data/doc/release_notes/3.5.0.md +63 -0
  116. data/doc/release_notes/3.6.0.md +23 -0
  117. data/doc/retrieving_uploads.md +5 -2
  118. data/doc/securing_uploads.md +60 -37
  119. data/doc/storage/file_system.md +20 -3
  120. data/doc/storage/memory.md +19 -0
  121. data/doc/storage/s3.md +122 -78
  122. data/doc/testing.md +141 -133
  123. data/doc/upgrading_to_3.md +708 -0
  124. data/doc/validation.md +54 -90
  125. data/lib/shrine/attacher.rb +292 -169
  126. data/lib/shrine/attachment.rb +13 -46
  127. data/lib/shrine/plugins/_persistence.rb +93 -0
  128. data/lib/shrine/plugins/activerecord.rb +77 -34
  129. data/lib/shrine/plugins/add_metadata.rb +25 -17
  130. data/lib/shrine/plugins/atomic_helpers.rb +119 -0
  131. data/lib/shrine/plugins/backgrounding.rb +77 -113
  132. data/lib/shrine/plugins/cached_attachment_data.rb +6 -15
  133. data/lib/shrine/plugins/column.rb +102 -0
  134. data/lib/shrine/plugins/data_uri.rb +38 -36
  135. data/lib/shrine/plugins/default_storage.rb +45 -15
  136. data/lib/shrine/plugins/default_url.rb +12 -24
  137. data/lib/shrine/plugins/default_url_options.rb +3 -30
  138. data/lib/shrine/plugins/delete_raw.rb +10 -16
  139. data/lib/shrine/plugins/derivation_endpoint.rb +130 -171
  140. data/lib/shrine/plugins/derivatives.rb +645 -0
  141. data/lib/shrine/plugins/determine_mime_type.rb +9 -21
  142. data/lib/shrine/plugins/download_endpoint.rb +118 -133
  143. data/lib/shrine/plugins/dynamic_storage.rb +5 -11
  144. data/lib/shrine/plugins/entity.rb +158 -0
  145. data/lib/shrine/plugins/form_assign.rb +108 -0
  146. data/lib/shrine/plugins/included.rb +6 -6
  147. data/lib/shrine/plugins/infer_extension.rb +17 -20
  148. data/lib/shrine/plugins/instrumentation.rb +59 -43
  149. data/lib/shrine/plugins/keep_files.rb +3 -15
  150. data/lib/shrine/plugins/metadata_attributes.rb +28 -19
  151. data/lib/shrine/plugins/mirroring.rb +142 -0
  152. data/lib/shrine/plugins/model.rb +160 -0
  153. data/lib/shrine/plugins/module_include.rb +3 -3
  154. data/lib/shrine/plugins/multi_cache.rb +27 -0
  155. data/lib/shrine/plugins/presign_endpoint.rb +27 -28
  156. data/lib/shrine/plugins/pretty_location.rb +15 -9
  157. data/lib/shrine/plugins/processing.rb +22 -9
  158. data/lib/shrine/plugins/rack_file.rb +2 -42
  159. data/lib/shrine/plugins/rack_response.rb +21 -10
  160. data/lib/shrine/plugins/recache.rb +6 -5
  161. data/lib/shrine/plugins/refresh_metadata.rb +13 -11
  162. data/lib/shrine/plugins/remote_url.rb +49 -49
  163. data/lib/shrine/plugins/remove_attachment.rb +12 -6
  164. data/lib/shrine/plugins/remove_invalid.rb +19 -8
  165. data/lib/shrine/plugins/restore_cached_data.rb +13 -7
  166. data/lib/shrine/plugins/sequel.rb +86 -36
  167. data/lib/shrine/plugins/signature.rb +10 -16
  168. data/lib/shrine/plugins/store_dimensions.rb +35 -40
  169. data/lib/shrine/plugins/tempfile.rb +1 -3
  170. data/lib/shrine/plugins/type_predicates.rb +113 -0
  171. data/lib/shrine/plugins/upload_endpoint.rb +28 -24
  172. data/lib/shrine/plugins/upload_options.rb +14 -15
  173. data/lib/shrine/plugins/url_options.rb +31 -0
  174. data/lib/shrine/plugins/validation.rb +80 -0
  175. data/lib/shrine/plugins/validation_helpers.rb +35 -58
  176. data/lib/shrine/plugins/versions.rb +107 -87
  177. data/lib/shrine/plugins.rb +22 -0
  178. data/lib/shrine/storage/file_system.rb +46 -64
  179. data/lib/shrine/storage/linter.rb +42 -7
  180. data/lib/shrine/storage/memory.rb +49 -0
  181. data/lib/shrine/storage/s3.rb +173 -160
  182. data/lib/shrine/uploaded_file.rb +32 -32
  183. data/lib/shrine/version.rb +3 -3
  184. data/lib/shrine.rb +87 -150
  185. data/shrine.gemspec +11 -12
  186. metadata +92 -82
  187. data/doc/migrating_storage.md +0 -76
  188. data/doc/plugins/backup.md +0 -31
  189. data/doc/plugins/copy.md +0 -24
  190. data/doc/plugins/delete_promoted.md +0 -12
  191. data/doc/plugins/direct_upload.md +0 -172
  192. data/doc/plugins/hooks.md +0 -58
  193. data/doc/plugins/logging.md +0 -42
  194. data/doc/plugins/migration_helpers.md +0 -60
  195. data/doc/plugins/moving.md +0 -19
  196. data/doc/plugins/multi_delete.md +0 -20
  197. data/doc/plugins/parallelize.md +0 -16
  198. data/doc/plugins/parsed_json.md +0 -23
  199. data/doc/regenerating_versions.md +0 -143
  200. data/lib/shrine/plugins/background_helpers.rb +0 -5
  201. data/lib/shrine/plugins/backup.rb +0 -90
  202. data/lib/shrine/plugins/copy.rb +0 -50
  203. data/lib/shrine/plugins/delete_promoted.rb +0 -20
  204. data/lib/shrine/plugins/direct_upload.rb +0 -217
  205. data/lib/shrine/plugins/hooks.rb +0 -90
  206. data/lib/shrine/plugins/logging.rb +0 -142
  207. data/lib/shrine/plugins/migration_helpers.rb +0 -70
  208. data/lib/shrine/plugins/moving.rb +0 -57
  209. data/lib/shrine/plugins/multi_delete.rb +0 -32
  210. data/lib/shrine/plugins/parallelize.rb +0 -78
  211. data/lib/shrine/plugins/parsed_json.rb +0 -29
@@ -1,113 +1,70 @@
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
- # Documentation lives in [doc/plugins/versions.md] on GitHub.
6
- #
7
- # [doc/plugins/versions.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/versions.md
7
+ # Documentation can be found on https://shrinerb.com/docs/plugins/versions
8
8
  module Versions
9
- def self.load_dependencies(uploader, *)
9
+ def self.load_dependencies(uploader, **)
10
+ uploader.plugin :processing
10
11
  uploader.plugin :default_url
11
12
  end
12
13
 
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))
14
+ def self.configure(uploader, **opts)
15
+ uploader.opts[:versions] ||= { fallbacks: {}, fallback_to_original: true }
16
+ uploader.opts[:versions].merge!(opts)
19
17
  end
20
18
 
21
19
  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
20
  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)
21
+ opts[:versions][:fallbacks]
35
22
  end
36
23
 
37
24
  # 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))
25
+ def uploaded_file(object)
26
+ object = JSON.parse(object) if object.is_a?(String)
27
+
28
+ Utils.deep_map(object, transform_keys: :to_sym) do |path, value|
29
+ if value.is_a?(Hash) && (value["id"].is_a?(String) || value[:id].is_a?(String))
30
+ file = super(value)
31
+ elsif value.is_a?(UploadedFile)
32
+ file = value
33
+ end
34
+
35
+ if file
36
+ yield file if block_given?
37
+ file
42
38
  end
43
- elsif object.is_a?(Array)
44
- object.map { |value| uploaded_file(value, &block) }
45
- else
46
- super
47
39
  end
48
40
  end
49
41
  end
50
42
 
51
43
  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
44
+ def upload(io, **options)
45
+ files = process(io, **options) || io
62
46
 
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
47
+ Utils.map_file(files) do |name, version|
48
+ options.merge!(version: name.one? ? name.first : name) if name
83
49
 
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
50
+ super(version, **options, process: false)
96
51
  end
97
52
  end
98
53
  end
99
54
 
100
55
  module AttacherMethods
56
+ def destroy(*)
57
+ Utils.each_file(self.file) { |_, file| file.delete }
58
+ end
59
+
101
60
  # Smart versioned URLs, which include the version name in the default
102
61
  # URL, and properly forwards any options to the underlying storage.
103
62
  def url(version = nil, **options)
104
- attachment = get
105
-
106
- if attachment.is_a?(Hash)
63
+ if file.is_a?(Hash)
107
64
  if version
108
65
  version = version.to_sym
109
- if attachment.key?(version)
110
- attachment[version].url(**options)
66
+ if file.key?(version)
67
+ file[version].url(**options)
111
68
  elsif fallback = shrine_class.version_fallbacks[version]
112
69
  url(fallback, **options)
113
70
  else
@@ -118,8 +75,8 @@ class Shrine
118
75
  end
119
76
  else
120
77
  if version
121
- if attachment && fallback_to_original?
122
- attachment.url(**options)
78
+ if file && shrine_class.opts[:versions][:fallback_to_original]
79
+ file.url(**options)
123
80
  else
124
81
  default_url(**options, version: version)
125
82
  end
@@ -129,22 +86,85 @@ class Shrine
129
86
  end
130
87
  end
131
88
 
89
+ # Converts the Hash/Array of UploadedFile objects into a Hash/Array of data.
90
+ def data
91
+ Utils.map_file(file, transform_keys: :to_s) do |_, version|
92
+ version.data
93
+ end
94
+ end
95
+
96
+ def file=(file)
97
+ if file.is_a?(Hash) || file.is_a?(Array)
98
+ @file = file
99
+ else
100
+ super
101
+ end
102
+ end
103
+
104
+ def uploaded_file(value, &block)
105
+ shrine_class.uploaded_file(value, &block)
106
+ end
107
+
132
108
  private
133
109
 
134
- def fallback_to_original?
135
- shrine_class.opts[:versions_fallback_to_original]
110
+ def uploaded?(file, storage_key)
111
+ if file.is_a?(Hash) || file.is_a?(Array)
112
+ Utils.each_file(file).all? { |_, f| f.storage_key == storage_key }
113
+ else
114
+ super
115
+ end
136
116
  end
117
+ end
137
118
 
138
- # Converts the Hash/Array of UploadedFile objects into a Hash/Array of data.
139
- def convert_to_data(object)
119
+ module Utils
120
+ module_function
121
+
122
+ def each_file(object)
123
+ return enum_for(__method__, object) unless block_given?
124
+
125
+ map_file(object) do |path, file|
126
+ yield path, file
127
+ file
128
+ end
129
+ end
130
+
131
+ def map_file(object, transform_keys: :to_sym)
132
+ if object.is_a?(Hash) || object.is_a?(Array)
133
+ deep_map(object, transform_keys: transform_keys) do |path, value|
134
+ yield path, value unless value.is_a?(Hash) || value.is_a?(Array)
135
+ end
136
+ elsif object
137
+ yield nil, object
138
+ else
139
+ object
140
+ end
141
+ end
142
+
143
+ def deep_map(object, path = [], transform_keys:, &block)
140
144
  if object.is_a?(Hash)
141
- object.inject({}) do |hash, (name, value)|
142
- hash.merge!(name => convert_to_data(value))
145
+ result = yield path, object
146
+
147
+ return result if result
148
+
149
+ object.inject({}) do |hash, (key, value)|
150
+ key = key.send(transform_keys)
151
+ result = yield [*path, key], value
152
+
153
+ hash.merge! key => (result || deep_map(value, [*path, key], transform_keys: transform_keys, &block))
143
154
  end
144
155
  elsif object.is_a?(Array)
145
- object.map { |value| convert_to_data(value) }
156
+ result = yield path, object
157
+
158
+ return result if result
159
+
160
+ object.map.with_index do |value, idx|
161
+ result = yield [*path, idx], value
162
+
163
+ result || deep_map(value, [*path, idx], transform_keys: transform_keys, &block)
164
+ end
146
165
  else
147
- super
166
+ result = yield path, object
167
+ result or fail Shrine::Error, "leaf reached"
148
168
  end
149
169
  end
150
170
  end
@@ -18,6 +18,28 @@ class Shrine
18
18
  plugin
19
19
  end
20
20
 
21
+ # Delegate call to the plugin in a way that works across Ruby versions.
22
+ def self.load_dependencies(plugin, uploader, *args, **kwargs, &block)
23
+ return unless plugin.respond_to?(:load_dependencies)
24
+
25
+ if kwargs.any?
26
+ plugin.load_dependencies(uploader, *args, **kwargs, &block)
27
+ else
28
+ plugin.load_dependencies(uploader, *args, &block)
29
+ end
30
+ end
31
+
32
+ # Delegate call to the plugin in a way that works across Ruby versions.
33
+ def self.configure(plugin, uploader, *args, **kwargs, &block)
34
+ return unless plugin.respond_to?(:configure)
35
+
36
+ if kwargs.any?
37
+ plugin.configure(uploader, *args, **kwargs, &block)
38
+ else
39
+ plugin.configure(uploader, *args, &block)
40
+ end
41
+ end
42
+
21
43
  # Register the given plugin with Shrine, so that it can be loaded using
22
44
  # `Shrine.plugin` with a symbol. Should be used by plugin files. Example:
23
45
  #
@@ -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.
@@ -91,6 +70,16 @@ class Shrine
91
70
  path(id).exist?
92
71
  end
93
72
 
73
+ # If #prefix is not present, returns a path composed of #directory and
74
+ # the given `id`. If #prefix is present, it excludes the #directory part
75
+ # from the returned path (e.g. #directory can be set to "public" folder).
76
+ # Both cases accept a `:host` value which will be prefixed to the
77
+ # generated path.
78
+ def url(id, host: nil, **options)
79
+ path = (prefix ? relative_path(id) : path(id)).to_s
80
+ host ? host + path : path
81
+ end
82
+
94
83
  # Delets the file, and by default deletes the containing directory if
95
84
  # it's empty.
96
85
  def delete(id)
@@ -100,14 +89,11 @@ class Shrine
100
89
  rescue Errno::ENOENT
101
90
  end
102
91
 
103
- # If #prefix is not present, returns a path composed of #directory and
104
- # the given `id`. If #prefix is present, it excludes the #directory part
105
- # from the returned path (e.g. #directory can be set to "public" folder).
106
- # Both cases accept a `:host` value which will be prefixed to the
107
- # generated path.
108
- def url(id, host: self.host, **options)
109
- path = (prefix ? relative_path(id) : path(id)).to_s
110
- host ? host + path : path
92
+ # Deletes the specified directory on the filesystem.
93
+ #
94
+ # file_system.delete_prefixed("somekey/derivatives/")
95
+ def delete_prefixed(delete_prefix)
96
+ FileUtils.rm_rf directory.join(delete_prefix)
111
97
  end
112
98
 
113
99
  # Deletes all files from the #directory. If a block is passed in, deletes
@@ -115,15 +101,10 @@ class Shrine
115
101
  #
116
102
  # file_system.clear! # deletes all files and subdirectories in the storage directory
117
103
  # 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
104
+ def clear!(&condition)
105
+ if condition
120
106
  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
107
+ next unless condition.call(path)
127
108
  path.delete
128
109
  clean(path) if clean?
129
110
  end
@@ -137,15 +118,6 @@ class Shrine
137
118
  directory.join(id.gsub("/", File::SEPARATOR))
138
119
  end
139
120
 
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
121
  protected
150
122
 
151
123
  # Cleans all empty subdirectories up the hierarchy.
@@ -165,6 +137,24 @@ class Shrine
165
137
 
166
138
  private
167
139
 
140
+ # Moves the file to the given location. This gets called by the `moving`
141
+ # plugin.
142
+ def move(io, path)
143
+ if io.respond_to?(:path)
144
+ FileUtils.mv io.path, path
145
+ else
146
+ FileUtils.mv io.storage.path(io.id), path
147
+ io.storage.clean(io.storage.path(io.id)) if io.storage.clean?
148
+ end
149
+ end
150
+
151
+ # Returns true if the file is a `File` or a UploadedFile uploaded by the
152
+ # FileSystem storage.
153
+ def movable?(io)
154
+ io.respond_to?(:path) ||
155
+ (io.is_a?(UploadedFile) && io.storage.is_a?(Storage::FileSystem))
156
+ end
157
+
168
158
  # Creates all intermediate directories for that location.
169
159
  def path!(id)
170
160
  path = path(id)
@@ -191,20 +181,12 @@ class Shrine
191
181
  Dir.empty?(path)
192
182
  end
193
183
  else
184
+ # :nocov:
194
185
  def dir_empty?(path)
195
186
  Dir.foreach(path) { |x| return false unless [".", ".."].include?(x) }
196
187
  true
197
188
  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
189
+ # :nocov:
208
190
  end
209
191
  end
210
192
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "shrine"
4
2
 
5
3
  require "forwardable"
@@ -30,19 +28,32 @@ class Shrine
30
28
  new(*args).call
31
29
  end
32
30
 
33
- def initialize(storage, action: :error)
34
- @storage = storage
35
- @action = action
31
+ def initialize(storage, action: :error, nonexisting: "nonexisting")
32
+ @storage = storage
33
+ @action = action
34
+ @nonexisting = nonexisting
36
35
  end
37
36
 
38
37
  def call(io_factory = default_io_factory)
39
- storage.upload(io_factory.call, id = "foo".dup, {})
38
+ storage.upload(io_factory.call, id = "foo", shrine_metadata: { "foo" => "bar" })
40
39
 
41
40
  lint_open(id)
42
41
  lint_exists(id)
43
42
  lint_url(id)
44
43
  lint_delete(id)
45
44
 
45
+ if storage.respond_to?(:delete_prefixed)
46
+ storage.upload(io_factory.call, id1 = "a/a/a")
47
+ storage.upload(io_factory.call, id2 = "a/a/b")
48
+ storage.upload(io_factory.call, id3 = "a/aaa/a")
49
+
50
+ lint_delete_prefixed(prefix: "a/a/",
51
+ expect_deleted: [id1, id2],
52
+ expect_remaining: [id3])
53
+
54
+ storage.delete(id3)
55
+ end
56
+
46
57
  if storage.respond_to?(:clear!)
47
58
  storage.upload(io_factory.call, id = "quux".dup)
48
59
  lint_clear(id)
@@ -51,6 +62,8 @@ class Shrine
51
62
  if storage.respond_to?(:presign)
52
63
  lint_presign(id)
53
64
  end
65
+
66
+ true
54
67
  end
55
68
 
56
69
  def lint_open(id)
@@ -58,6 +71,14 @@ class Shrine
58
71
  error :open, "doesn't return a valid IO object" if !io?(opened)
59
72
  error :open, "returns an empty IO object" if opened.read.empty?
60
73
  opened.close
74
+
75
+ begin
76
+ storage.open(@nonexisting)
77
+ error :open, "should raise an exception on nonexisting file"
78
+ rescue Shrine::FileNotFound
79
+ rescue => exception
80
+ error :open, "should raise Shrine::FileNotFound on nonexisting file"
81
+ end
61
82
  end
62
83
 
63
84
  def lint_exists(id)
@@ -86,12 +107,26 @@ class Shrine
86
107
  end
87
108
 
88
109
  def lint_presign(id)
89
- data = storage.presign(id, {})
110
+ data = storage.presign(id)
90
111
  error :presign, "result should be a Hash" unless data.respond_to?(:to_h)
91
112
  error :presign, "result should include :method key" unless data.to_h.key?(:method)
92
113
  error :presign, "result should include :url key" unless data.to_h.key?(:url)
93
114
  end
94
115
 
116
+ def lint_delete_prefixed(prefix:, expect_deleted:, expect_remaining:)
117
+ storage.delete_prefixed(prefix)
118
+
119
+ expect_deleted.each do |key|
120
+ next unless storage.exists?(key)
121
+ error :delete_prefixed, "#{key} still #exists? after #clear_prefix('a/a/')"
122
+ end
123
+
124
+ expect_remaining.each do |key|
125
+ next if storage.exists?(key)
126
+ error :delete_prefixed, "#{key} doesn't #exists? but should after #clear_prefix('a/a/')"
127
+ end
128
+ end
129
+
95
130
  private
96
131
 
97
132
  attr_reader :storage
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shrine"
4
+ require "stringio"
5
+
6
+ class Shrine
7
+ module Storage
8
+ class Memory
9
+ attr_reader :store
10
+
11
+ def initialize(store = {})
12
+ @store = store
13
+ end
14
+
15
+ def upload(io, id, **)
16
+ store[id] = io.read
17
+ end
18
+
19
+ def open(id, **)
20
+ io = StringIO.new(store.fetch(id))
21
+ io.set_encoding(io.string.encoding) # Ruby 2.7.0 – https://bugs.ruby-lang.org/issues/16497
22
+ io
23
+ rescue KeyError
24
+ raise Shrine::FileNotFound, "file #{id.inspect} not found on storage"
25
+ end
26
+
27
+ def exists?(id)
28
+ store.key?(id)
29
+ end
30
+
31
+ def url(id, *)
32
+ "memory://#{id}"
33
+ end
34
+
35
+ def delete(id)
36
+ store.delete(id)
37
+ end
38
+
39
+ def delete_prefixed(delete_prefix)
40
+ delete_prefix = delete_prefix.chomp("/") + "/"
41
+ store.delete_if { |key, _value| key.start_with?(delete_prefix) }
42
+ end
43
+
44
+ def clear!
45
+ store.clear
46
+ end
47
+ end
48
+ end
49
+ end