shrine 2.8.0 → 2.9.0

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +681 -0
  3. data/README.md +73 -21
  4. data/doc/carrierwave.md +75 -20
  5. data/doc/creating_storages.md +15 -26
  6. data/doc/direct_s3.md +113 -31
  7. data/doc/multiple_files.md +4 -8
  8. data/doc/paperclip.md +98 -31
  9. data/doc/refile.md +4 -6
  10. data/doc/testing.md +24 -21
  11. data/lib/shrine.rb +32 -20
  12. data/lib/shrine/plugins/activerecord.rb +2 -0
  13. data/lib/shrine/plugins/add_metadata.rb +2 -0
  14. data/lib/shrine/plugins/background_helpers.rb +2 -0
  15. data/lib/shrine/plugins/backgrounding.rb +11 -4
  16. data/lib/shrine/plugins/backup.rb +2 -0
  17. data/lib/shrine/plugins/cached_attachment_data.rb +2 -0
  18. data/lib/shrine/plugins/copy.rb +2 -0
  19. data/lib/shrine/plugins/data_uri.rb +20 -12
  20. data/lib/shrine/plugins/default_storage.rb +2 -0
  21. data/lib/shrine/plugins/default_url.rb +2 -0
  22. data/lib/shrine/plugins/default_url_options.rb +2 -0
  23. data/lib/shrine/plugins/delete_promoted.rb +2 -0
  24. data/lib/shrine/plugins/delete_raw.rb +2 -0
  25. data/lib/shrine/plugins/determine_mime_type.rb +18 -2
  26. data/lib/shrine/plugins/direct_upload.rb +6 -6
  27. data/lib/shrine/plugins/download_endpoint.rb +2 -0
  28. data/lib/shrine/plugins/dynamic_storage.rb +2 -0
  29. data/lib/shrine/plugins/hooks.rb +2 -0
  30. data/lib/shrine/plugins/included.rb +2 -0
  31. data/lib/shrine/plugins/infer_extension.rb +131 -0
  32. data/lib/shrine/plugins/keep_files.rb +2 -0
  33. data/lib/shrine/plugins/logging.rb +6 -4
  34. data/lib/shrine/plugins/metadata_attributes.rb +2 -0
  35. data/lib/shrine/plugins/migration_helpers.rb +2 -0
  36. data/lib/shrine/plugins/module_include.rb +2 -0
  37. data/lib/shrine/plugins/moving.rb +2 -0
  38. data/lib/shrine/plugins/multi_delete.rb +4 -0
  39. data/lib/shrine/plugins/parallelize.rb +2 -0
  40. data/lib/shrine/plugins/parsed_json.rb +2 -0
  41. data/lib/shrine/plugins/presign_endpoint.rb +7 -7
  42. data/lib/shrine/plugins/pretty_location.rb +2 -0
  43. data/lib/shrine/plugins/processing.rb +2 -0
  44. data/lib/shrine/plugins/rack_file.rb +2 -0
  45. data/lib/shrine/plugins/rack_response.rb +2 -0
  46. data/lib/shrine/plugins/recache.rb +2 -0
  47. data/lib/shrine/plugins/refresh_metadata.rb +2 -0
  48. data/lib/shrine/plugins/remote_url.rb +12 -1
  49. data/lib/shrine/plugins/remove_attachment.rb +2 -0
  50. data/lib/shrine/plugins/remove_invalid.rb +2 -0
  51. data/lib/shrine/plugins/restore_cached_data.rb +2 -0
  52. data/lib/shrine/plugins/sequel.rb +2 -0
  53. data/lib/shrine/plugins/signature.rb +10 -8
  54. data/lib/shrine/plugins/store_dimensions.rb +5 -3
  55. data/lib/shrine/plugins/upload_endpoint.rb +7 -8
  56. data/lib/shrine/plugins/upload_options.rb +2 -0
  57. data/lib/shrine/plugins/validation_helpers.rb +2 -0
  58. data/lib/shrine/plugins/versions.rb +72 -31
  59. data/lib/shrine/storage/file_system.rb +11 -4
  60. data/lib/shrine/storage/linter.rb +5 -13
  61. data/lib/shrine/storage/s3.rb +16 -13
  62. data/lib/shrine/version.rb +3 -1
  63. data/shrine.gemspec +7 -6
  64. metadata +26 -10
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Shrine
2
4
  module Plugins
3
5
  # The `pretty_location` plugin attempts to generate a nicer folder structure
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Shrine
2
4
  module Plugins
3
5
  # Shrine uploaders can define the `#process` method, which will get called
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "forwardable"
2
4
 
3
5
  class Shrine
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rack"
2
4
 
3
5
  class Shrine
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Shrine
2
4
  module Plugins
3
5
  # The `recache` plugin allows you to process your attachment after
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Shrine
2
4
  module Plugins
3
5
  # The `refresh_metadata` plugin allows you to re-extract metadata from an
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "down"
2
4
 
3
5
  class Shrine
@@ -66,8 +68,17 @@ class Shrine
66
68
  # can use the [shrine-url] storage which allows you to assign a custom URL
67
69
  # as cached file ID, and pair that with the `backgrounding` plugin.
68
70
  #
71
+ # ## File extension
72
+ #
73
+ # When attaching from a remote URL, the uploaded file location will have
74
+ # the extension inferred from the URL. However, some URLs might not have an
75
+ # extension, in which case the uploaded file location also won't have the
76
+ # extension. If you want the upload location to always have an extension,
77
+ # you can load the `infer_extension` plugin to infer it from the MIME type.
78
+ #
79
+ # plugin :infer_extension
80
+ #
69
81
  # [Down]: https://github.com/janko-m/down
70
- # [Addressable]: https://github.com/sporkmonger/addressable
71
82
  # [shrine-url]: https://github.com/janko-m/shrine-url
72
83
  module RemoteUrl
73
84
  def self.configure(uploader, opts = {})
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Shrine
2
4
  module Plugins
3
5
  # The `remove_attachment` plugin allows you to delete attachments through
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Shrine
2
4
  module Plugins
3
5
  # The `remove_invalid` plugin automatically deletes a new assigned file if
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Shrine
2
4
  module Plugins
3
5
  # The `restore_cached_data` plugin re-extracts metadata when assigning
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sequel"
2
4
 
3
5
  class Shrine
@@ -1,14 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Shrine
2
4
  module Plugins
3
5
  # The `signature` plugin provides the ability to calculate a hash from file
4
- # content. The hash can then be used as a checksum or just as a unique
5
- # signature of the file.
6
+ # content. This hash can be used as a checksum or just as a unique
7
+ # signature for the uploaded file.
6
8
  #
7
- # plugin :signature
9
+ # Shrine.plugin :signature
8
10
  #
9
- # The plugin adds a `calculate_signature` instance and class method to
10
- # `Shrine`, which accepts the IO object and hashing algorithm and returns
11
- # the calculated hash.
11
+ # The plugin adds a `#calculate_signature` instance and class method to the
12
+ # uploader. The method accepts an IO object and a hashing algorithm, and
13
+ # returns the calculated hash.
12
14
  #
13
15
  # Shrine.calculate_signature(io, :md5)
14
16
  # #=> "9a0364b9e99bb480dd25e1f0284c8555"
@@ -102,14 +104,14 @@ class Shrine
102
104
  def calculate_crc32(io)
103
105
  require "zlib"
104
106
  crc = 0
105
- crc = Zlib.crc32(io.read(16*1024, buffer ||= ""), crc) until io.eof?
107
+ crc = Zlib.crc32(io.read(16*1024, buffer ||= String.new), crc) until io.eof?
106
108
  crc.to_s
107
109
  end
108
110
 
109
111
  def calculate_digest(name, io)
110
112
  require "digest"
111
113
  digest = Digest.const_get(name).new
112
- digest.update(io.read(16*1024, buffer ||= "")) until io.eof?
114
+ digest.update(io.read(16*1024, buffer ||= String.new)) until io.eof?
113
115
  digest.digest
114
116
  end
115
117
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Shrine
2
4
  module Plugins
3
5
  # The `store_dimensions` plugin extracts and stores dimensions of the
@@ -25,9 +27,9 @@ class Shrine
25
27
  #
26
28
  # require "mini_magick"
27
29
  #
28
- # plugin :store_dimensions, analyzer: ->(io, analyzers) do
29
- # dimensions = analyzers[:fastimage].call(io)
30
- # dimensions || MiniMagick::Image.new(io).dimensions
30
+ # plugin :store_dimensions, analyzer: -> (io, analyzers) do
31
+ # dimensions = analyzers[:fastimage].call(io) # try extracting dimensions with FastImage
32
+ # dimensions || MiniMagick::Image.new(io).dimensions # otherwise fall back to MiniMagick
31
33
  # end
32
34
  #
33
35
  # You can also use methods for extracting the dimensions directly:
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rack"
2
4
 
3
5
  require "json"
@@ -5,9 +7,8 @@ require "json"
5
7
  class Shrine
6
8
  module Plugins
7
9
  # The `upload_endpoint` plugin provides a Rack endpoint which accepts file
8
- # uploads and forwards them to specified storage. It can be used with
9
- # client-side file upload libraries like [FineUploader], [Dropzone] or
10
- # [jQuery-File-Upload] for asynchronous uploads.
10
+ # uploads and forwards them to specified storage. On the client side it's
11
+ # recommended to use [Uppy] for asynchronous uploads.
11
12
  #
12
13
  # plugin :upload_endpoint
13
14
  #
@@ -65,7 +66,7 @@ class Shrine
65
66
  #
66
67
  # The upload context will *not* contain `:record` and `:name` values, as
67
68
  # the upload happens independently of a database record. The endpoint will
68
- # sent the following upload context:
69
+ # send the following upload context:
69
70
  #
70
71
  # * `:action` - holds the value `:upload`
71
72
  # * `:request` - holds an instance of `Rack::Request`
@@ -100,9 +101,7 @@ class Shrine
100
101
  #
101
102
  # Shrine.upload_endpoint(:cache, max_size: 20*1024*1024)
102
103
  #
103
- # [FineUploader]: https://github.com/FineUploader/fine-uploader
104
- # [Dropzone]: https://github.com/enyo/dropzone
105
- # [jQuery-File-Upload]: https://github.com/blueimp/jQuery-File-Upload
104
+ # [Uppy]: https://uppy.io
106
105
  module UploadEndpoint
107
106
  def self.load_dependencies(uploader, opts = {})
108
107
  uploader.plugin :rack_file
@@ -135,7 +134,7 @@ class Shrine
135
134
  end
136
135
  end
137
136
 
138
- # Rack application that accepts multipart POSt request to the root URL,
137
+ # Rack application that accepts multipart POST request to the root URL,
139
138
  # calls `#upload` with the uploaded file, and returns the uploaded file
140
139
  # information in JSON format.
141
140
  class App
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Shrine
2
4
  module Plugins
3
5
  # The `upload_options` plugin allows you to automatically pass additional
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Shrine
2
4
  module Plugins
3
5
  # The `validation_helpers` plugin provides helper methods for validating
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Shrine
2
4
  module Plugins
3
5
  # The `versions` plugin enables your uploader to deal with versions, by
@@ -59,21 +61,6 @@ class Shrine
59
61
  # # ...
60
62
  # end
61
63
  #
62
- # ## Original file
63
- #
64
- # If you want to keep the original file, you can include the original
65
- # `Shrine::UploadedFile` object as one of the versions:
66
- #
67
- # process(:store) do |io, context|
68
- # # processing thumbnail
69
- # {original: io, thumbnail: thumbnail}
70
- # end
71
- #
72
- # If both temporary and permanent storage are Amazon S3, the cached original
73
- # will simply be copied over to permanent storage (without any downloading
74
- # and reuploading), so in these cases the performance impact of storing the
75
- # original file in addition to processed versions is neglibible.
76
- #
77
64
  # ## Fallbacks
78
65
  #
79
66
  # If versions are processed in a background job, there will be a period
@@ -106,6 +93,47 @@ class Shrine
106
93
  # user.avatar_url(:thumb_2x) # returns :thumb URL until :thumb_2x becomes available
107
94
  # user.avatar_url(:large_2x) # returns :large URL until :large_2x becomes available
108
95
  #
96
+ # ## Arrays
97
+ #
98
+ # In addition to Hashes, the plugin also supports Arrays of files. For
99
+ # example, you might want to split a PDf into pages:
100
+ #
101
+ # process(:store) do |io, context|
102
+ # pdf = io.download
103
+ # image = MiniMagick::Image.new(pdf.path)
104
+ # versions = []
105
+ #
106
+ # image.pages.each_with_index do |page, index|
107
+ # page_file = Tempfile.new("version-#{index}", binmode: true)
108
+ # MiniMagick::Tool::Convert.new do |convert|
109
+ # convert << page.path
110
+ # convert << page_file.path
111
+ # end
112
+ # page_file.open # refresh updated file
113
+ # versions << page_file
114
+ # end
115
+ #
116
+ # versions # array of pages
117
+ # end
118
+ #
119
+ # You can also combine Hashes and Arrays, there is no limit to the level of
120
+ # nesting.
121
+ #
122
+ # ## Original file
123
+ #
124
+ # If you want to keep the original file, you can include the original
125
+ # `Shrine::UploadedFile` object as one of the versions:
126
+ #
127
+ # process(:store) do |io, context|
128
+ # # processing thumbnail
129
+ # {original: io, thumbnail: thumbnail}
130
+ # end
131
+ #
132
+ # If both temporary and permanent storage are Amazon S3, the cached original
133
+ # will simply be copied over to permanent storage (without any downloading
134
+ # and reuploading), so in these cases the performance impact of storing the
135
+ # original file in addition to processed versions is neglibible.
136
+ #
109
137
  # ## Context
110
138
  #
111
139
  # The version name will be available via `:version` when generating
@@ -122,7 +150,6 @@ class Shrine
122
150
  # [image_processing]: https://github.com/janko-m/image_processing
123
151
  module Versions
124
152
  def self.load_dependencies(uploader, *)
125
- uploader.plugin :multi_delete
126
153
  uploader.plugin :default_url
127
154
  end
128
155
 
@@ -152,10 +179,12 @@ class Shrine
152
179
 
153
180
  # Converts a hash of data into a hash of versions.
154
181
  def uploaded_file(object, &block)
155
- if (hash = object).is_a?(Hash) && !hash.key?("storage")
156
- hash.inject({}) do |result, (name, data)|
157
- result.update(name.to_sym => uploaded_file(data, &block))
182
+ if object.is_a?(Hash) && object.values.none? { |value| value.is_a?(String) }
183
+ object.inject({}) do |result, (name, value)|
184
+ result.merge!(name.to_sym => uploaded_file(value, &block))
158
185
  end
186
+ elsif object.is_a?(Array)
187
+ object.map { |value| uploaded_file(value, &block) }
159
188
  else
160
189
  super
161
190
  end
@@ -164,9 +193,11 @@ class Shrine
164
193
 
165
194
  module InstanceMethods
166
195
  # Checks whether all versions are uploaded by this uploader.
167
- def uploaded?(uploaded_file)
168
- if (hash = uploaded_file).is_a?(Hash)
169
- hash.all? { |name, version| uploaded?(version) }
196
+ def uploaded?(object)
197
+ if object.is_a?(Hash)
198
+ object.all? { |name, version| uploaded?(version) }
199
+ elsif object.is_a?(Array)
200
+ object.all? { |version| uploaded?(version) }
170
201
  else
171
202
  super
172
203
  end
@@ -183,9 +214,11 @@ class Shrine
183
214
  raise Error, ":location is not applicable to versions" if context.key?(:location)
184
215
  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
185
216
 
186
- hash.inject({}) do |result, (name, version)|
187
- result.update(name => _store(version, version: name, **context))
217
+ hash.inject({}) do |result, (name, value)|
218
+ result.merge!(name => _store(value, context.merge(version: name){|_, v1, v2| Array(v1) + Array(v2)}))
188
219
  end
220
+ elsif (array = io).is_a?(Array)
221
+ array.map.with_index { |value, idx| _store(value, context.merge(version: idx){|_, v1, v2| Array(v1) + Array(v2)}) }
189
222
  else
190
223
  super
191
224
  end
@@ -194,8 +227,14 @@ class Shrine
194
227
  # Deletes each file individually, but uses S3's multi delete
195
228
  # capabilities.
196
229
  def _delete(uploaded_file, context)
197
- if (versions = uploaded_file).is_a?(Hash)
198
- _delete(versions.values, context)
230
+ if (hash = uploaded_file).is_a?(Hash)
231
+ hash.each do |name, value|
232
+ _delete(value, context)
233
+ end
234
+ elsif (array = uploaded_file).is_a?(Array)
235
+ array.each do |value|
236
+ _delete(value, context)
237
+ end
199
238
  else
200
239
  super
201
240
  end
@@ -241,16 +280,18 @@ class Shrine
241
280
 
242
281
  def assign_cached(value)
243
282
  cached_file = uploaded_file(value)
244
- Shrine.deprecation("Assigning cached hash of files is deprecated for security reasons and will be removed in Shrine 3.") if cached_file.is_a?(Hash)
283
+ Shrine.deprecation("Assigning cached hash of files is deprecated for security reasons and will be removed in Shrine 3.") if cached_file.is_a?(Hash) || cached_file.is_a?(Array)
245
284
  super(cached_file)
246
285
  end
247
286
 
248
287
  # Converts the Hash of UploadedFile objects into a Hash of data.
249
- def convert_to_data(value)
250
- if value.is_a?(Hash)
251
- value.inject({}) do |hash, (name, uploaded_file)|
252
- hash.merge!(name => super(uploaded_file))
288
+ def convert_to_data(object)
289
+ if object.is_a?(Hash)
290
+ object.inject({}) do |hash, (name, value)|
291
+ hash.merge!(name => convert_to_data(value))
253
292
  end
293
+ elsif object.is_a?(Array)
294
+ object.map { |value| convert_to_data(value) }
254
295
  else
255
296
  super
256
297
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "fileutils"
2
4
  require "pathname"
3
5
 
@@ -201,10 +203,15 @@ class Shrine
201
203
  # Catches the deprecated `#download` method.
202
204
  def method_missing(name, *args)
203
205
  if name == :download
204
- Shrine.deprecation("Shrine::Storage::FileSystem#download is deprecated and will be removed in Shrine 3.")
205
- tempfile = Tempfile.new(["shrine-filesystem", File.extname(args[0])], binmode: true)
206
- open(*args) { |file| IO.copy_stream(file, tempfile) }
207
- tempfile.tap(&:open)
206
+ begin
207
+ Shrine.deprecation("Shrine::Storage::FileSystem#download is deprecated and will be removed in Shrine 3.")
208
+ tempfile = Tempfile.new(["shrine-filesystem", File.extname(args[0])], binmode: true)
209
+ open(*args) { |file| IO.copy_stream(file, tempfile) }
210
+ tempfile.tap(&:open)
211
+ rescue
212
+ tempfile.close! if tempfile
213
+ raise
214
+ end
208
215
  else
209
216
  super
210
217
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "shrine"
2
4
 
3
5
  require "forwardable"
@@ -34,7 +36,7 @@ class Shrine
34
36
  end
35
37
 
36
38
  def call(io_factory = default_io_factory)
37
- storage.upload(io_factory.call, id = "foo", {})
39
+ storage.upload(io_factory.call, id = "foo".dup, {})
38
40
 
39
41
  lint_download(id) if storage.respond_to?(:download)
40
42
  lint_open(id)
@@ -43,17 +45,12 @@ class Shrine
43
45
  lint_delete(id)
44
46
 
45
47
  if storage.respond_to?(:move)
46
- uploaded_file = uploader.upload(io_factory.call, location: "bar")
48
+ uploaded_file = uploader.upload(io_factory.call, location: "bar".dup)
47
49
  lint_move(uploaded_file, "quux")
48
50
  end
49
51
 
50
- if storage.respond_to?(:multi_delete)
51
- storage.upload(io_factory.call, id = "baz")
52
- lint_multi_delete(id)
53
- end
54
-
55
52
  if storage.respond_to?(:clear!)
56
- storage.upload(io_factory.call, id = "quux")
53
+ storage.upload(io_factory.call, id = "quux".dup)
57
54
  lint_clear(id)
58
55
  end
59
56
  end
@@ -99,11 +96,6 @@ class Shrine
99
96
  end
100
97
  end
101
98
 
102
- def lint_multi_delete(id)
103
- storage.multi_delete([id])
104
- error :exists?, "returns true for a file that was multi-deleted" if storage.exists?(id)
105
- end
106
-
107
99
  def lint_clear(id)
108
100
  storage.clear!
109
101
  error :clear!, "file still #exists? after clearing" if storage.exists?(id)