shrine 3.1.0 → 3.4.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/README.md +11 -4
  4. data/doc/advantages.md +4 -4
  5. data/doc/attacher.md +2 -2
  6. data/doc/carrierwave.md +24 -12
  7. data/doc/changing_derivatives.md +1 -1
  8. data/doc/changing_location.md +6 -5
  9. data/doc/design.md +134 -85
  10. data/doc/direct_s3.md +26 -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 +156 -85
  15. data/doc/metadata.md +80 -44
  16. data/doc/multiple_files.md +1 -1
  17. data/doc/paperclip.md +28 -9
  18. data/doc/plugins/add_metadata.md +112 -35
  19. data/doc/plugins/atomic_helpers.md +41 -3
  20. data/doc/plugins/backgrounding.md +12 -2
  21. data/doc/plugins/column.md +36 -7
  22. data/doc/plugins/default_url.md +6 -3
  23. data/doc/plugins/derivatives.md +83 -44
  24. data/doc/plugins/download_endpoint.md +5 -5
  25. data/doc/plugins/dynamic_storage.md +1 -1
  26. data/doc/plugins/entity.md +12 -4
  27. data/doc/plugins/form_assign.md +5 -5
  28. data/doc/plugins/included.md +25 -5
  29. data/doc/plugins/infer_extension.md +9 -0
  30. data/doc/plugins/instrumentation.md +1 -1
  31. data/doc/plugins/metadata_attributes.md +1 -0
  32. data/doc/plugins/mirroring.md +1 -1
  33. data/doc/plugins/model.md +8 -3
  34. data/doc/plugins/persistence.md +10 -1
  35. data/doc/plugins/remote_url.md +6 -1
  36. data/doc/plugins/remove_invalid.md +9 -1
  37. data/doc/plugins/sequel.md +1 -1
  38. data/doc/plugins/store_dimensions.md +10 -0
  39. data/doc/plugins/type_predicates.md +96 -0
  40. data/doc/plugins/upload_endpoint.md +1 -1
  41. data/doc/plugins/upload_options.md +1 -1
  42. data/doc/plugins/url_options.md +4 -4
  43. data/doc/plugins/validation.md +14 -4
  44. data/doc/plugins/versions.md +7 -7
  45. data/doc/processing.md +287 -123
  46. data/doc/refile.md +9 -9
  47. data/doc/release_notes/2.8.0.md +1 -1
  48. data/doc/release_notes/3.0.0.md +1 -1
  49. data/doc/release_notes/3.2.0.md +96 -0
  50. data/doc/release_notes/3.2.1.md +31 -0
  51. data/doc/release_notes/3.2.2.md +14 -0
  52. data/doc/release_notes/3.3.0.md +105 -0
  53. data/doc/release_notes/3.4.0.md +35 -0
  54. data/doc/securing_uploads.md +2 -2
  55. data/doc/storage/memory.md +19 -0
  56. data/doc/storage/s3.md +104 -77
  57. data/doc/testing.md +12 -2
  58. data/doc/upgrading_to_3.md +99 -53
  59. data/lib/shrine.rb +9 -8
  60. data/lib/shrine/attacher.rb +20 -10
  61. data/lib/shrine/attachment.rb +2 -2
  62. data/lib/shrine/plugins.rb +22 -0
  63. data/lib/shrine/plugins/activerecord.rb +3 -3
  64. data/lib/shrine/plugins/add_metadata.rb +20 -5
  65. data/lib/shrine/plugins/backgrounding.rb +2 -2
  66. data/lib/shrine/plugins/default_url.rb +1 -1
  67. data/lib/shrine/plugins/derivation_endpoint.rb +13 -8
  68. data/lib/shrine/plugins/derivatives.rb +59 -30
  69. data/lib/shrine/plugins/determine_mime_type.rb +5 -3
  70. data/lib/shrine/plugins/entity.rb +12 -11
  71. data/lib/shrine/plugins/instrumentation.rb +12 -18
  72. data/lib/shrine/plugins/mirroring.rb +8 -8
  73. data/lib/shrine/plugins/model.rb +3 -3
  74. data/lib/shrine/plugins/presign_endpoint.rb +16 -4
  75. data/lib/shrine/plugins/pretty_location.rb +1 -1
  76. data/lib/shrine/plugins/processing.rb +1 -1
  77. data/lib/shrine/plugins/refresh_metadata.rb +2 -2
  78. data/lib/shrine/plugins/remote_url.rb +3 -3
  79. data/lib/shrine/plugins/remove_attachment.rb +5 -0
  80. data/lib/shrine/plugins/remove_invalid.rb +10 -5
  81. data/lib/shrine/plugins/sequel.rb +1 -1
  82. data/lib/shrine/plugins/store_dimensions.rb +4 -2
  83. data/lib/shrine/plugins/type_predicates.rb +113 -0
  84. data/lib/shrine/plugins/upload_endpoint.rb +10 -5
  85. data/lib/shrine/plugins/upload_options.rb +2 -2
  86. data/lib/shrine/plugins/url_options.rb +2 -2
  87. data/lib/shrine/plugins/validation.rb +9 -7
  88. data/lib/shrine/storage/linter.rb +4 -4
  89. data/lib/shrine/storage/memory.rb +5 -3
  90. data/lib/shrine/storage/s3.rb +117 -38
  91. data/lib/shrine/version.rb +1 -1
  92. data/shrine.gemspec +8 -8
  93. metadata +42 -34
@@ -69,27 +69,25 @@ class Shrine
69
69
 
70
70
  # Sends a `upload.shrine` event.
71
71
  def _upload(io, location:, metadata:, upload_options: {}, **options)
72
- self.class.instrument(
73
- :upload,
72
+ self.class.instrument(:upload, {
74
73
  storage: storage_key,
75
74
  location: location,
76
75
  io: io,
77
76
  upload_options: upload_options,
78
77
  metadata: metadata,
79
78
  options: options,
80
- ) { super }
79
+ }) { super }
81
80
  end
82
81
 
83
82
  # Sends a `metadata.shrine` event.
84
83
  def get_metadata(io, metadata: nil, **options)
85
84
  return super if io.is_a?(UploadedFile) && metadata != true || metadata == false
86
85
 
87
- self.class.instrument(
88
- :metadata,
86
+ self.class.instrument(:metadata, {
89
87
  storage: storage_key,
90
88
  io: io,
91
89
  options: options,
92
- ) { super }
90
+ }) { super }
93
91
  end
94
92
  end
95
93
 
@@ -98,30 +96,27 @@ class Shrine
98
96
  def stream(destination, **options)
99
97
  return super if opened?
100
98
 
101
- shrine_class.instrument(
102
- :download,
99
+ shrine_class.instrument(:download, {
103
100
  storage: storage_key,
104
101
  location: id,
105
102
  download_options: options,
106
- ) { super(destination, **options, instrument: false) }
103
+ }) { super(destination, **options, instrument: false) }
107
104
  end
108
105
 
109
106
  # Sends a `exists.shrine` event.
110
107
  def exists?
111
- shrine_class.instrument(
112
- :exists,
108
+ shrine_class.instrument(:exists, {
113
109
  storage: storage_key,
114
110
  location: id,
115
- ) { super }
111
+ }) { super }
116
112
  end
117
113
 
118
114
  # Sends a `delete.shrine` event.
119
115
  def delete
120
- shrine_class.instrument(
121
- :delete,
116
+ shrine_class.instrument(:delete, {
122
117
  storage: storage_key,
123
118
  location: id,
124
- ) { super }
119
+ }) { super }
125
120
  end
126
121
 
127
122
  private
@@ -130,12 +125,11 @@ class Shrine
130
125
  def _open(instrument: true, **options)
131
126
  return super(**options) unless instrument
132
127
 
133
- shrine_class.instrument(
134
- :open,
128
+ shrine_class.instrument(:open, {
135
129
  storage: storage_key,
136
130
  location: id,
137
131
  download_options: options,
138
- ) { super(**options) }
132
+ }) { super(**options) }
139
133
  end
140
134
  end
141
135
 
@@ -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 if instance_variable_defined?(:"@#{name}_attacher")
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)
@@ -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