shrine 2.19.4 → 3.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of shrine might be problematic. Click here for more details.

Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +299 -11
  3. data/README.md +9 -3
  4. data/doc/advantages.md +1 -1
  5. data/doc/carrierwave.md +4 -4
  6. data/doc/creating_persistence_plugins.md +172 -0
  7. data/doc/creating_plugins.md +1 -1
  8. data/doc/creating_storages.md +3 -1
  9. data/doc/design.md +2 -2
  10. data/doc/direct_s3.md +0 -22
  11. data/doc/paperclip.md +3 -3
  12. data/doc/plugins/activerecord.md +211 -42
  13. data/doc/plugins/atomic_helpers.md +153 -0
  14. data/doc/plugins/column.md +90 -0
  15. data/doc/plugins/derivation_endpoint.md +54 -62
  16. data/doc/plugins/derivatives.md +752 -0
  17. data/doc/plugins/entity.md +204 -0
  18. data/doc/plugins/infer_extension.md +8 -8
  19. data/doc/plugins/instrumentation.md +33 -13
  20. data/doc/plugins/keep_files.md +5 -15
  21. data/doc/plugins/model.md +157 -0
  22. data/doc/plugins/presign_endpoint.md +2 -1
  23. data/doc/plugins/refresh_metadata.md +44 -7
  24. data/doc/plugins/sequel.md +190 -33
  25. data/doc/plugins/{default_url_options.md → url_options.md} +5 -5
  26. data/doc/processing.md +1 -1
  27. data/doc/release_notes/1.1.0.md +2 -2
  28. data/doc/release_notes/2.15.0.md +1 -1
  29. data/doc/storage/s3.md +2 -2
  30. data/doc/testing.md +1 -1
  31. data/lib/shrine.rb +72 -138
  32. data/lib/shrine/attacher.rb +272 -176
  33. data/lib/shrine/attachment.rb +2 -42
  34. data/lib/shrine/plugins/activerecord.rb +103 -26
  35. data/lib/shrine/plugins/add_metadata.rb +9 -10
  36. data/lib/shrine/plugins/atomic_helpers.rb +111 -0
  37. data/lib/shrine/plugins/attacher_options.rb +55 -0
  38. data/lib/shrine/plugins/backgrounding.rb +147 -115
  39. data/lib/shrine/plugins/cached_attachment_data.rb +6 -9
  40. data/lib/shrine/plugins/column.rb +104 -0
  41. data/lib/shrine/plugins/data_uri.rb +35 -38
  42. data/lib/shrine/plugins/default_storage.rb +18 -12
  43. data/lib/shrine/plugins/default_url.rb +11 -21
  44. data/lib/shrine/plugins/default_url_options.rb +3 -30
  45. data/lib/shrine/plugins/delete_raw.rb +9 -13
  46. data/lib/shrine/plugins/derivation_endpoint.rb +75 -114
  47. data/lib/shrine/plugins/derivatives.rb +576 -0
  48. data/lib/shrine/plugins/determine_mime_type.rb +3 -15
  49. data/lib/shrine/plugins/download_endpoint.rb +83 -131
  50. data/lib/shrine/plugins/dynamic_storage.rb +4 -8
  51. data/lib/shrine/plugins/entity.rb +128 -0
  52. data/lib/shrine/plugins/form_assign.rb +107 -0
  53. data/lib/shrine/plugins/included.rb +4 -3
  54. data/lib/shrine/plugins/infer_extension.rb +10 -17
  55. data/lib/shrine/plugins/instrumentation.rb +45 -25
  56. data/lib/shrine/plugins/keep_files.rb +2 -12
  57. data/lib/shrine/plugins/metadata_attributes.rb +15 -14
  58. data/lib/shrine/plugins/model.rb +137 -0
  59. data/lib/shrine/plugins/module_include.rb +2 -0
  60. data/lib/shrine/plugins/presign_endpoint.rb +1 -15
  61. data/lib/shrine/plugins/pretty_location.rb +5 -5
  62. data/lib/shrine/plugins/processing.rb +21 -6
  63. data/lib/shrine/plugins/rack_file.rb +1 -39
  64. data/lib/shrine/plugins/rack_response.rb +14 -7
  65. data/lib/shrine/plugins/recache.rb +5 -2
  66. data/lib/shrine/plugins/refresh_metadata.rb +12 -8
  67. data/lib/shrine/plugins/remote_url.rb +44 -53
  68. data/lib/shrine/plugins/remove_attachment.rb +7 -2
  69. data/lib/shrine/plugins/remove_invalid.rb +8 -4
  70. data/lib/shrine/plugins/restore_cached_data.rb +12 -4
  71. data/lib/shrine/plugins/sequel.rb +115 -27
  72. data/lib/shrine/plugins/signature.rb +2 -7
  73. data/lib/shrine/plugins/store_dimensions.rb +13 -27
  74. data/lib/shrine/plugins/upload_endpoint.rb +14 -15
  75. data/lib/shrine/plugins/upload_options.rb +9 -8
  76. data/lib/shrine/plugins/url_options.rb +33 -0
  77. data/lib/shrine/plugins/validation.rb +87 -0
  78. data/lib/shrine/plugins/validation_helpers.rb +33 -54
  79. data/lib/shrine/plugins/versions.rb +106 -84
  80. data/lib/shrine/storage/file_system.rb +32 -57
  81. data/lib/shrine/storage/linter.rb +9 -1
  82. data/lib/shrine/storage/memory.rb +42 -0
  83. data/lib/shrine/storage/s3.rb +38 -146
  84. data/lib/shrine/uploaded_file.rb +22 -29
  85. data/lib/shrine/version.rb +4 -4
  86. data/shrine.gemspec +2 -3
  87. metadata +27 -54
  88. data/doc/plugins/backup.md +0 -31
  89. data/doc/plugins/copy.md +0 -24
  90. data/doc/plugins/delete_promoted.md +0 -12
  91. data/doc/plugins/direct_upload.md +0 -172
  92. data/doc/plugins/hooks.md +0 -58
  93. data/doc/plugins/logging.md +0 -42
  94. data/doc/plugins/migration_helpers.md +0 -60
  95. data/doc/plugins/moving.md +0 -19
  96. data/doc/plugins/multi_delete.md +0 -20
  97. data/doc/plugins/parallelize.md +0 -16
  98. data/doc/plugins/parsed_json.md +0 -23
  99. data/lib/shrine/plugins/background_helpers.rb +0 -5
  100. data/lib/shrine/plugins/backup.rb +0 -90
  101. data/lib/shrine/plugins/copy.rb +0 -50
  102. data/lib/shrine/plugins/delete_promoted.rb +0 -20
  103. data/lib/shrine/plugins/direct_upload.rb +0 -217
  104. data/lib/shrine/plugins/hooks.rb +0 -90
  105. data/lib/shrine/plugins/logging.rb +0 -142
  106. data/lib/shrine/plugins/migration_helpers.rb +0 -70
  107. data/lib/shrine/plugins/moving.rb +0 -57
  108. data/lib/shrine/plugins/multi_delete.rb +0 -32
  109. data/lib/shrine/plugins/parallelize.rb +0 -78
  110. data/lib/shrine/plugins/parsed_json.rb +0 -29
@@ -13,19 +13,12 @@ class Shrine
13
13
  }.inspect}"
14
14
  end
15
15
 
16
- def self.configure(uploader, opts = {})
17
- if opts[:analyzer] == :default
18
- Shrine.deprecation("The :default analyzer of the determine_mime_type plugin has been renamed to :content_type. The :default alias will not be supported in Shrine 3.")
19
- opts = opts.merge(analyzer: :content_type)
20
- end
21
-
22
- uploader.opts[:determine_mime_type] ||= { analyzer: :file, analyzer_options: {}, log_subscriber: LOG_SUBSCRIBER }
16
+ def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, **opts)
17
+ uploader.opts[:determine_mime_type] ||= { analyzer: :file, analyzer_options: {} }
23
18
  uploader.opts[:determine_mime_type].merge!(opts)
24
19
 
25
20
  # instrumentation plugin integration
26
- if uploader.respond_to?(:subscribe)
27
- uploader.subscribe(:mime_type, &uploader.opts[:determine_mime_type][:log_subscriber])
28
- end
21
+ uploader.subscribe(:mime_type, &log_subscriber) if uploader.respond_to?(:subscribe)
29
22
  end
30
23
 
31
24
  module ClassMethods
@@ -79,11 +72,6 @@ class Shrine
79
72
  def extract_mime_type(io)
80
73
  self.class.determine_mime_type(io)
81
74
  end
82
-
83
- # Returns a hash of built-in MIME type analyzers.
84
- def mime_type_analyzers
85
- self.class.mime_type_analyzers
86
- end
87
75
  end
88
76
 
89
77
  class MimeTypeAnalyzer
@@ -6,44 +6,20 @@ class Shrine
6
6
  #
7
7
  # [doc/plugins/download_endpoint.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/download_endpoint.md
8
8
  module DownloadEndpoint
9
- def self.load_dependencies(uploader, opts = {})
9
+ def self.load_dependencies(uploader, **)
10
10
  uploader.plugin :rack_response
11
11
  uploader.plugin :_urlsafe_serialization
12
12
  end
13
13
 
14
- def self.configure(uploader, opts = {})
14
+ def self.configure(uploader, **opts)
15
15
  uploader.opts[:download_endpoint] ||= { disposition: "inline", download_options: {} }
16
16
  uploader.opts[:download_endpoint].merge!(opts)
17
-
18
- Shrine.deprecation("The :storages download_endpoint option is deprecated, you should use UploadedFile#download_url for generating URLs to the download endpoint.") if uploader.opts[:download_endpoint][:storages]
19
-
20
- uploader.assign_download_endpoint(App) unless uploader.const_defined?(:DownloadEndpoint)
21
17
  end
22
18
 
23
19
  module ClassMethods
24
- # Assigns the subclass a copy of the download endpoint class.
25
- def inherited(subclass)
26
- super
27
- subclass.assign_download_endpoint(@download_endpoint)
28
- end
29
-
30
20
  # Returns the Rack application that retrieves requested files.
31
21
  def download_endpoint(**options)
32
- new_download_endpoint(App, **options)
33
- end
34
-
35
- # Assigns the subclassed endpoint as the `DownloadEndpoint` constant.
36
- def assign_download_endpoint(app_class)
37
- @download_endpoint = new_download_endpoint(app_class)
38
-
39
- const_set(:DownloadEndpoint, @download_endpoint)
40
- deprecate_constant(:DownloadEndpoint)
41
- end
42
-
43
- private
44
-
45
- def new_download_endpoint(app_class, **options)
46
- app_class.new(
22
+ Shrine::DownloadEndpoint.new(
47
23
  shrine_class: self,
48
24
  **opts[:download_endpoint],
49
25
  **options,
@@ -52,28 +28,10 @@ class Shrine
52
28
  end
53
29
 
54
30
  module FileMethods
55
- # Constructs the URL from the optional host, prefix, storage key and
56
- # uploaded file's id. For other uploaded files that aren't in the list
57
- # of storages it just returns their original URL.
58
- def url(**options)
59
- if download_storages && download_storages.include?(storage_key.to_sym)
60
- Shrine.deprecation("The :storages option for download_endpoint plugin is deprecated and will be obsolete in Shrine 3. Use UploadedFile#download_url instead.")
61
- download_url
62
- else
63
- super
64
- end
65
- end
66
-
67
31
  # Returns file URL on the download endpoint.
68
32
  def download_url(**options)
69
33
  FileUrl.new(self).call(**options)
70
34
  end
71
-
72
- private
73
-
74
- def download_storages
75
- shrine_class.opts[:download_endpoint][:storages]
76
- end
77
35
  end
78
36
 
79
37
  class FileUrl
@@ -105,116 +63,110 @@ class Shrine
105
63
  file.shrine_class.opts[:download_endpoint]
106
64
  end
107
65
  end
66
+ end
108
67
 
109
- # Routes incoming requests. It first asserts that the storage is existent
110
- # and allowed. Afterwards it proceeds with the file download using
111
- # streaming.
112
- class App
113
- # Writes given options to instance variables.
114
- def initialize(options)
115
- options.each do |name, value|
116
- instance_variable_set("@#{name}", value)
117
- end
118
- end
68
+ register_plugin(:download_endpoint, DownloadEndpoint)
69
+ end
119
70
 
120
- def call(env)
121
- request = Rack::Request.new(env)
122
71
 
123
- status, headers, body = catch(:halt) do
124
- error!(405, "Method Not Allowed") unless request.get?
72
+ # Routes incoming requests. It first asserts that the storage is existent
73
+ # and allowed. Afterwards it proceeds with the file download using
74
+ # streaming.
75
+ class DownloadEndpoint
76
+ # Writes given options to instance variables.
77
+ def initialize(options)
78
+ options.each do |name, value|
79
+ instance_variable_set("@#{name}", value)
80
+ end
81
+ end
125
82
 
126
- handle_request(request)
127
- end
83
+ def call(env)
84
+ request = Rack::Request.new(env)
128
85
 
129
- headers["Content-Length"] ||= body.map(&:bytesize).inject(0, :+).to_s
86
+ status, headers, body = catch(:halt) do
87
+ error!(405, "Method Not Allowed") unless request.get?
130
88
 
131
- [status, headers, body]
132
- end
89
+ handle_request(request)
90
+ end
133
91
 
134
- def inspect
135
- "#<#{@shrine_class}::DownloadEndpoint>"
136
- end
137
- alias to_s inspect
92
+ headers["Content-Length"] ||= body.map(&:bytesize).inject(0, :+).to_s
138
93
 
139
- private
94
+ [status, headers, body]
95
+ end
140
96
 
141
- def handle_request(request)
142
- _, *components = request.path_info.split("/")
97
+ def inspect
98
+ "#<#{@shrine_class}::DownloadEndpoint>"
99
+ end
100
+ alias to_s inspect
143
101
 
144
- if components.count == 1
145
- uploaded_file = get_uploaded_file(components.first)
146
- elsif components.count == 2
147
- # handle legacy "/:storage/:id" URLs
148
- uploaded_file = @shrine_class::UploadedFile.new(
149
- "storage" => components.first,
150
- "id" => components.last,
151
- )
152
- end
102
+ private
153
103
 
154
- serve_file(uploaded_file, request)
155
- end
104
+ def handle_request(request)
105
+ _, serialized, * = request.path_info.split("/")
156
106
 
157
- # Streams or redirects to the uploaded file.
158
- def serve_file(uploaded_file, request)
159
- if @redirect
160
- redirect_to_file(uploaded_file, request)
161
- else
162
- stream_file(uploaded_file, request)
163
- end
164
- end
107
+ uploaded_file = get_uploaded_file(serialized)
165
108
 
166
- # Streams the uploaded file content.
167
- def stream_file(uploaded_file, request)
168
- open_file(uploaded_file, request)
109
+ serve_file(uploaded_file, request)
110
+ end
169
111
 
170
- response = uploaded_file.to_rack_response(
171
- disposition: @disposition,
172
- range: request.env["HTTP_RANGE"],
173
- )
112
+ # Streams or redirects to the uploaded file.
113
+ def serve_file(uploaded_file, request)
114
+ if @redirect
115
+ redirect_to_file(uploaded_file, request)
116
+ else
117
+ stream_file(uploaded_file, request)
118
+ end
119
+ end
174
120
 
175
- response[1]["Cache-Control"] = "max-age=#{365*24*60*60}" # cache for a year
121
+ # Streams the uploaded file content.
122
+ def stream_file(uploaded_file, request)
123
+ open_file(uploaded_file, request)
176
124
 
177
- response
178
- end
125
+ response = uploaded_file.to_rack_response(
126
+ disposition: @disposition,
127
+ range: request.env["HTTP_RANGE"],
128
+ )
179
129
 
180
- # Redirects to the uploaded file's direct URL or the specified URL proc.
181
- def redirect_to_file(uploaded_file, request)
182
- if @redirect == true
183
- redirect_url = uploaded_file.url
184
- else
185
- redirect_url = @redirect.call(uploaded_file, request)
186
- end
130
+ response[1]["Cache-Control"] = "max-age=#{365*24*60*60}" # cache for a year
187
131
 
188
- [302, { "Location" => redirect_url }, []]
189
- end
132
+ response
133
+ end
190
134
 
191
- def open_file(uploaded_file, request)
192
- download_options = @download_options
193
- download_options = download_options.call(uploaded_file, request) if download_options.respond_to?(:call)
135
+ # Redirects to the uploaded file's direct URL or the specified URL proc.
136
+ def redirect_to_file(uploaded_file, request)
137
+ if @redirect == true
138
+ redirect_url = uploaded_file.url
139
+ else
140
+ redirect_url = @redirect.call(uploaded_file, request)
141
+ end
194
142
 
195
- uploaded_file.open(**download_options)
196
- end
143
+ [302, { "Location" => redirect_url }, []]
144
+ end
197
145
 
198
- # Returns a Shrine::UploadedFile, or returns 404 if file doesn't exist.
199
- def get_uploaded_file(serialized)
200
- uploaded_file = @shrine_class::UploadedFile.urlsafe_load(serialized)
201
- not_found! unless uploaded_file.exists?
202
- uploaded_file
203
- rescue Shrine::Error # storage not found
204
- not_found!
205
- end
146
+ def open_file(uploaded_file, request)
147
+ download_options = @download_options
148
+ download_options = download_options.call(uploaded_file, request) if download_options.respond_to?(:call)
206
149
 
207
- def not_found!
208
- error!(404, "File Not Found")
209
- end
150
+ uploaded_file.open(**download_options)
151
+ rescue Shrine::FileNotFound
152
+ not_found!
153
+ end
210
154
 
211
- # Halts the request with the error message.
212
- def error!(status, message)
213
- throw :halt, [status, { "Content-Type" => "text/plain" }, [message]]
214
- end
215
- end
155
+ # Deserializes a Shrine::UploadedFile from a URL component. Returns 404 if
156
+ # storage is not found.
157
+ def get_uploaded_file(serialized)
158
+ @shrine_class::UploadedFile.urlsafe_load(serialized)
159
+ rescue Shrine::Error # storage not found
160
+ not_found!
216
161
  end
217
162
 
218
- register_plugin(:download_endpoint, DownloadEndpoint)
163
+ def not_found!
164
+ error!(404, "File Not Found")
165
+ end
166
+
167
+ # Halts the request with the error message.
168
+ def error!(status, message)
169
+ throw :halt, [status, { "Content-Type" => "text/plain" }, [message]]
170
+ end
219
171
  end
220
172
  end
@@ -6,17 +6,13 @@ class Shrine
6
6
  #
7
7
  # [doc/plugins/dynamic_storage.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/dynamic_storage.md
8
8
  module DynamicStorage
9
- def self.configure(uploader, options = {})
10
- uploader.opts[:dynamic_storages] ||= {}
9
+ def self.configure(uploader)
10
+ uploader.opts[:dynamic_storage] ||= { resolvers: {} }
11
11
  end
12
12
 
13
13
  module ClassMethods
14
- def dynamic_storages
15
- opts[:dynamic_storages]
16
- end
17
-
18
14
  def storage(regex, &block)
19
- dynamic_storages[regex] = block
15
+ opts[:dynamic_storage][:resolvers][regex] = block
20
16
  end
21
17
 
22
18
  def find_storage(name)
@@ -26,7 +22,7 @@ class Shrine
26
22
  private
27
23
 
28
24
  def resolve_dynamic_storage(name)
29
- dynamic_storages.each do |regex, block|
25
+ opts[:dynamic_storage][:resolvers].each do |regex, block|
30
26
  if match = name.to_s.match(regex)
31
27
  return block.call(match)
32
28
  end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # Documentation lives in [doc/plugins/entity.md] on GitHub.
6
+ #
7
+ # [doc/plugins/entity.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/entity.md
8
+ module Entity
9
+ def self.load_dependencies(uploader, **)
10
+ uploader.plugin :column
11
+ end
12
+
13
+ module AttachmentMethods
14
+ # Defines `#<name>`, `#<name>_url`, and `#<name>_attacher` methods.
15
+ def initialize(name, **options)
16
+ super
17
+
18
+ attachment = self
19
+
20
+ # Returns the attached file.
21
+ define_method :"#{name}" do |*args|
22
+ send(:"#{name}_attacher").get(*args)
23
+ end
24
+
25
+ # Returns the URL to the attached file.
26
+ define_method :"#{name}_url" do |*args|
27
+ send(:"#{name}_attacher").url(*args)
28
+ end
29
+
30
+ # Returns an attacher instance.
31
+ define_method :"#{name}_attacher" do |**options|
32
+ attachment.attacher(self, options)
33
+ end
34
+ end
35
+
36
+ # Creates an instance of the corresponding Attacher subclass. It's not
37
+ # memoized because the entity object could be frozen.
38
+ def attacher(record, options)
39
+ shrine_class::Attacher.from_entity(record, @name, @options.merge(options))
40
+ end
41
+ end
42
+
43
+ module AttacherClassMethods
44
+ # Initializes itself from an entity instance and attachment name.
45
+ #
46
+ # photo.image_data #=> "{...}" # a file is attached
47
+ #
48
+ # attacher = Attacher.from_entity(photo, :image)
49
+ # attacher.file #=> #<Shrine::UploadedFile>
50
+ def from_entity(record, name, type: :entity, **options)
51
+ attacher = new(**options)
52
+ attacher.load_entity(record, name, type: type)
53
+ attacher
54
+ end
55
+ end
56
+
57
+ module AttacherMethods
58
+ attr_reader :record, :name
59
+
60
+ # Saves record and name and initializes attachment from the entity
61
+ # attribute. Called from `Attacher.from_entity`.
62
+ def load_entity(record, name, type: :entity)
63
+ @record = record
64
+ @name = name.to_sym
65
+ @type = type
66
+
67
+ @context.merge!(record: record, name: name)
68
+
69
+ read
70
+ end
71
+
72
+ # Overwrites the current attachment with the one from model attribute.
73
+ #
74
+ # photo.image_data #=> nil
75
+ # attacher = Shrine::Attacher.from_entity(photo, :image)
76
+ # photo.image_data = uploaded_file.to_json
77
+ #
78
+ # attacher.file #=> nil
79
+ # attacher.reload
80
+ # attacher.file #=> #<Shrine::UploadedFile>
81
+ def reload
82
+ read
83
+ self
84
+ end
85
+
86
+ # Returns a hash with entity attribute name and column data.
87
+ #
88
+ # attacher.column_values
89
+ # #=> { image_data: '{"id":"...","storage":"...","metadata":{...}}' }
90
+ def column_values
91
+ { attribute => column_data }
92
+ end
93
+
94
+ # Returns the entity attribute name used for reading and writing
95
+ # attachment data.
96
+ #
97
+ # attacher = Shrine::Attacher.from_entity(photo, :image)
98
+ # attacher.attribute #=> :image_data
99
+ def attribute
100
+ fail Shrine::Error, "record is not loaded" if name.nil?
101
+
102
+ :"#{name}_data"
103
+ end
104
+
105
+ private
106
+
107
+ # Loads attachment from the entity attribute.
108
+ def read
109
+ load_column(read_attribute)
110
+ end
111
+
112
+ # Reads value from the entity attribute.
113
+ def read_attribute
114
+ record.public_send(attribute)
115
+ end
116
+
117
+ # Returns whether the attacher has been loaded from an entity instance.
118
+ def entity?
119
+ type == :entity
120
+ end
121
+
122
+ attr_reader :type
123
+ end
124
+ end
125
+
126
+ register_plugin(:entity, Entity)
127
+ end
128
+ end