shrine 3.2.2 → 3.3.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/README.md +5 -0
  4. data/doc/advantages.md +1 -1
  5. data/doc/carrierwave.md +7 -4
  6. data/doc/direct_s3.md +1 -0
  7. data/doc/external/articles.md +6 -8
  8. data/doc/external/extensions.md +3 -0
  9. data/doc/external/misc.md +23 -8
  10. data/doc/getting_started.md +4 -8
  11. data/doc/multiple_files.md +1 -1
  12. data/doc/paperclip.md +14 -5
  13. data/doc/plugins/add_metadata.md +20 -0
  14. data/doc/plugins/atomic_helpers.md +41 -3
  15. data/doc/plugins/derivatives.md +43 -12
  16. data/doc/plugins/download_endpoint.md +5 -5
  17. data/doc/plugins/dynamic_storage.md +1 -1
  18. data/doc/plugins/infer_extension.md +9 -0
  19. data/doc/plugins/metadata_attributes.md +1 -0
  20. data/doc/plugins/mirroring.md +1 -1
  21. data/doc/plugins/persistence.md +10 -1
  22. data/doc/plugins/store_dimensions.md +10 -0
  23. data/doc/plugins/url_options.md +2 -2
  24. data/doc/processing.md +7 -8
  25. data/doc/release_notes/2.8.0.md +1 -1
  26. data/doc/release_notes/3.2.1.md +2 -3
  27. data/doc/release_notes/3.3.0.md +105 -0
  28. data/doc/storage/s3.md +9 -5
  29. data/doc/upgrading_to_3.md +11 -27
  30. data/lib/shrine/attacher.rb +6 -1
  31. data/lib/shrine/plugins/activerecord.rb +1 -1
  32. data/lib/shrine/plugins/add_metadata.rb +6 -4
  33. data/lib/shrine/plugins/backgrounding.rb +2 -2
  34. data/lib/shrine/plugins/derivation_endpoint.rb +2 -1
  35. data/lib/shrine/plugins/derivatives.rb +45 -15
  36. data/lib/shrine/plugins/mirroring.rb +8 -8
  37. data/lib/shrine/plugins/presign_endpoint.rb +14 -2
  38. data/lib/shrine/plugins/remove_attachment.rb +5 -0
  39. data/lib/shrine/plugins/sequel.rb +1 -1
  40. data/lib/shrine/plugins/store_dimensions.rb +4 -2
  41. data/lib/shrine/plugins/upload_endpoint.rb +7 -2
  42. data/lib/shrine/storage/memory.rb +5 -3
  43. data/lib/shrine/storage/s3.rb +61 -6
  44. data/lib/shrine/version.rb +2 -2
  45. data/shrine.gemspec +1 -1
  46. metadata +6 -5
@@ -36,8 +36,8 @@ class Shrine
36
36
 
37
37
  module AttacherMethods
38
38
  # Inherits global hooks if defined.
39
- def initialize(*args)
40
- super
39
+ def initialize(**args)
40
+ super(**args)
41
41
  @destroy_block = self.class.destroy_block
42
42
  @promote_block = self.class.promote_block
43
43
  end
@@ -392,6 +392,7 @@ class Shrine
392
392
  options[:type] = request.params["type"] if request.params["type"]
393
393
  options[:disposition] = request.params["disposition"] if request.params["disposition"]
394
394
  options[:filename] = request.params["filename"] if request.params["filename"]
395
+ options[:version] = request.params["version"] if request.params["version"]
395
396
  options[:expires_in] = expires_in(request) if request.params["expires_at"]
396
397
 
397
398
  derivation = uploaded_file.derivation(name, *args, **options)
@@ -738,7 +739,7 @@ class Shrine
738
739
  def verify_signature(string, signature)
739
740
  if signature.nil?
740
741
  fail InvalidSignature, "missing \"signature\" param"
741
- elsif signature != generate_signature(string)
742
+ elsif !Rack::Utils.secure_compare(signature, generate_signature(string))
742
743
  fail InvalidSignature, "provided signature does not match the calculated signature"
743
744
  end
744
745
  end
@@ -4,8 +4,6 @@ class Shrine
4
4
  module Plugins
5
5
  # Documentation can be found on https://shrinerb.com/docs/plugins/derivatives
6
6
  module Derivatives
7
- NOOP_PROCESSOR = -> (*) { Hash.new }
8
-
9
7
  LOG_SUBSCRIBER = -> (event) do
10
8
  Shrine.logger.info "Derivatives (#{event.duration}ms) – #{{
11
9
  processor: event[:processor],
@@ -21,7 +19,7 @@ class Shrine
21
19
  end
22
20
 
23
21
  def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, **opts)
24
- uploader.opts[:derivatives] ||= { processors: {}, storage: proc { store_key } }
22
+ uploader.opts[:derivatives] ||= { processors: {}, processor_settings: {}, storage: proc { store_key } }
25
23
  uploader.opts[:derivatives].merge!(opts)
26
24
 
27
25
  # instrumentation plugin integration
@@ -52,20 +50,37 @@ class Shrine
52
50
  # Attacher.derivatives_processor :thumbnails do |original|
53
51
  # # ...
54
52
  # end
55
- def derivatives_processor(name = :default, &block)
53
+ #
54
+ # By default, Shrine will convert the source IO object into a file
55
+ # before it's passed to the processor block. You can set `download:
56
+ # false` to pass the source IO object to the processor block as is.
57
+ #
58
+ # Attacher.derivatives_processor :thumbnails, download: false do |original|
59
+ # # ...
60
+ # end
61
+ #
62
+ # This can be useful if you'd like to defer or avoid a possibly
63
+ # expensive download operation for processor logic that does not
64
+ # require it.
65
+ def derivatives_processor(name = :default, download: true, &block)
56
66
  if block
57
- shrine_class.opts[:derivatives][:processors][name.to_sym] = block
67
+ shrine_class.derivatives_options[:processors][name.to_sym] = block
68
+ shrine_class.derivatives_options[:processor_settings][name.to_sym] = { download: download }
58
69
  else
59
- processor = shrine_class.opts[:derivatives][:processors][name.to_sym]
60
- processor ||= NOOP_PROCESSOR if name == :default
61
-
62
- fail Error, "derivatives processor #{name.inspect} not registered" unless processor
63
-
64
- processor
70
+ shrine_class.derivatives_options[:processors].fetch(name.to_sym) do
71
+ fail Error, "derivatives processor #{name.inspect} not registered" unless name == :default
72
+ end
65
73
  end
66
74
  end
67
75
  alias derivatives derivatives_processor
68
76
 
77
+ # Returns settings for the given derivatives processor.
78
+ #
79
+ # Attacher.derivatives_processor_settings(:thumbnails) #=> { download: true }
80
+ def derivatives_processor_settings(name)
81
+ shrine_class.derivatives_options[:processor_settings][name.to_sym] || {}
82
+ end
83
+
69
84
  # Specifies default storage to which derivatives will be uploaded.
70
85
  #
71
86
  # Attacher.derivatives_storage :other_store
@@ -79,9 +94,9 @@ class Shrine
79
94
  # end
80
95
  def derivatives_storage(storage_key = nil, &block)
81
96
  if storage_key || block
82
- shrine_class.opts[:derivatives][:storage] = storage_key || block
97
+ shrine_class.derivatives_options[:storage] = storage_key || block
83
98
  else
84
- shrine_class.opts[:derivatives][:storage]
99
+ shrine_class.derivatives_options[:storage]
85
100
  end
86
101
  end
87
102
  end
@@ -147,6 +162,7 @@ class Shrine
147
162
  def promote(**options)
148
163
  super
149
164
  promote_derivatives
165
+ create_derivatives if create_derivatives_on_promote?
150
166
  end
151
167
 
152
168
  # Uploads any cached derivatives to permanent storage.
@@ -268,8 +284,10 @@ class Shrine
268
284
 
269
285
  source ||= file!
270
286
 
271
- if source.is_a?(UploadedFile)
272
- source.download do |file|
287
+ processor_settings = self.class.derivatives_processor_settings(processor_name) || {}
288
+
289
+ if processor_settings[:download]
290
+ shrine_class.with_file(source) do |file|
273
291
  _process_derivatives(processor_name, file, **options)
274
292
  end
275
293
  else
@@ -472,6 +490,8 @@ class Shrine
472
490
  def _process_derivatives(processor_name, source, **options)
473
491
  processor = self.class.derivatives_processor(processor_name)
474
492
 
493
+ return {} unless processor
494
+
475
495
  result = instrument_derivatives(processor_name, source, options) do
476
496
  instance_exec(source, **options, &processor)
477
497
  end
@@ -521,6 +541,11 @@ class Shrine
521
541
  o2
522
542
  end
523
543
  end
544
+
545
+ # Whether to automatically create derivatives on promotion
546
+ def create_derivatives_on_promote?
547
+ shrine_class.derivatives_options[:create_on_promote]
548
+ end
524
549
  end
525
550
 
526
551
  module ClassMethods
@@ -577,6 +602,11 @@ class Shrine
577
602
  yield path, object
578
603
  end
579
604
  end
605
+
606
+ # Returns derivatives plugin options.
607
+ def derivatives_options
608
+ opts[:derivatives]
609
+ end
580
610
  end
581
611
 
582
612
  module FileMethods
@@ -53,7 +53,7 @@ class Shrine
53
53
  # Mirrors upload to other mirror storages.
54
54
  def upload(io, mirror: true, **options)
55
55
  file = super(io, **options)
56
- file.trigger_mirror_upload if mirror
56
+ file.trigger_mirror_upload(**options) if mirror
57
57
  file
58
58
  end
59
59
  end
@@ -61,31 +61,31 @@ class Shrine
61
61
  module FileMethods
62
62
  # Mirrors upload if mirrors are defined. Calls mirror block if
63
63
  # registered, otherwise mirrors synchronously.
64
- def trigger_mirror_upload
64
+ def trigger_mirror_upload(**options)
65
65
  return unless shrine_class.mirrors[storage_key] && shrine_class.mirror_upload?
66
66
 
67
67
  if shrine_class.mirror_upload_block
68
- mirror_upload_background
68
+ mirror_upload_background(**options)
69
69
  else
70
- mirror_upload
70
+ mirror_upload(**options)
71
71
  end
72
72
  end
73
73
 
74
74
  # Calls mirror upload block.
75
- def mirror_upload_background
75
+ def mirror_upload_background(**options)
76
76
  fail Error, "mirror upload block is not registered" unless shrine_class.mirror_upload_block
77
77
 
78
- shrine_class.mirror_upload_block.call(self)
78
+ shrine_class.mirror_upload_block.call(self, **options)
79
79
  end
80
80
 
81
81
  # Uploads the file to each mirror storage.
82
- def mirror_upload
82
+ def mirror_upload(**options)
83
83
  previously_opened = opened?
84
84
 
85
85
  each_mirror do |mirror|
86
86
  rewind if opened?
87
87
 
88
- shrine_class.upload(self, mirror, location: id, close: false, action: :mirror)
88
+ shrine_class.upload(self, mirror, **options, location: id, close: false, action: :mirror)
89
89
  end
90
90
  ensure
91
91
  if opened? && !previously_opened
@@ -81,9 +81,14 @@ class Shrine
81
81
 
82
82
  status, headers, body = catch(:halt) do
83
83
  error!(404, "Not Found") unless ["", "/"].include?(request.path_info)
84
- error!(405, "Method Not Allowed") unless request.get?
85
84
 
86
- handle_request(request)
85
+ if request.get?
86
+ handle_request(request)
87
+ elsif request.options?
88
+ handle_options_request(request)
89
+ else
90
+ error!(405, "Method Not Allowed")
91
+ end
87
92
  end
88
93
 
89
94
  headers["Content-Length"] ||= body.map(&:bytesize).inject(0, :+).to_s
@@ -109,6 +114,13 @@ class Shrine
109
114
  make_response(presign, request)
110
115
  end
111
116
 
117
+ # Uppy client sends an OPTIONS request to fetch information about the
118
+ # Uppy Companion. Since our Rack app is only acting as Uppy Companion, we
119
+ # just return a successful response.
120
+ def handle_options_request(request)
121
+ [200, {}, []]
122
+ end
123
+
112
124
  # Generates the location using `Shrine#generate_uid`, and extracts the
113
125
  # extension from the `filename` query parameter. If `:presign_location`
114
126
  # option is given, calls that instead.
@@ -32,6 +32,11 @@ class Shrine
32
32
 
33
33
  private
34
34
 
35
+ # Don't override previously removed attachment that wasn't yet deleted.
36
+ def change?(file)
37
+ super && !(changed? && remove?)
38
+ end
39
+
35
40
  # Rails sends "0" or "false" if the checkbox hasn't been ticked.
36
41
  def remove?
37
42
  remove && remove != "" && remove !~ /\A(0|false)\z/
@@ -62,7 +62,7 @@ class Shrine
62
62
  # reload the attacher on record reload
63
63
  define_method :_refresh do |*args|
64
64
  result = super(*args)
65
- instance_variable_set(:"@#{name}_attacher", nil)
65
+ send(:"#{name}_attacher").reload
66
66
  result
67
67
  end
68
68
  private :_refresh
@@ -12,7 +12,7 @@ class Shrine
12
12
  end
13
13
 
14
14
  def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, **opts)
15
- uploader.opts[:store_dimensions] ||= { analyzer: :fastimage, on_error: :warn }
15
+ uploader.opts[:store_dimensions] ||= { analyzer: :fastimage, on_error: :warn, auto_extraction: true }
16
16
  uploader.opts[:store_dimensions].merge!(opts)
17
17
 
18
18
  # resolve error strategy
@@ -71,8 +71,10 @@ class Shrine
71
71
  end
72
72
 
73
73
  module InstanceMethods
74
- # We update the metadata with "width" and "height".
75
74
  def extract_metadata(io, **options)
75
+ return super unless opts[:store_dimensions][:auto_extraction]
76
+
77
+ # We update the metadata with "width" and "height".
76
78
  width, height = self.class.extract_dimensions(io)
77
79
 
78
80
  super.merge!("width" => width, "height" => height)
@@ -135,9 +135,14 @@ class Shrine
135
135
  end
136
136
 
137
137
  error!(400, "Upload Not Found") if value.nil?
138
- error!(400, "Upload Not Valid") unless value.is_a?(Hash) && value[:tempfile]
139
138
 
140
- @shrine_class.rack_file(value)
139
+ if value.is_a?(Hash) && value[:tempfile]
140
+ @shrine_class.rack_file(value)
141
+ elsif %i[read rewind eof? close].all? { |m| value.respond_to?(m) }
142
+ value
143
+ else
144
+ error!(400, "Upload Not Valid")
145
+ end
141
146
  end
142
147
 
143
148
  # Returns a hash of information containing `:action` and `:request`
@@ -12,12 +12,14 @@ class Shrine
12
12
  @store = store
13
13
  end
14
14
 
15
- def upload(io, id, *)
15
+ def upload(io, id, **)
16
16
  store[id] = io.read
17
17
  end
18
18
 
19
- def open(id, *)
20
- StringIO.new(store.fetch(id))
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
21
23
  rescue KeyError
22
24
  raise Shrine::FileNotFound, "file #{id.inspect} not found on storage"
23
25
  end
@@ -9,6 +9,7 @@ require "down/chunked_io"
9
9
  require "content_disposition"
10
10
 
11
11
  require "uri"
12
+ require "tempfile"
12
13
 
13
14
  class Shrine
14
15
  module Storage
@@ -103,7 +104,7 @@ class Shrine
103
104
  #
104
105
  # [`Aws::S3::Object#get`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#get-instance_method
105
106
  def open(id, rewindable: true, **options)
106
- chunks, length = get(object(id), options)
107
+ chunks, length = get(id, **options)
107
108
 
108
109
  Down::ChunkedIO.new(chunks: chunks, rewindable: rewindable, size: length)
109
110
  rescue Aws::S3::Errors::NoSuchKey
@@ -212,7 +213,7 @@ class Shrine
212
213
 
213
214
  # Returns an `Aws::S3::Object` for the given id.
214
215
  def object(id)
215
- bucket.object([*prefix, id].join("/"))
216
+ bucket.object(object_key(id))
216
217
  end
217
218
 
218
219
  private
@@ -279,9 +280,12 @@ class Shrine
279
280
  end
280
281
  end
281
282
 
283
+ # Aws::S3::Object#get doesn't allow us to get the content length of the
284
+ # object before all content is downloaded, so we hack our way around it.
285
+ # This way get the content length without an additional HEAD request.
282
286
  if Gem::Version.new(Aws::CORE_GEM_VERSION) >= Gem::Version.new("3.104.0")
283
- def get(object, params)
284
- enum = object.enum_for(:get, **params)
287
+ def get(id, **params)
288
+ enum = object(id).enum_for(:get, **params)
285
289
 
286
290
  begin
287
291
  content_length = Integer(enum.peek.last["content-length"])
@@ -294,8 +298,8 @@ class Shrine
294
298
  [chunks, content_length]
295
299
  end
296
300
  else
297
- def get(object, params)
298
- req = client.build_request(:get_object, bucket: bucket.name, key: object.key, **params)
301
+ def get(id, **params)
302
+ req = client.build_request(:get_object, bucket: bucket.name, key: object_key(id), **params)
299
303
 
300
304
  body = req.enum_for(:send_request)
301
305
  begin
@@ -326,6 +330,57 @@ class Shrine
326
330
  bucket.delete_objects(delete: delete_params)
327
331
  end
328
332
  end
333
+
334
+ # Returns object key with potential prefix.
335
+ def object_key(id)
336
+ [*prefix, id].join("/")
337
+ end
338
+
339
+ # Adds support for Aws::S3::Encryption::Client.
340
+ module ClientSideEncryption
341
+ attr_reader :encryption_client
342
+
343
+ # Save the encryption client and continue initialization with normal
344
+ # client.
345
+ def initialize(client: nil, **options)
346
+ return super unless client.class.name.start_with?("Aws::S3::Encryption")
347
+
348
+ super(client: client.client, **options)
349
+ @encryption_client = client
350
+ end
351
+
352
+ private
353
+
354
+ # Encryption client doesn't support multipart uploads, so we always use
355
+ # #put_object.
356
+ def put(io, id, **options)
357
+ return super unless encryption_client
358
+
359
+ encryption_client.put_object(body: io, bucket: bucket.name, key: object_key(id), **options)
360
+ end
361
+
362
+ def get(id, **options)
363
+ return super unless encryption_client
364
+
365
+ # Encryption client v2 warns against streaming download, so we first
366
+ # download all content into a file.
367
+ tempfile = Tempfile.new("shrine-s3", binmode: true)
368
+ response = encryption_client.get_object(response_target: tempfile, bucket: bucket.name, key: object_key(id), **options)
369
+ tempfile.rewind
370
+
371
+ chunks = Enumerator.new do |yielder|
372
+ begin
373
+ yielder << tempfile.read(16*1024) until tempfile.eof?
374
+ ensure
375
+ tempfile.close!
376
+ end
377
+ end
378
+
379
+ [chunks, tempfile.size]
380
+ end
381
+ end
382
+
383
+ prepend ClientSideEncryption
329
384
  end
330
385
  end
331
386
  end
@@ -7,8 +7,8 @@ class Shrine
7
7
 
8
8
  module VERSION
9
9
  MAJOR = 3
10
- MINOR = 2
11
- TINY = 2
10
+ MINOR = 3
11
+ TINY = 0
12
12
  PRE = nil
13
13
 
14
14
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
@@ -61,7 +61,7 @@ direct uploads for fully asynchronous user experience.
61
61
  gem.add_development_dependency "ruby-vips", "~> 2.0" unless ENV["CI"]
62
62
 
63
63
  # for S3 storage
64
- gem.add_development_dependency "aws-sdk-s3", "~> 1.16"
64
+ gem.add_development_dependency "aws-sdk-s3", "~> 1.69"
65
65
  gem.add_development_dependency "aws-sdk-core", "~> 3.23"
66
66
 
67
67
  # for instrumentation plugin
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shrine
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.2
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-05 00:00:00.000000000 Z
11
+ date: 2020-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: down
@@ -240,14 +240,14 @@ dependencies:
240
240
  requirements:
241
241
  - - "~>"
242
242
  - !ruby/object:Gem::Version
243
- version: '1.16'
243
+ version: '1.69'
244
244
  type: :development
245
245
  prerelease: false
246
246
  version_requirements: !ruby/object:Gem::Requirement
247
247
  requirements:
248
248
  - - "~>"
249
249
  - !ruby/object:Gem::Version
250
- version: '1.16'
250
+ version: '1.69'
251
251
  - !ruby/object:Gem::Dependency
252
252
  name: aws-sdk-core
253
253
  requirement: !ruby/object:Gem::Requirement
@@ -458,6 +458,7 @@ files:
458
458
  - doc/release_notes/3.2.0.md
459
459
  - doc/release_notes/3.2.1.md
460
460
  - doc/release_notes/3.2.2.md
461
+ - doc/release_notes/3.3.0.md
461
462
  - doc/retrieving_uploads.md
462
463
  - doc/securing_uploads.md
463
464
  - doc/storage/file_system.md
@@ -552,7 +553,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
552
553
  - !ruby/object:Gem::Version
553
554
  version: '0'
554
555
  requirements: []
555
- rubygems_version: 3.1.1
556
+ rubygems_version: 3.1.4
556
557
  signing_key:
557
558
  specification_version: 4
558
559
  summary: Toolkit for file attachments in Ruby applications