shrine 3.0.1 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +15 -5
  5. data/doc/advantages.md +33 -16
  6. data/doc/attacher.md +2 -2
  7. data/doc/carrierwave.md +78 -34
  8. data/doc/changing_derivatives.md +39 -39
  9. data/doc/design.md +134 -85
  10. data/doc/direct_s3.md +1 -0
  11. data/doc/external/articles.md +57 -45
  12. data/doc/external/extensions.md +41 -35
  13. data/doc/external/misc.md +23 -8
  14. data/doc/getting_started.md +177 -112
  15. data/doc/metadata.md +79 -43
  16. data/doc/multiple_files.md +6 -4
  17. data/doc/paperclip.md +119 -42
  18. data/doc/plugins/activerecord.md +1 -1
  19. data/doc/plugins/add_metadata.md +112 -35
  20. data/doc/plugins/atomic_helpers.md +41 -3
  21. data/doc/plugins/backgrounding.md +12 -2
  22. data/doc/plugins/column.md +36 -7
  23. data/doc/plugins/data_uri.md +2 -2
  24. data/doc/plugins/default_url.md +6 -3
  25. data/doc/plugins/derivation_endpoint.md +26 -28
  26. data/doc/plugins/derivatives.md +238 -171
  27. data/doc/plugins/determine_mime_type.md +2 -2
  28. data/doc/plugins/download_endpoint.md +5 -5
  29. data/doc/plugins/dynamic_storage.md +1 -1
  30. data/doc/plugins/form_assign.md +5 -5
  31. data/doc/plugins/included.md +25 -5
  32. data/doc/plugins/infer_extension.md +11 -2
  33. data/doc/plugins/instrumentation.md +1 -1
  34. data/doc/plugins/metadata_attributes.md +22 -10
  35. data/doc/plugins/mirroring.md +1 -1
  36. data/doc/plugins/persistence.md +11 -1
  37. data/doc/plugins/refresh_metadata.md +5 -4
  38. data/doc/plugins/remote_url.md +8 -3
  39. data/doc/plugins/remove_invalid.md +9 -1
  40. data/doc/plugins/signature.md +11 -2
  41. data/doc/plugins/store_dimensions.md +12 -2
  42. data/doc/plugins/type_predicates.md +96 -0
  43. data/doc/plugins/upload_endpoint.md +7 -11
  44. data/doc/plugins/upload_options.md +1 -1
  45. data/doc/plugins/url_options.md +4 -4
  46. data/doc/plugins/validation.md +14 -4
  47. data/doc/plugins/validation_helpers.md +3 -3
  48. data/doc/plugins/versions.md +7 -7
  49. data/doc/processing.md +290 -127
  50. data/doc/refile.md +39 -18
  51. data/doc/release_notes/2.19.0.md +1 -1
  52. data/doc/release_notes/2.8.0.md +1 -1
  53. data/doc/release_notes/3.0.0.md +1 -1
  54. data/doc/release_notes/3.0.1.md +4 -0
  55. data/doc/release_notes/3.1.0.md +73 -0
  56. data/doc/release_notes/3.2.0.md +96 -0
  57. data/doc/release_notes/3.2.1.md +31 -0
  58. data/doc/release_notes/3.2.2.md +14 -0
  59. data/doc/release_notes/3.3.0.md +105 -0
  60. data/doc/securing_uploads.md +3 -3
  61. data/doc/storage/file_system.md +1 -1
  62. data/doc/storage/memory.md +19 -0
  63. data/doc/storage/s3.md +105 -82
  64. data/doc/testing.md +2 -2
  65. data/doc/upgrading_to_3.md +97 -49
  66. data/doc/validation.md +3 -2
  67. data/lib/shrine.rb +8 -8
  68. data/lib/shrine/attacher.rb +24 -14
  69. data/lib/shrine/attachment.rb +5 -5
  70. data/lib/shrine/plugins.rb +22 -0
  71. data/lib/shrine/plugins/activerecord.rb +1 -1
  72. data/lib/shrine/plugins/add_metadata.rb +18 -7
  73. data/lib/shrine/plugins/backgrounding.rb +2 -2
  74. data/lib/shrine/plugins/default_storage.rb +6 -6
  75. data/lib/shrine/plugins/default_url.rb +1 -1
  76. data/lib/shrine/plugins/derivation_endpoint.rb +12 -7
  77. data/lib/shrine/plugins/derivatives.rb +61 -29
  78. data/lib/shrine/plugins/determine_mime_type.rb +3 -3
  79. data/lib/shrine/plugins/entity.rb +6 -6
  80. data/lib/shrine/plugins/mirroring.rb +8 -8
  81. data/lib/shrine/plugins/model.rb +3 -3
  82. data/lib/shrine/plugins/presign_endpoint.rb +16 -4
  83. data/lib/shrine/plugins/pretty_location.rb +1 -1
  84. data/lib/shrine/plugins/processing.rb +1 -1
  85. data/lib/shrine/plugins/refresh_metadata.rb +2 -2
  86. data/lib/shrine/plugins/remote_url.rb +3 -3
  87. data/lib/shrine/plugins/remove_attachment.rb +5 -0
  88. data/lib/shrine/plugins/remove_invalid.rb +10 -5
  89. data/lib/shrine/plugins/sequel.rb +1 -1
  90. data/lib/shrine/plugins/signature.rb +7 -6
  91. data/lib/shrine/plugins/store_dimensions.rb +22 -11
  92. data/lib/shrine/plugins/type_predicates.rb +113 -0
  93. data/lib/shrine/plugins/upload_endpoint.rb +10 -5
  94. data/lib/shrine/plugins/upload_options.rb +2 -2
  95. data/lib/shrine/plugins/url_options.rb +2 -2
  96. data/lib/shrine/plugins/validation.rb +9 -7
  97. data/lib/shrine/storage/linter.rb +4 -4
  98. data/lib/shrine/storage/memory.rb +5 -3
  99. data/lib/shrine/storage/s3.rb +117 -38
  100. data/lib/shrine/uploaded_file.rb +0 -1
  101. data/lib/shrine/version.rb +2 -2
  102. data/shrine.gemspec +7 -8
  103. metadata +25 -31
@@ -141,7 +141,7 @@ class Shrine
141
141
  require "mimemagic"
142
142
 
143
143
  mime = MimeMagic.by_magic(io)
144
- mime.type if mime
144
+ mime&.type
145
145
  end
146
146
 
147
147
  def extract_with_marcel(io, options)
@@ -158,7 +158,7 @@ class Shrine
158
158
 
159
159
  if filename = extract_filename(io)
160
160
  mime_type = MIME::Types.of(filename).first
161
- mime_type.content_type if mime_type
161
+ mime_type&.content_type
162
162
  end
163
163
  end
164
164
 
@@ -167,7 +167,7 @@ class Shrine
167
167
 
168
168
  if filename = extract_filename(io)
169
169
  info = MiniMime.lookup_by_filename(filename)
170
- info.content_type if info
170
+ info&.content_type
171
171
  end
172
172
  end
173
173
 
@@ -41,27 +41,27 @@ class Shrine
41
41
  end
42
42
 
43
43
  # Returns the URL to the attached file.
44
- define_method :"#{name}_url" do |*args|
45
- send(:"#{name}_attacher").url(*args)
44
+ define_method :"#{name}_url" do |*args, **options|
45
+ send(:"#{name}_attacher").url(*args, **options)
46
46
  end
47
47
 
48
48
  # Returns an attacher instance.
49
49
  define_method :"#{name}_attacher" do |**options|
50
- attachment.send(:attacher, self, options)
50
+ attachment.send(:attacher, self, **options)
51
51
  end
52
52
  end
53
53
 
54
54
  # Returns the class attacher instance with loaded entity. It's not
55
55
  # memoized because the entity object could be frozen.
56
- def attacher(record, options)
57
- attacher = class_attacher(options)
56
+ def attacher(record, **options)
57
+ attacher = class_attacher(**options)
58
58
  attacher.load_entity(record, @name)
59
59
  attacher
60
60
  end
61
61
 
62
62
  # Creates an instance of the corresponding attacher class with set
63
63
  # name.
64
- def class_attacher(options)
64
+ def class_attacher(**options)
65
65
  attacher = shrine_class::Attacher.new(**@options, **options)
66
66
  attacher.instance_variable_set(:@name, @name)
67
67
  attacher
@@ -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
@@ -55,11 +55,11 @@ class Shrine
55
55
  end
56
56
 
57
57
  # Memoizes the attacher instance into an instance variable.
58
- def attacher(record, options)
58
+ def attacher(record, **options)
59
59
  return super unless model?
60
60
 
61
61
  if !record.instance_variable_get(:"@#{@name}_attacher") || options.any?
62
- attacher = class_attacher(options)
62
+ attacher = class_attacher(**options)
63
63
  attacher.load_model(record, @name)
64
64
 
65
65
  record.instance_variable_set(:"@#{@name}_attacher", attacher)
@@ -118,7 +118,7 @@ class Shrine
118
118
  end
119
119
 
120
120
  # Writes uploaded file data into the model.
121
- def set(*args)
121
+ def set(*)
122
122
  result = super
123
123
  write if model?
124
124
  result
@@ -8,7 +8,7 @@ class Shrine
8
8
  module Plugins
9
9
  # Documentation can be found on https://shrinerb.com/docs/plugins/presign_endpoint
10
10
  module PresignEndpoint
11
- def self.configure(uploader, opts = {})
11
+ def self.configure(uploader, **opts)
12
12
  uploader.opts[:presign_endpoint] ||= {}
13
13
  uploader.opts[:presign_endpoint].merge!(opts)
14
14
  end
@@ -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.
@@ -135,7 +147,7 @@ class Shrine
135
147
  if @presign
136
148
  data = @presign.call(location, options, request)
137
149
  else
138
- data = storage.presign(location, options)
150
+ data = storage.presign(location, **options)
139
151
  end
140
152
 
141
153
  { fields: {}, headers: {} }.merge(data.to_h)
@@ -11,7 +11,7 @@ class Shrine
11
11
 
12
12
  module InstanceMethods
13
13
  def generate_location(io, **options)
14
- pretty_location(io, options)
14
+ pretty_location(io, **options)
15
15
  end
16
16
 
17
17
  def pretty_location(io, name: nil, record: nil, version: nil, derivative: nil, identifier: nil, metadata: {}, **)
@@ -33,7 +33,7 @@ class Shrine
33
33
  def process(io, **options)
34
34
  pipeline = processing_pipeline(options[:action])
35
35
  pipeline.inject(io) do |input, processor|
36
- instance_exec(input, options, &processor) || input
36
+ instance_exec(input, **options, &processor) || input
37
37
  end
38
38
  end
39
39
 
@@ -6,8 +6,8 @@ class Shrine
6
6
  module RefreshMetadata
7
7
  module AttacherMethods
8
8
  def refresh_metadata!(**options)
9
- file.refresh_metadata!(**context, **options)
10
- set(file)
9
+ file!.refresh_metadata!(**context, **options)
10
+ set(file) # trigger model write
11
11
  end
12
12
  end
13
13
 
@@ -16,7 +16,7 @@ class Shrine
16
16
  }.inspect}"
17
17
  end
18
18
 
19
- DOWNLOADER = -> (url, options) { Down.download(url, options) }
19
+ DOWNLOADER = -> (url, **options) { Down.download(url, **options) }
20
20
 
21
21
  def self.load_dependencies(uploader, *)
22
22
  uploader.plugin :validation
@@ -63,7 +63,7 @@ class Shrine
63
63
  private
64
64
 
65
65
  def download_remote_url(url, options)
66
- opts[:remote_url][:downloader].call(url, options)
66
+ opts[:remote_url][:downloader].call(url, **options)
67
67
  rescue Down::TooLarge
68
68
  fail DownloadError, "remote file too large"
69
69
  rescue Down::Error
@@ -87,7 +87,7 @@ class Shrine
87
87
  def assign_remote_url(url, downloader: {}, **options)
88
88
  return if url == "" || url.nil?
89
89
 
90
- downloaded_file = shrine_class.remote_url(url, downloader)
90
+ downloaded_file = shrine_class.remote_url(url, **downloader)
91
91
  attach_cached(downloaded_file, **options)
92
92
  rescue DownloadError => error
93
93
  errors.clear << remote_url_error_message(url, error)
@@ -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/
@@ -9,18 +9,23 @@ class Shrine
9
9
  end
10
10
 
11
11
  module AttacherMethods
12
- def change(*)
12
+ def validate(*)
13
13
  super
14
14
  ensure
15
- revert_change if errors.any?
15
+ deassign if errors.any?
16
16
  end
17
17
 
18
18
  private
19
19
 
20
- def revert_change
20
+ def deassign
21
21
  destroy
22
- set @previous.file
23
- remove_instance_variable(:@previous)
22
+
23
+ if changed?
24
+ load_data @previous.data
25
+ @previous = nil
26
+ else
27
+ load_data nil
28
+ end
24
29
  end
25
30
  end
26
31
  end
@@ -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
@@ -21,10 +21,13 @@ class Shrine
21
21
  module ClassMethods
22
22
  # Calculates `algorithm` hash of the contents of the IO object, and
23
23
  # encodes it into `format`.
24
- def calculate_signature(io, algorithm, format: :hex)
25
- instrument_signature(io, algorithm, format) do
26
- SignatureCalculator.new(algorithm.downcase, format: format).call(io)
27
- end
24
+ def calculate_signature(io, algorithm, format: :hex, rewind: true)
25
+ calculator = SignatureCalculator.new(algorithm.downcase, format: format)
26
+
27
+ signature = instrument_signature(io, algorithm, format) { calculator.call(io) }
28
+ io.rewind if rewind
29
+
30
+ signature
28
31
  end
29
32
  alias signature calculate_signature
30
33
 
@@ -62,8 +65,6 @@ class Shrine
62
65
 
63
66
  def call(io)
64
67
  hash = send(:"calculate_#{algorithm}", io)
65
- io.rewind
66
-
67
68
  send(:"encode_#{format}", hash)
68
69
  end
69
70
 
@@ -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)
@@ -113,23 +115,32 @@ class Shrine
113
115
 
114
116
  def extract_with_fastimage(io)
115
117
  require "fastimage"
116
- FastImage.size(io, raise_on_failure: true)
117
- rescue FastImage::FastImageException => error
118
- on_error(error)
118
+
119
+ begin
120
+ FastImage.size(io, raise_on_failure: true)
121
+ rescue FastImage::FastImageException => error
122
+ on_error(error)
123
+ end
119
124
  end
120
125
 
121
126
  def extract_with_mini_magick(io)
122
127
  require "mini_magick"
123
- Shrine.with_file(io) { |file| MiniMagick::Image.new(file.path).dimensions }
124
- rescue MiniMagick::Error => error
125
- on_error(error)
128
+
129
+ begin
130
+ Shrine.with_file(io) { |file| MiniMagick::Image.new(file.path).dimensions }
131
+ rescue MiniMagick::Error => error
132
+ on_error(error)
133
+ end
126
134
  end
127
135
 
128
136
  def extract_with_ruby_vips(io)
129
137
  require "vips"
130
- Shrine.with_file(io) { |file| Vips::Image.new_from_file(file.path).size }
131
- rescue Vips::Error => error
132
- on_error(error)
138
+
139
+ begin
140
+ Shrine.with_file(io) { |file| Vips::Image.new_from_file(file.path).size }
141
+ rescue Vips::Error => error
142
+ on_error(error)
143
+ end
133
144
  end
134
145
 
135
146
  def on_error(error)
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # Documentation can be found on https://shrinerb.com/docs/plugins/type_predicates
6
+ module TypePredicates
7
+ def self.configure(uploader, methods: [], **opts)
8
+ uploader.opts[:type_predicates] ||= { mime: :mini_mime }
9
+ uploader.opts[:type_predicates].merge!(opts)
10
+
11
+ methods.each do |name|
12
+ uploader::UploadedFile.send(:define_method, "#{name}?") { type?(name) }
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def type_lookup(extension, database = nil)
18
+ database ||= opts[:type_predicates][:mime]
19
+ database = MimeDatabase.new(database) if database.is_a?(Symbol)
20
+ database.call(extension.to_s)
21
+ end
22
+ end
23
+
24
+ module FileMethods
25
+ def image?
26
+ general_type?("image")
27
+ end
28
+
29
+ def video?
30
+ general_type?("video")
31
+ end
32
+
33
+ def audio?
34
+ general_type?("audio")
35
+ end
36
+
37
+ def text?
38
+ general_type?("text")
39
+ end
40
+
41
+ def type?(type)
42
+ matching_mime_type = shrine_class.type_lookup(type)
43
+
44
+ fail Error, "type #{type.inspect} is not recognized by the MIME library" unless matching_mime_type
45
+
46
+ mime_type! == matching_mime_type
47
+ end
48
+
49
+ private
50
+
51
+ def general_type?(type)
52
+ mime_type!.start_with?(type)
53
+ end
54
+
55
+ def mime_type!
56
+ mime_type or fail Error, "mime_type metadata value is missing"
57
+ end
58
+ end
59
+
60
+ class MimeDatabase
61
+ SUPPORTED_TOOLS = %i[mini_mime mime_types mimemagic marcel rack_mime]
62
+
63
+ def initialize(tool)
64
+ raise Error, "unknown type database #{tool.inspect}, supported databases are: #{SUPPORTED_TOOLS.join(",")}" unless SUPPORTED_TOOLS.include?(tool)
65
+
66
+ @tool = tool
67
+ end
68
+
69
+ def call(extension)
70
+ send(:"lookup_with_#{@tool}", extension)
71
+ end
72
+
73
+ private
74
+
75
+ def lookup_with_mini_mime(extension)
76
+ require "mini_mime"
77
+
78
+ info = MiniMime.lookup_by_extension(extension)
79
+ info&.content_type
80
+ end
81
+
82
+ def lookup_with_mime_types(extension)
83
+ require "mime/types"
84
+
85
+ mime_type = MIME::Types.of(".#{extension}").first
86
+ mime_type&.content_type
87
+ end
88
+
89
+ def lookup_with_mimemagic(extension)
90
+ require "mimemagic"
91
+
92
+ magic = MimeMagic.by_extension(".#{extension}")
93
+ magic&.type
94
+ end
95
+
96
+ def lookup_with_marcel(extension)
97
+ require "marcel"
98
+
99
+ type = Marcel::MimeType.for(extension: ".#{extension}")
100
+ type unless type == "application/octet-stream"
101
+ end
102
+
103
+ def lookup_with_rack_mime(extension)
104
+ require "rack/mime"
105
+
106
+ Rack::Mime.mime_type(".#{extension}", nil)
107
+ end
108
+ end
109
+ end
110
+
111
+ register_plugin(:type_predicates, TypePredicates)
112
+ end
113
+ end