shrine 3.1.0 → 3.4.0

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