shrine 3.2.2 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
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