shrine 2.13.0 → 2.14.0

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -0
  3. data/README.md +20 -16
  4. data/doc/creating_storages.md +0 -21
  5. data/doc/design.md +1 -0
  6. data/doc/direct_s3.md +26 -15
  7. data/doc/metadata.md +67 -22
  8. data/doc/multiple_files.md +3 -3
  9. data/doc/processing.md +1 -1
  10. data/doc/retrieving_uploads.md +184 -0
  11. data/lib/shrine.rb +268 -900
  12. data/lib/shrine/attacher.rb +271 -0
  13. data/lib/shrine/attachment.rb +97 -0
  14. data/lib/shrine/plugins.rb +29 -0
  15. data/lib/shrine/plugins/_urlsafe_serialization.rb +182 -0
  16. data/lib/shrine/plugins/activerecord.rb +16 -14
  17. data/lib/shrine/plugins/add_metadata.rb +58 -24
  18. data/lib/shrine/plugins/backgrounding.rb +6 -1
  19. data/lib/shrine/plugins/cached_attachment_data.rb +9 -9
  20. data/lib/shrine/plugins/copy.rb +12 -8
  21. data/lib/shrine/plugins/data_uri.rb +23 -20
  22. data/lib/shrine/plugins/default_url_options.rb +5 -4
  23. data/lib/shrine/plugins/determine_mime_type.rb +24 -23
  24. data/lib/shrine/plugins/download_endpoint.rb +61 -73
  25. data/lib/shrine/plugins/migration_helpers.rb +17 -17
  26. data/lib/shrine/plugins/module_include.rb +9 -8
  27. data/lib/shrine/plugins/presign_endpoint.rb +13 -7
  28. data/lib/shrine/plugins/processing.rb +1 -1
  29. data/lib/shrine/plugins/rack_response.rb +128 -36
  30. data/lib/shrine/plugins/refresh_metadata.rb +20 -5
  31. data/lib/shrine/plugins/remote_url.rb +8 -8
  32. data/lib/shrine/plugins/remove_attachment.rb +9 -9
  33. data/lib/shrine/plugins/sequel.rb +21 -18
  34. data/lib/shrine/plugins/tempfile.rb +68 -0
  35. data/lib/shrine/plugins/upload_endpoint.rb +3 -2
  36. data/lib/shrine/plugins/upload_options.rb +7 -6
  37. data/lib/shrine/plugins/validation_helpers.rb +2 -1
  38. data/lib/shrine/storage/file_system.rb +20 -17
  39. data/lib/shrine/storage/linter.rb +0 -7
  40. data/lib/shrine/storage/s3.rb +159 -50
  41. data/lib/shrine/uploaded_file.rb +258 -0
  42. data/lib/shrine/version.rb +1 -1
  43. data/shrine.gemspec +7 -19
  44. metadata +41 -21
@@ -36,9 +36,9 @@ attributes:
36
36
  Album.create(
37
37
  title: "My Album",
38
38
  photos_attributes: [
39
- { image: File.open("image1.jpg", "rb") },
40
- { image: File.open("image2.jpg", "rb") },
41
- { image: File.open("image3.jpg", "rb") },
39
+ { image: File.open("image1.jpg", binmode: true) },
40
+ { image: File.open("image2.jpg", binmode: true) },
41
+ { image: File.open("image3.jpg", binmode: true) },
42
42
  ]
43
43
  )
44
44
  ```
@@ -71,7 +71,7 @@ class ImageUploader < Shrine
71
71
 
72
72
  original.close!
73
73
 
74
- File.open(optimized_path, "rb")
74
+ File.open(optimized_path, binmode: true)
75
75
  end
76
76
  end
77
77
  ```
@@ -0,0 +1,184 @@
1
+ # Retrieving Uploads
2
+
3
+ Uploaded file content is typically retrieved from the storage using a
4
+ `Shrine::UploadedFile` object. This guide explains the various methods of
5
+ retrieving file content and how do they work.
6
+
7
+ For context, `Shrine::UploadedFile` object is what is returned by the
8
+ attachment reader method on the model instance (e.g. `photo.image`),
9
+ `Shrine::Attacher#get` if you're using the attacher directly, or
10
+ `Shrine#upload` if you're using the uploader directly.
11
+
12
+ ## IO-like interface
13
+
14
+ In order for `Shrine::UploadedFile` objects to be uploadable to a storage, they
15
+ too conform to Shrine's IO-like interface, meaning they implement `#read`,
16
+ `#rewind`, `#eof?`, and `#close` matching the behaviour of the same methods on
17
+ Ruby's IO class.
18
+
19
+ ```rb
20
+ uploaded_file.eof? # => false
21
+ uploaded_file.read # => "..."
22
+ uploaded_file.eof? # => true
23
+ uploaded_file.rewind # rewinds the underlying IO object
24
+ uploaded_file.eof? # => false
25
+ uploaded_file.close # closes the underlying IO object (this should be called when you're done)
26
+ ```
27
+
28
+ In reality these methods are simply delegated on the IO object returned by the
29
+ `Storage#open` method of the underlying Shrine storage. For
30
+ `Shrine::Storage::FileSystem` this IO object will be a `File` object, while for
31
+ `Shrine::Storage::S3` (and most other remote storages) it will be a
32
+ [`Down::ChunkedIO`] object. `Storage#open` is implicitly called when any of
33
+ these IO methods are called for the first time.
34
+
35
+ ```rb
36
+ uploaded_file.read(10) # calls `Storage#open` and assigns result to an instance variable
37
+ uploaded_file.read(10)
38
+ # ...
39
+ ```
40
+
41
+ You can retrieve the underlying IO object returned by `Storage#open` with
42
+ `#to_io`:
43
+
44
+ ```rb
45
+ uploaded_file.to_io # the underlying IO object returned by `Storage#open`
46
+ ```
47
+
48
+ ## Opening
49
+
50
+ The `Shrine::UploadedFile#open` method can be used to open the uploaded file
51
+ explicitly:
52
+
53
+ ```rb
54
+ uploaded_file.open # calls `Storage#open` and assigns result to an instance variable
55
+ uploaded_file.read
56
+ uploaded_file.close
57
+ ```
58
+
59
+ This is useful if you want to control where `Storage#open` will be called. It's
60
+ also useful if you want to pass additional parameters to `Storage#open`, which
61
+ will depend on the storage. For example, if you're using S3 storage and
62
+ server-side encryption, you can pass the necessary server-side-encryption
63
+ parameters to `Shrine::Storage::S3#open`:
64
+
65
+ ```rb
66
+ # server-side encryption parameters for S3 storage
67
+ uploaded_file.open(
68
+ sse_customer_algorithm: "AES256",
69
+ sse_customer_key: "secret_key",
70
+ sse_customer_key_md5: "secret_key_md5",
71
+ )
72
+ ```
73
+
74
+ `Shrine::UploadedFile#open` also accepts a block, which will ensure that the
75
+ underlying IO object is closed at the end of the block.
76
+
77
+ ```rb
78
+ uploaded_file.open do
79
+ uploaded_file.read(1000)
80
+ # ...
81
+ end # underlying IO object is closed
82
+ ```
83
+
84
+ `Shrine::UploadedFile#open` will return the result of a given block.
85
+ block. We can use that to safely retrieve the whole content of a file, without
86
+ leaving any temporary files lying around.
87
+
88
+ ```rb
89
+ content = uploaded_file.open(&:read) # open, read, and close
90
+ content # uploaded file content
91
+ ```
92
+
93
+ ## Streaming
94
+
95
+ The `Shrine::UploadedFile#stream` method can be used to stream uploaded file
96
+ content to a writable destination object.
97
+
98
+ ```rb
99
+ destination = StringIO.new # from the "stringio" standard library
100
+ uploaded_file.stream(destination)
101
+ destination.rewind
102
+
103
+ destination # holds the file content
104
+ ```
105
+
106
+ The destination object can be any object that responds to `#write` and returns
107
+ number of bytes written, or a path string.
108
+
109
+ `Shrine::UploadedFile#stream` will play nicely with
110
+ `Shrine::UploadedFile#open`, meaning it will not re-open the uploaded file if
111
+ it's already opened.
112
+
113
+ ```rb
114
+ uploaded_file.open do
115
+ uploaded_file.stream(destination)
116
+ end
117
+ ```
118
+
119
+ Any additional parameters to `Shrine::UploadeFile#stream` are forwarded to
120
+ `Storage#open`. For example, if you're using S3 storage, you can tell AWS S3 to
121
+ use HTTP compression for the download request:
122
+
123
+ ```rb
124
+ uploaded_file.stream(destination, response_content_encoding: "gzip")
125
+ ```
126
+
127
+ If you want to stream uploaded file content to the response body in a Rack
128
+ application (Rails, Sinatra, Roda etc), see the `rack_response` plugin.
129
+
130
+ ## Downloading
131
+
132
+ The `Shrine::UploadedFile#download` method can be used to download uploaded
133
+ file content do disk. Internally a temporary file will be created (using the
134
+ `tempfile` standard library) and passed to `Shrine::UploadedFile#stream`. The
135
+ return value is an open `Tempfile` object (a delegate of the `File` class).
136
+
137
+ ```rb
138
+ tempfile = uploaded_file.download
139
+ tempfile #=> #<Tempfile:...>
140
+
141
+ tempfile.path #=> "/var/folders/k7/6zx6dx6x7ys3rv3srh0nyfj00000gn/T/20181227-2915-m2l6c1"
142
+ tempfile.read #=> "..."
143
+ tempfile.close! # close and unlink
144
+ ```
145
+
146
+ Like `Shrine::UploadedFile#open`, `Shrine::UploadedFile#download` accepts a
147
+ block as well. The `Tempfile` object is yielded to the block, and after the
148
+ block finishes it's automatically closed and deleted.
149
+
150
+ ```rb
151
+ uploaded_file.download do |tempfile|
152
+ tempfile.path #=> "/var/folders/k7/6zx6dx6x7ys3rv3srh0nyfj00000gn/T/20181227-2915-m2l6c1"
153
+ tempfile.read #=> "..."
154
+ # ...
155
+ end # tempfile is closed and deleted
156
+ ```
157
+
158
+ Since `Shrine::UploadedFile#download` internally uses
159
+ `Shrine::UploadedFile#stream`, it plays nicely with `Shrine::UploadedFile#open`
160
+ as well, meaning it will only open the uploaded file if it's not already
161
+ opened.
162
+
163
+ ```rb
164
+ uploaded_file.open do
165
+ tempfile = uploaded_file.download
166
+ # ...
167
+ end
168
+ ```
169
+
170
+ Any options passed to `Shrine::UploadedFile#download` are forwarded to
171
+ `Storage#open` (unless the uploaded file was already opened, in which case
172
+ `Storage#open` was already called). For example, if you're using S3 storage,
173
+ you can tell AWS S3 to use HTTP compression for the download request:
174
+
175
+ ```rb
176
+ uploaded_file.download(response_content_encoding: "gzip")
177
+ ```
178
+
179
+ Every time `Shrine::UploadedFile#download` is called, it will make a new copy
180
+ of the uploaded file content. If you plan to retrieve uploaded file content
181
+ multiple times for the same `Shrine::UploadedFile` instance, consider using the
182
+ `tempfile` plugin.
183
+
184
+ [`Down::ChunkedIO`]: https://github.com/janko-m/down#streaming
@@ -1,11 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "shrine/version"
4
+ require "shrine/uploaded_file"
5
+ require "shrine/attacher"
6
+ require "shrine/attachment"
7
+ require "shrine/plugins"
4
8
 
5
9
  require "securerandom"
6
10
  require "json"
7
11
  require "tempfile"
8
12
 
13
+ # Core class that represents uploader.
14
+ # Base implementation is defined in InstanceMethods and ClassMethods.
9
15
  class Shrine
10
16
  # A generic exception used by Shrine.
11
17
  class Error < StandardError; end
@@ -27,960 +33,322 @@ class Shrine
27
33
  size: [],
28
34
  close: [],
29
35
  }
30
- deprecate_constant(:IO_METHODS) if RUBY_VERSION > "2.3"
31
-
32
- # Core class that represents a file uploaded to a storage. The instance
33
- # methods for this class are added by Shrine::Plugins::Base::FileMethods, the
34
- # class methods are added by Shrine::Plugins::Base::FileClassMethods.
35
- class UploadedFile
36
- @shrine_class = ::Shrine
37
- end
38
-
39
- # Core class which creates attachment modules for specified attribute names
40
- # that are included into model classes. The instance methods for this class
41
- # are added by Shrine::Plugins::Base::AttachmentMethods, the class methods
42
- # are added by Shrine::Plugins::Base::AttachmentClassMethods.
43
- class Attachment < Module
44
- @shrine_class = ::Shrine
45
- end
46
-
47
- # Core class which handles attaching files to model instances. The instance
48
- # methods for this class are added by Shrine::Plugins::Base::AttacherMethods,
49
- # the class methods are added by Shrine::Plugins::Base::AttacherClassMethods.
50
- class Attacher
51
- @shrine_class = ::Shrine
52
- end
36
+ deprecate_constant(:IO_METHODS)
53
37
 
54
38
  @opts = {}
55
39
  @storages = {}
56
40
 
57
- # Module in which all Shrine plugins should be stored. Also contains logic
58
- # for registering and loading plugins.
59
- module Plugins
60
- @plugins = {}
61
-
62
- # If the registered plugin already exists, use it. Otherwise, require it
63
- # and return it. This raises a LoadError if such a plugin doesn't exist,
64
- # or a Shrine::Error if it exists but it does not register itself
65
- # correctly.
66
- def self.load_plugin(name)
67
- unless plugin = @plugins[name]
68
- require "shrine/plugins/#{name}"
69
- raise Error, "plugin #{name} did not register itself correctly in Shrine::Plugins" unless plugin = @plugins[name]
70
- end
71
- plugin
72
- end
73
-
74
- # Register the given plugin with Shrine, so that it can be loaded using
75
- # `Shrine.plugin` with a symbol. Should be used by plugin files. Example:
76
- #
77
- # Shrine::Plugins.register_plugin(:plugin_name, PluginModule)
78
- def self.register_plugin(name, mod)
79
- @plugins[name] = mod
80
- end
81
-
82
- # The base plugin for Shrine, implementing all default functionality.
83
- # Methods are put into a plugin so future plugins can easily override
84
- # them and call `super` to get the default behavior.
85
- module Base
86
- module ClassMethods
87
- # Generic options for this class, plugins store their options here.
88
- attr_reader :opts
89
-
90
- # A hash of storages with their symbol identifiers.
91
- attr_accessor :storages
92
-
93
- # When inheriting Shrine, copy the instance variables into the subclass,
94
- # and create subclasses of core classes.
95
- def inherited(subclass)
96
- subclass.instance_variable_set(:@opts, opts.dup)
97
- subclass.opts.each do |key, value|
98
- if value.is_a?(Enumerable) && !value.frozen?
99
- subclass.opts[key] = value.dup
100
- end
101
- end
102
- subclass.instance_variable_set(:@storages, storages.dup)
103
-
104
- file_class = Class.new(self::UploadedFile)
105
- file_class.shrine_class = subclass
106
- subclass.const_set(:UploadedFile, file_class)
107
-
108
- attachment_class = Class.new(self::Attachment)
109
- attachment_class.shrine_class = subclass
110
- subclass.const_set(:Attachment, attachment_class)
111
-
112
- attacher_class = Class.new(self::Attacher)
113
- attacher_class.shrine_class = subclass
114
- subclass.const_set(:Attacher, attacher_class)
115
- end
116
-
117
- # Load a new plugin into the current class. A plugin can be a module
118
- # which is used directly, or a symbol representing a registered plugin
119
- # which will be required and then loaded.
120
- #
121
- # Shrine.plugin MyPlugin
122
- # Shrine.plugin :my_plugin
123
- def plugin(plugin, *args, &block)
124
- plugin = Plugins.load_plugin(plugin) if plugin.is_a?(Symbol)
125
- plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies)
126
- self.include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods)
127
- self.extend(plugin::ClassMethods) if defined?(plugin::ClassMethods)
128
- self::UploadedFile.include(plugin::FileMethods) if defined?(plugin::FileMethods)
129
- self::UploadedFile.extend(plugin::FileClassMethods) if defined?(plugin::FileClassMethods)
130
- self::Attachment.include(plugin::AttachmentMethods) if defined?(plugin::AttachmentMethods)
131
- self::Attachment.extend(plugin::AttachmentClassMethods) if defined?(plugin::AttachmentClassMethods)
132
- self::Attacher.include(plugin::AttacherMethods) if defined?(plugin::AttacherMethods)
133
- self::Attacher.extend(plugin::AttacherClassMethods) if defined?(plugin::AttacherClassMethods)
134
- plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
135
- plugin
136
- end
137
-
138
- # Retrieves the storage under the given identifier (can be a Symbol or
139
- # a String), and raises Shrine::Error if the storage is missing.
140
- def find_storage(name)
141
- storages.each { |key, value| return value if key.to_s == name.to_s }
142
- raise Error, "storage #{name.inspect} isn't registered on #{self}"
143
- end
144
-
145
- # Generates an instance of Shrine::Attachment to be included in the
146
- # model class. Example:
147
- #
148
- # class Photo
149
- # include Shrine.attachment(:image) # creates a Shrine::Attachment object
150
- # end
151
- def attachment(name, *args)
152
- self::Attachment.new(name, *args)
153
- end
154
- alias [] attachment
155
-
156
- # Instantiates a Shrine::UploadedFile from a hash, and optionally
157
- # yields the returned object.
158
- #
159
- # data = {"storage" => "cache", "id" => "abc123.jpg", "metadata" => {}}
160
- # Shrine.uploaded_file(data) #=> #<Shrine::UploadedFile>
161
- def uploaded_file(object, &block)
162
- case object
163
- when String
164
- uploaded_file(JSON.parse(object), &block)
165
- when Hash
166
- uploaded_file(self::UploadedFile.new(object), &block)
167
- when self::UploadedFile
168
- object.tap { |f| yield(f) if block_given? }
169
- else
170
- raise Error, "cannot convert #{object.inspect} to a #{self}::UploadedFile"
171
- end
172
- end
173
-
174
- # Temporarily converts an IO-like object into a file. If the input IO
175
- # object is already a file, it simply yields it to the block, otherwise
176
- # it copies IO content into a Tempfile object which is then yielded and
177
- # afterwards deleted.
178
- #
179
- # Shrine.with_file(io) { |file| file.path }
180
- def with_file(io)
181
- if io.respond_to?(:path)
182
- yield io
183
- elsif io.is_a?(UploadedFile)
184
- io.download { |tempfile| yield tempfile }
185
- else
186
- Tempfile.create("shrine-file", binmode: true) do |file|
187
- IO.copy_stream(io, file.path)
188
- io.rewind
189
-
190
- yield file
191
- end
192
- end
193
- end
194
-
195
- # Prints a deprecation warning to standard error.
196
- def deprecation(message)
197
- warn "SHRINE DEPRECATION WARNING: #{message}"
198
- end
199
- end
200
-
201
- module InstanceMethods
202
- # The symbol identifier for the storage used by the uploader.
203
- attr_reader :storage_key
204
-
205
- # The storage object used by the uploader.
206
- attr_reader :storage
207
-
208
- # Accepts a storage symbol registered in `Shrine.storages`.
209
- def initialize(storage_key)
210
- @storage = self.class.find_storage(storage_key)
211
- @storage_key = storage_key.to_sym
212
- end
213
-
214
- # The class-level options hash. This should probably not be modified at
215
- # the instance level.
216
- def opts
217
- self.class.opts
218
- end
219
-
220
- # The main method for uploading files. Takes an IO-like object and an
221
- # optional context hash (used internally by Shrine::Attacher). It calls
222
- # user-defined #process, and afterwards it calls #store. The `io` is
223
- # closed after upload.
224
- def upload(io, context = {})
225
- io = processed(io, context) || io
226
- store(io, context)
227
- end
228
-
229
- # User is expected to perform processing inside this method, and
230
- # return the processed files. Returning nil signals that no proccessing
231
- # has been done and that the original file should be used.
232
- #
233
- # class ImageUploader < Shrine
234
- # def process(io, context)
235
- # # do processing and return processed files
236
- # end
237
- # end
238
- def process(io, context = {})
239
- end
240
-
241
- # Uploads the file and returns an instance of Shrine::UploadedFile. By
242
- # default the location of the file is automatically generated by
243
- # \#generate_location, but you can pass in `:location` to upload to
244
- # a specific location.
245
- #
246
- # uploader.store(io)
247
- def store(io, context = {})
248
- _store(io, context)
249
- end
250
-
251
- # Returns true if the storage of the given uploaded file matches the
252
- # storage of this uploader.
253
- def uploaded?(uploaded_file)
254
- uploaded_file.storage_key == storage_key.to_s
255
- end
256
-
257
- # Deletes the given uploaded file and returns it.
258
- def delete(uploaded_file, context = {})
259
- _delete(uploaded_file, context)
260
- uploaded_file
261
- end
262
-
263
- # Generates a unique location for the uploaded file, preserving the
264
- # file extension. Can be overriden in uploaders for generating custom
265
- # location.
266
- def generate_location(io, context = {})
267
- extension = ".#{io.extension}" if io.is_a?(UploadedFile) && io.extension
268
- extension ||= File.extname(extract_filename(io).to_s).downcase
269
- basename = generate_uid(io)
270
-
271
- basename + extension
272
- end
273
-
274
- # Extracts filename, size and MIME type from the file, which is later
275
- # accessible through UploadedFile#metadata.
276
- def extract_metadata(io, context = {})
277
- {
278
- "filename" => extract_filename(io),
279
- "size" => extract_size(io),
280
- "mime_type" => extract_mime_type(io),
281
- }
282
- end
283
-
284
- private
285
-
286
- # Attempts to extract the appropriate filename from the IO object.
287
- def extract_filename(io)
288
- if io.respond_to?(:original_filename)
289
- io.original_filename
290
- elsif io.respond_to?(:path)
291
- File.basename(io.path)
292
- end
293
- end
294
-
295
- # Attempts to extract the MIME type from the IO object.
296
- def extract_mime_type(io)
297
- if io.respond_to?(:content_type)
298
- warn "The \"mime_type\" Shrine metadata field will be set from the \"Content-Type\" request header, which might not hold the actual MIME type of the file. It is recommended to load the determine_mime_type plugin which determines MIME type from file content."
299
- io.content_type
300
- end
301
- end
302
-
303
- # Extracts the filesize from the IO object.
304
- def extract_size(io)
305
- io.size if io.respond_to?(:size)
306
- end
307
-
308
- # It first asserts that `io` is a valid IO object. It then extracts
309
- # metadata and generates the location, before calling the storage to
310
- # upload the IO object, passing the extracted metadata and location.
311
- # Finally it returns a Shrine::UploadedFile object which represents the
312
- # file that was uploaded.
313
- def _store(io, context)
314
- _enforce_io(io)
315
-
316
- metadata = get_metadata(io, context)
317
- metadata = metadata.merge(context[:metadata]) if context[:metadata]
318
-
319
- location = get_location(io, context.merge(metadata: metadata))
41
+ module ClassMethods
42
+ # Generic options for this class, plugins store their options here.
43
+ attr_reader :opts
320
44
 
321
- put(io, context.merge(location: location, metadata: metadata))
45
+ # A hash of storages with their symbol identifiers.
46
+ attr_accessor :storages
322
47
 
323
- self.class.uploaded_file(
324
- "id" => location,
325
- "storage" => storage_key.to_s,
326
- "metadata" => metadata,
327
- )
328
- end
329
-
330
- # Delegates to #remove.
331
- def _delete(uploaded_file, context)
332
- remove(uploaded_file, context)
333
- end
334
-
335
- # Delegates to #copy.
336
- def put(io, context)
337
- copy(io, context)
338
- end
339
-
340
- # Calls `#upload` on the storage, passing to it the location, metadata
341
- # and any upload options. The storage might modify the location or
342
- # metadata that were passed in. The uploaded IO is then closed.
343
- def copy(io, context)
344
- location = context[:location]
345
- metadata = context[:metadata]
346
- upload_options = context[:upload_options] || {}
347
-
348
- storage.upload(io, location, shrine_metadata: metadata, **upload_options)
349
- ensure
350
- io.close rescue nil
351
- end
352
-
353
- # Delegates to `UploadedFile#delete`.
354
- def remove(uploaded_file, context)
355
- uploaded_file.delete
356
- end
357
-
358
- # Delegates to #process.
359
- def processed(io, context)
360
- process(io, context)
361
- end
362
-
363
- # Retrieves the location for the given IO and context. First it looks
364
- # for the `:location` option, otherwise it calls #generate_location.
365
- def get_location(io, context)
366
- location = context[:location] || generate_location(io, context)
367
- location or raise Error, "location generated for #{io.inspect} was nil (context = #{context})"
368
- end
369
-
370
- # If the IO object is a Shrine::UploadedFile, it simply copies over its
371
- # metadata, otherwise it calls #extract_metadata.
372
- def get_metadata(io, context)
373
- if io.is_a?(UploadedFile)
374
- io.metadata.dup
375
- else
376
- extract_metadata(io, context)
377
- end
378
- end
379
-
380
- # Asserts that the object is a valid IO object, specifically that it
381
- # responds to `#read`, `#eof?`, `#rewind`, `#size` and `#close`. If the
382
- # object doesn't respond to one of these methods, a Shrine::InvalidFile
383
- # error is raised.
384
- def _enforce_io(io)
385
- missing_methods = %i[read eof? rewind close].select { |m| !io.respond_to?(m) }
386
- raise InvalidFile.new(io, missing_methods) if missing_methods.any?
387
- end
388
-
389
- # Generates a unique identifier that can be used for a location.
390
- def generate_uid(io)
391
- SecureRandom.hex
48
+ # When inheriting Shrine, copy the instance variables into the subclass,
49
+ # and create subclasses of core classes.
50
+ def inherited(subclass)
51
+ subclass.instance_variable_set(:@opts, opts.dup)
52
+ subclass.opts.each do |key, value|
53
+ if value.is_a?(Enumerable) && !value.frozen?
54
+ subclass.opts[key] = value.dup
392
55
  end
393
56
  end
57
+ subclass.instance_variable_set(:@storages, storages.dup)
394
58
 
395
- module AttachmentClassMethods
396
- # Returns the Shrine class that this attachment class is
397
- # namespaced under.
398
- attr_accessor :shrine_class
59
+ file_class = Class.new(self::UploadedFile)
60
+ file_class.shrine_class = subclass
61
+ subclass.const_set(:UploadedFile, file_class)
399
62
 
400
- # Since Attachment is anonymously subclassed when Shrine is subclassed,
401
- # and then assigned to a constant of the Shrine subclass, make inspect
402
- # reflect the likely name for the class.
403
- def inspect
404
- "#{shrine_class.inspect}::Attachment"
405
- end
406
- end
407
-
408
- module AttachmentMethods
409
- # Instantiates an attachment module for a given attribute name, which
410
- # can then be included to a model class. Second argument will be passed
411
- # to an attacher module.
412
- def initialize(name, **options)
413
- @name = name
414
- @options = options
415
-
416
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
417
- def #{name}_attacher(options = {})
418
- @#{name}_attacher = nil if options.any?
419
- @#{name}_attacher ||= (
420
- attachments = self.class.ancestors.grep(Shrine::Attachment)
421
- attachment = attachments.find { |mod| mod.attachment_name == :#{name} }
422
- attacher_class = attachment.shrine_class::Attacher
423
- options = attachment.options.merge(options)
424
-
425
- attacher_class.new(self, :#{name}, options)
426
- )
427
- end
428
-
429
- def #{name}=(value)
430
- #{name}_attacher.assign(value)
431
- end
432
-
433
- def #{name}
434
- #{name}_attacher.get
435
- end
436
-
437
- def #{name}_url(*args)
438
- #{name}_attacher.url(*args)
439
- end
440
- RUBY
441
- end
63
+ attachment_class = Class.new(self::Attachment)
64
+ attachment_class.shrine_class = subclass
65
+ subclass.const_set(:Attachment, attachment_class)
442
66
 
443
- # Returns name of the attachment this module provides.
444
- def attachment_name
445
- @name
446
- end
67
+ attacher_class = Class.new(self::Attacher)
68
+ attacher_class.shrine_class = subclass
69
+ subclass.const_set(:Attacher, attacher_class)
70
+ end
447
71
 
448
- # Returns options that are to be passed to the Attacher.
449
- def options
450
- @options
451
- end
72
+ # Load a new plugin into the current class. A plugin can be a module
73
+ # which is used directly, or a symbol representing a registered plugin
74
+ # which will be required and then loaded.
75
+ #
76
+ # Shrine.plugin MyPlugin
77
+ # Shrine.plugin :my_plugin
78
+ def plugin(plugin, *args, &block)
79
+ plugin = Plugins.load_plugin(plugin) if plugin.is_a?(Symbol)
80
+ plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies)
81
+ self.include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods)
82
+ self.extend(plugin::ClassMethods) if defined?(plugin::ClassMethods)
83
+ self::UploadedFile.include(plugin::FileMethods) if defined?(plugin::FileMethods)
84
+ self::UploadedFile.extend(plugin::FileClassMethods) if defined?(plugin::FileClassMethods)
85
+ self::Attachment.include(plugin::AttachmentMethods) if defined?(plugin::AttachmentMethods)
86
+ self::Attachment.extend(plugin::AttachmentClassMethods) if defined?(plugin::AttachmentClassMethods)
87
+ self::Attacher.include(plugin::AttacherMethods) if defined?(plugin::AttacherMethods)
88
+ self::Attacher.extend(plugin::AttacherClassMethods) if defined?(plugin::AttacherClassMethods)
89
+ plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
90
+ plugin
91
+ end
452
92
 
453
- # Returns class name with attachment name included.
454
- #
455
- # Shrine[:image].to_s #=> "#<Shrine::Attachment(image)>"
456
- def to_s
457
- "#<#{self.class.inspect}(#{attachment_name})>"
458
- end
93
+ # Retrieves the storage under the given identifier (can be a Symbol or
94
+ # a String), and raises Shrine::Error if the storage is missing.
95
+ def find_storage(name)
96
+ storages.each { |key, value| return value if key.to_s == name.to_s }
97
+ raise Error, "storage #{name.inspect} isn't registered on #{self}"
98
+ end
459
99
 
460
- # Returns class name with attachment name included.
461
- #
462
- # Shrine[:image].inspect #=> "#<Shrine::Attachment(image)>"
463
- def inspect
464
- "#<#{self.class.inspect}(#{attachment_name})>"
465
- end
100
+ # Generates an instance of Shrine::Attachment to be included in the
101
+ # model class. Example:
102
+ #
103
+ # class Photo
104
+ # include Shrine.attachment(:image) # creates a Shrine::Attachment object
105
+ # end
106
+ def attachment(name, *args)
107
+ self::Attachment.new(name, *args)
108
+ end
109
+ alias [] attachment
466
110
 
467
- # Returns the Shrine class that this attachment's class is namespaced
468
- # under.
469
- def shrine_class
470
- self.class.shrine_class
471
- end
111
+ # Instantiates a Shrine::UploadedFile from a hash, and optionally
112
+ # yields the returned object.
113
+ #
114
+ # data = {"storage" => "cache", "id" => "abc123.jpg", "metadata" => {}}
115
+ # Shrine.uploaded_file(data) #=> #<Shrine::UploadedFile>
116
+ def uploaded_file(object, &block)
117
+ case object
118
+ when String
119
+ uploaded_file(JSON.parse(object), &block)
120
+ when Hash
121
+ uploaded_file(self::UploadedFile.new(object), &block)
122
+ when self::UploadedFile
123
+ object.tap { |f| yield(f) if block_given? }
124
+ else
125
+ raise Error, "cannot convert #{object.inspect} to a #{self}::UploadedFile"
472
126
  end
127
+ end
473
128
 
474
- module AttacherClassMethods
475
- # Returns the Shrine class that this attacher class is namespaced
476
- # under.
477
- attr_accessor :shrine_class
478
-
479
- # Since Attacher is anonymously subclassed when Shrine is subclassed,
480
- # and then assigned to a constant of the Shrine subclass, make inspect
481
- # reflect the likely name for the class.
482
- def inspect
483
- "#{shrine_class.inspect}::Attacher"
484
- end
129
+ # Temporarily converts an IO-like object into a file. If the input IO
130
+ # object is already a file, it simply yields it to the block, otherwise
131
+ # it copies IO content into a Tempfile object which is then yielded and
132
+ # afterwards deleted.
133
+ #
134
+ # Shrine.with_file(io) { |file| file.path }
135
+ def with_file(io)
136
+ if io.respond_to?(:path)
137
+ yield io
138
+ elsif io.is_a?(UploadedFile)
139
+ io.download { |tempfile| yield tempfile }
140
+ else
141
+ Tempfile.create("shrine-file", binmode: true) do |file|
142
+ IO.copy_stream(io, file.path)
143
+ io.rewind
485
144
 
486
- # Block that is executed in context of Shrine::Attacher during
487
- # validation. Example:
488
- #
489
- # Shrine::Attacher.validate do
490
- # if get.size > 5*1024*1024
491
- # errors << "is too big (max is 5 MB)"
492
- # end
493
- # end
494
- def validate(&block)
495
- define_method(:validate_block, &block)
496
- private :validate_block
145
+ yield file
497
146
  end
498
147
  end
148
+ end
499
149
 
500
- module AttacherMethods
501
- # Returns the uploader that is used for the temporary storage.
502
- attr_reader :cache
503
-
504
- # Returns the uploader that is used for the permanent storage.
505
- attr_reader :store
506
-
507
- # Returns the context that will be sent to the uploader when uploading
508
- # and deleting. Can be modified with additional data to be sent to the
509
- # uploader.
510
- attr_reader :context
511
-
512
- # Returns an array of validation errors created on file assignment in
513
- # the `Attacher.validate` block.
514
- attr_reader :errors
515
-
516
- # Initializes the necessary attributes.
517
- def initialize(record, name, cache: :cache, store: :store)
518
- @cache = shrine_class.new(cache)
519
- @store = shrine_class.new(store)
520
- @context = {record: record, name: name}
521
- @errors = []
522
- end
523
-
524
- # Returns the model instance associated with the attacher.
525
- def record; context[:record]; end
526
-
527
- # Returns the attachment name associated with the attacher.
528
- def name; context[:name]; end
529
-
530
- # Receives the attachment value from the form. It can receive an
531
- # already cached file as a JSON string, otherwise it assumes that it's
532
- # an IO object and uploads it to the temporary storage. The cached file
533
- # is then written to the attachment attribute in the JSON format.
534
- def assign(value, **options)
535
- if value.is_a?(String)
536
- return if value == "" || !cache.uploaded?(uploaded_file(value))
537
- assign_cached(uploaded_file(value))
538
- else
539
- uploaded_file = cache!(value, action: :cache, **options) if value
540
- set(uploaded_file)
541
- end
542
- end
543
-
544
- # Accepts a Shrine::UploadedFile object and writes it to the attachment
545
- # attribute. It then runs file validations, and records that the
546
- # attachment has changed.
547
- def set(uploaded_file)
548
- file = get
549
- @old = file unless uploaded_file == file
550
- _set(uploaded_file)
551
- validate
552
- end
553
-
554
- # Runs the validations defined by `Attacher.validate`.
555
- def validate
556
- errors.clear
557
- validate_block if get
558
- end
559
-
560
- # Returns true if a new file has been attached.
561
- def changed?
562
- instance_variable_defined?(:@old)
563
- end
564
- alias attached? changed?
565
-
566
- # Plugins can override this if they want something to be done before
567
- # save.
568
- def save
569
- end
570
-
571
- # Deletes the old file and promotes the new one. Typically this should
572
- # be called after saving the model instance.
573
- def finalize
574
- return if !instance_variable_defined?(:@old)
575
- replace
576
- remove_instance_variable(:@old)
577
- _promote(action: :store) if cached?
578
- end
579
-
580
- # Delegates to #promote, overriden for backgrounding.
581
- def _promote(uploaded_file = get, **options)
582
- promote(uploaded_file, **options)
583
- end
584
-
585
- # Uploads the cached file to store, and writes the stored file to the
586
- # attachment attribute.
587
- def promote(uploaded_file = get, **options)
588
- stored_file = store!(uploaded_file, **options)
589
- result = swap(stored_file) or _delete(stored_file, action: :abort)
590
- result
591
- end
592
-
593
- # Calls #update, overriden in ORM plugins, and returns true if the
594
- # attachment was successfully updated.
595
- def swap(uploaded_file)
596
- update(uploaded_file)
597
- uploaded_file if uploaded_file == get
598
- end
599
-
600
- # Deletes the previous attachment that was replaced, typically called
601
- # after the model instance is saved with the new attachment.
602
- def replace
603
- _delete(@old, action: :replace) if @old && !cache.uploaded?(@old)
604
- end
605
-
606
- # Deletes the current attachment, typically called after destroying the
607
- # record.
608
- def destroy
609
- file = get
610
- _delete(file, action: :destroy) if file && !cache.uploaded?(file)
611
- end
612
-
613
- # Delegates to #delete!, overriden for backgrounding.
614
- def _delete(uploaded_file, **options)
615
- delete!(uploaded_file, **options)
616
- end
617
-
618
- # Returns the URL to the attached file if it's present. It forwards any
619
- # given URL options to the storage.
620
- def url(**options)
621
- get.url(**options) if read
622
- end
623
-
624
- # Returns true if attachment is present and cached.
625
- def cached?
626
- file = get
627
- file && cache.uploaded?(file)
628
- end
629
-
630
- # Returns true if attachment is present and stored.
631
- def stored?
632
- file = get
633
- file && store.uploaded?(file)
634
- end
635
-
636
- # Returns a Shrine::UploadedFile instantiated from the data written to
637
- # the attachment attribute.
638
- def get
639
- uploaded_file(read) if read
640
- end
641
-
642
- # Reads from the `<attachment>_data` attribute on the model instance.
643
- # It returns nil if the value is blank.
644
- def read
645
- value = record.send(data_attribute)
646
- convert_after_read(value) unless value.nil? || value.empty?
647
- end
648
-
649
- # Uploads the file using the #cache uploader, passing the #context.
650
- def cache!(io, **options)
651
- Shrine.deprecation("Sending :phase to Attacher#cache! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
652
- cache.upload(io, context.merge(_equalize_phase_and_action(options)))
653
- end
150
+ # Prints a deprecation warning to standard error.
151
+ def deprecation(message)
152
+ warn "SHRINE DEPRECATION WARNING: #{message}"
153
+ end
154
+ end
654
155
 
655
- # Uploads the file using the #store uploader, passing the #context.
656
- def store!(io, **options)
657
- Shrine.deprecation("Sending :phase to Attacher#store! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
658
- store.upload(io, context.merge(_equalize_phase_and_action(options)))
659
- end
156
+ module InstanceMethods
157
+ # The symbol identifier for the storage used by the uploader.
158
+ attr_reader :storage_key
660
159
 
661
- # Deletes the file using the uploader, passing the #context.
662
- def delete!(uploaded_file, **options)
663
- Shrine.deprecation("Sending :phase to Attacher#delete! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
664
- store.delete(uploaded_file, context.merge(_equalize_phase_and_action(options)))
665
- end
160
+ # The storage object used by the uploader.
161
+ attr_reader :storage
666
162
 
667
- # Enhances `Shrine.uploaded_file` with the ability to recognize uploaded
668
- # files as JSON strings.
669
- def uploaded_file(object, &block)
670
- shrine_class.uploaded_file(object, &block)
671
- end
672
-
673
- # The name of the attribute on the model instance that is used to store
674
- # the attachment data. Defaults to `<attachment>_data`.
675
- def data_attribute
676
- :"#{name}_data"
677
- end
163
+ # Accepts a storage symbol registered in `Shrine.storages`.
164
+ def initialize(storage_key)
165
+ @storage = self.class.find_storage(storage_key)
166
+ @storage_key = storage_key.to_sym
167
+ end
678
168
 
679
- # Returns the Shrine class that this attacher's class is namespaced
680
- # under.
681
- def shrine_class
682
- self.class.shrine_class
683
- end
169
+ # The class-level options hash. This should probably not be modified at
170
+ # the instance level.
171
+ def opts
172
+ self.class.opts
173
+ end
684
174
 
685
- private
175
+ # The main method for uploading files. Takes an IO-like object and an
176
+ # optional context hash (used internally by Shrine::Attacher). It calls
177
+ # user-defined #process, and afterwards it calls #store. The `io` is
178
+ # closed after upload.
179
+ def upload(io, context = {})
180
+ io = processed(io, context) || io
181
+ store(io, context)
182
+ end
686
183
 
687
- # Assigns a cached file.
688
- def assign_cached(cached_file)
689
- set(cached_file)
690
- end
184
+ # User is expected to perform processing inside this method, and
185
+ # return the processed files. Returning nil signals that no proccessing
186
+ # has been done and that the original file should be used.
187
+ #
188
+ # class ImageUploader < Shrine
189
+ # def process(io, context)
190
+ # # do processing and return processed files
191
+ # end
192
+ # end
193
+ def process(io, context = {})
194
+ end
691
195
 
692
- # Writes the uploaded file to the attachment attribute. Overriden in ORM
693
- # plugins to additionally save the model instance.
694
- def update(uploaded_file)
695
- _set(uploaded_file)
696
- end
196
+ # Uploads the file and returns an instance of Shrine::UploadedFile. By
197
+ # default the location of the file is automatically generated by
198
+ # \#generate_location, but you can pass in `:location` to upload to
199
+ # a specific location.
200
+ #
201
+ # uploader.store(io)
202
+ def store(io, context = {})
203
+ _store(io, context)
204
+ end
697
205
 
698
- # Performs validation actually.
699
- # This method is redefined with `Attacher.validate`.
700
- def validate_block
701
- end
206
+ # Returns true if the storage of the given uploaded file matches the
207
+ # storage of this uploader.
208
+ def uploaded?(uploaded_file)
209
+ uploaded_file.storage_key == storage_key.to_s
210
+ end
702
211
 
703
- # Converts the UploadedFile to a data hash and writes it to the
704
- # attribute.
705
- def _set(uploaded_file)
706
- data = convert_to_data(uploaded_file) if uploaded_file
707
- write(data ? convert_before_write(data) : nil)
708
- end
212
+ # Deletes the given uploaded file and returns it.
213
+ def delete(uploaded_file, context = {})
214
+ _delete(uploaded_file, context)
215
+ uploaded_file
216
+ end
709
217
 
710
- # Writes to the `<attachment>_data` attribute on the model instance.
711
- def write(value)
712
- record.send(:"#{data_attribute}=", value)
713
- end
218
+ # Generates a unique location for the uploaded file, preserving the
219
+ # file extension. Can be overriden in uploaders for generating custom
220
+ # location.
221
+ def generate_location(io, context = {})
222
+ extension = ".#{io.extension}" if io.is_a?(UploadedFile) && io.extension
223
+ extension ||= File.extname(extract_filename(io).to_s).downcase
224
+ basename = generate_uid(io)
714
225
 
715
- # Returns the data hash of the given UploadedFile.
716
- def convert_to_data(uploaded_file)
717
- uploaded_file.data
718
- end
226
+ basename + extension
227
+ end
719
228
 
720
- # Returns the hash value dumped to JSON.
721
- def convert_before_write(value)
722
- value.to_json
723
- end
229
+ # Extracts filename, size and MIME type from the file, which is later
230
+ # accessible through UploadedFile#metadata.
231
+ def extract_metadata(io, context = {})
232
+ {
233
+ "filename" => extract_filename(io),
234
+ "size" => extract_size(io),
235
+ "mime_type" => extract_mime_type(io),
236
+ }
237
+ end
724
238
 
725
- # Returns the read value unchanged.
726
- def convert_after_read(value)
727
- value
728
- end
239
+ private
729
240
 
730
- # Temporary method used for transitioning from :phase to :action.
731
- def _equalize_phase_and_action(options)
732
- options[:phase] = options[:action] if options.key?(:action)
733
- options[:action] = options[:phase] if options.key?(:phase)
734
- options
735
- end
241
+ # Attempts to extract the appropriate filename from the IO object.
242
+ def extract_filename(io)
243
+ if io.respond_to?(:original_filename)
244
+ io.original_filename
245
+ elsif io.respond_to?(:path)
246
+ File.basename(io.path)
736
247
  end
248
+ end
737
249
 
738
- module FileClassMethods
739
- # Returns the Shrine class that this file class is namespaced under.
740
- attr_accessor :shrine_class
741
-
742
- # Since UploadedFile is anonymously subclassed when Shrine is subclassed,
743
- # and then assigned to a constant of the Shrine subclass, make inspect
744
- # reflect the likely name for the class.
745
- def inspect
746
- "#{shrine_class.inspect}::UploadedFile"
747
- end
250
+ # Attempts to extract the MIME type from the IO object.
251
+ def extract_mime_type(io)
252
+ if io.respond_to?(:content_type) && io.content_type
253
+ warn "The \"mime_type\" Shrine metadata field will be set from the \"Content-Type\" request header, which might not hold the actual MIME type of the file. It is recommended to load the determine_mime_type plugin which determines MIME type from file content."
254
+ io.content_type.split(";").first # exclude media type parameters
748
255
  end
256
+ end
749
257
 
750
- module FileMethods
751
- # The hash of information which defines this uploaded file.
752
- attr_reader :data
753
-
754
- # Initializes the uploaded file with the given data hash.
755
- def initialize(data)
756
- raise Error, "#{data.inspect} isn't valid uploaded file data" unless data["id"] && data["storage"]
757
-
758
- @data = data
759
- @data["metadata"] ||= {}
760
- storage # ensure storage is registered
761
- end
762
-
763
- # The location where the file was uploaded to the storage.
764
- def id
765
- @data.fetch("id")
766
- end
767
-
768
- # The string identifier of the storage the file is uploaded to.
769
- def storage_key
770
- @data.fetch("storage")
771
- end
772
-
773
- # A hash of file metadata that was extracted during upload.
774
- def metadata
775
- @data.fetch("metadata")
776
- end
777
-
778
- # The filename that was extracted from the uploaded file.
779
- def original_filename
780
- metadata["filename"]
781
- end
782
-
783
- # The extension derived from #id if present, otherwise it's derived
784
- # from #original_filename.
785
- def extension
786
- result = File.extname(id)[1..-1] || File.extname(original_filename.to_s)[1..-1]
787
- result.downcase if result
788
- end
789
-
790
- # The filesize of the uploaded file.
791
- def size
792
- (@io && @io.size) || (metadata["size"] && Integer(metadata["size"]))
793
- end
794
-
795
- # The MIME type of the uploaded file.
796
- def mime_type
797
- metadata["mime_type"]
798
- end
799
- alias content_type mime_type
800
-
801
- # Calls `#open` on the storage to open the uploaded file for reading.
802
- # Most storages will return a lazy IO object which dynamically
803
- # retrieves file content from the storage as the object is being read.
804
- #
805
- # If a block is given, the opened IO object is yielded to the block,
806
- # and at the end of the block it's automatically closed. In this case
807
- # the return value of the method is the block return value.
808
- #
809
- # If no block is given, the opened IO object is returned.
810
- #
811
- # uploaded_file.open #=> IO object returned by the storage
812
- # uploaded_file.read #=> "..."
813
- # uploaded_file.close
814
- #
815
- # # or
816
- #
817
- # uploaded_file.open { |io| io.read } # the IO is automatically closed
818
- def open(*args)
819
- @io.close if @io && !(@io.respond_to?(:closed?) && @io.closed?)
820
- @io = storage.open(id, *args)
821
-
822
- return @io unless block_given?
823
-
824
- begin
825
- yield @io
826
- ensure
827
- @io.close
828
- @io = nil
829
- end
830
- end
831
-
832
- # Calls `#download` on the storage if the storage implements it,
833
- # otherwise streams content into a newly created Tempfile.
834
- #
835
- # If a block is given, the opened Tempfile object is yielded to the
836
- # block, and at the end of the block it's automatically closed and
837
- # deleted. In this case the return value of the method is the block
838
- # return value.
839
- #
840
- # If no block is given, the opened Tempfile is returned.
841
- #
842
- # uploaded_file.download
843
- # #=> #<File:/var/folders/.../20180302-33119-1h1vjbq.jpg>
844
- #
845
- # # or
846
- #
847
- # uploaded_file.download { |tempfile| tempfile.read } # tempfile is deleted
848
- def download(*args)
849
- if storage.respond_to?(:download)
850
- tempfile = storage.download(id, *args)
851
- else
852
- tempfile = Tempfile.new(["shrine", ".#{extension}"], binmode: true)
853
- stream(tempfile, *args)
854
- tempfile.open
855
- end
856
-
857
- block_given? ? yield(tempfile) : tempfile
858
- ensure
859
- tempfile.close! if ($! || block_given?) && tempfile
860
- end
861
-
862
- # Streams uploaded file content into the specified destination. The
863
- # destination object is given directly to `IO.copy_stream`, so it can
864
- # be either a path on disk or an object that responds to `#write`.
865
- #
866
- # If the uploaded file is already opened, it will be simply rewinded
867
- # after streaming finishes. Otherwise the uploaded file is opened and
868
- # then closed after streaming.
869
- #
870
- # uploaded_file.stream(StringIO.new)
871
- # # or
872
- # uploaded_file.stream("/path/to/destination")
873
- def stream(destination, *args)
874
- if @io
875
- IO.copy_stream(io, destination)
876
- io.rewind
877
- else
878
- open(*args) { |io| IO.copy_stream(io, destination) }
879
- end
880
- end
881
-
882
- # Part of complying to the IO interface. It delegates to the internally
883
- # opened IO object.
884
- def read(*args)
885
- io.read(*args)
886
- end
887
-
888
- # Part of complying to the IO interface. It delegates to the internally
889
- # opened IO object.
890
- def eof?
891
- io.eof?
892
- end
893
-
894
- # Part of complying to the IO interface. It delegates to the internally
895
- # opened IO object.
896
- def close
897
- io.close if @io
898
- end
899
-
900
- # Part of complying to the IO interface. It delegates to the internally
901
- # opened IO object.
902
- def rewind
903
- io.rewind
904
- end
258
+ # Extracts the filesize from the IO object.
259
+ def extract_size(io)
260
+ io.size if io.respond_to?(:size)
261
+ end
905
262
 
906
- # Calls `#url` on the storage, forwarding any given URL options.
907
- def url(**options)
908
- storage.url(id, **options)
909
- end
263
+ # It first asserts that `io` is a valid IO object. It then extracts
264
+ # metadata and generates the location, before calling the storage to
265
+ # upload the IO object, passing the extracted metadata and location.
266
+ # Finally it returns a Shrine::UploadedFile object which represents the
267
+ # file that was uploaded.
268
+ def _store(io, context)
269
+ _enforce_io(io)
910
270
 
911
- # Calls `#exists?` on the storage, which checks whether the file exists
912
- # on the storage.
913
- def exists?
914
- storage.exists?(id)
915
- end
271
+ metadata = get_metadata(io, context)
272
+ metadata = metadata.merge(context[:metadata]) if context[:metadata]
916
273
 
917
- # Uploads a new file to this file's location and returns it.
918
- def replace(io, context = {})
919
- uploader.upload(io, context.merge(location: id))
920
- end
274
+ location = get_location(io, context.merge(metadata: metadata))
921
275
 
922
- # Calls `#delete` on the storage, which deletes the file from the
923
- # storage.
924
- def delete
925
- storage.delete(id)
926
- end
276
+ put(io, context.merge(location: location, metadata: metadata))
927
277
 
928
- # Returns an opened IO object for the uploaded file.
929
- def to_io
930
- io
931
- end
278
+ self.class.uploaded_file(
279
+ "id" => location,
280
+ "storage" => storage_key.to_s,
281
+ "metadata" => metadata,
282
+ )
283
+ end
932
284
 
933
- # Returns the data hash in the JSON format. Suitable for storing in a
934
- # database column or passing to a background job.
935
- def to_json(*args)
936
- data.to_json(*args)
937
- end
285
+ # Delegates to #remove.
286
+ def _delete(uploaded_file, context)
287
+ remove(uploaded_file, context)
288
+ end
938
289
 
939
- # Conform to ActiveSupport's JSON interface.
940
- def as_json(*args)
941
- data
942
- end
290
+ # Delegates to #copy.
291
+ def put(io, context)
292
+ copy(io, context)
293
+ end
943
294
 
944
- # Returns true if the other UploadedFile is uploaded to the same
945
- # storage and it has the same #id.
946
- def ==(other)
947
- other.is_a?(self.class) &&
948
- self.id == other.id &&
949
- self.storage_key == other.storage_key
950
- end
951
- alias eql? ==
295
+ # Calls `#upload` on the storage, passing to it the location, metadata
296
+ # and any upload options. The storage might modify the location or
297
+ # metadata that were passed in. The uploaded IO is then closed.
298
+ def copy(io, context)
299
+ location = context[:location]
300
+ metadata = context[:metadata]
301
+ upload_options = context[:upload_options] || {}
302
+
303
+ storage.upload(io, location, shrine_metadata: metadata, **upload_options)
304
+ ensure
305
+ io.close rescue nil
306
+ end
952
307
 
953
- # Enables using UploadedFile objects as hash keys.
954
- def hash
955
- [id, storage_key].hash
956
- end
308
+ # Delegates to `UploadedFile#delete`.
309
+ def remove(uploaded_file, context)
310
+ uploaded_file.delete
311
+ end
957
312
 
958
- # Returns an uploader object for the corresponding storage.
959
- def uploader
960
- shrine_class.new(storage_key)
961
- end
313
+ # Delegates to #process.
314
+ def processed(io, context)
315
+ process(io, context)
316
+ end
962
317
 
963
- # Returns the storage that this file was uploaded to.
964
- def storage
965
- shrine_class.find_storage(storage_key)
966
- end
318
+ # Retrieves the location for the given IO and context. First it looks
319
+ # for the `:location` option, otherwise it calls #generate_location.
320
+ def get_location(io, context)
321
+ location = context[:location] || generate_location(io, context)
322
+ location or raise Error, "location generated for #{io.inspect} was nil (context = #{context})"
323
+ end
967
324
 
968
- # Returns the Shrine class that this file's class is namespaced under.
969
- def shrine_class
970
- self.class.shrine_class
971
- end
325
+ # If the IO object is a Shrine::UploadedFile, it simply copies over its
326
+ # metadata, otherwise it calls #extract_metadata.
327
+ def get_metadata(io, context)
328
+ if io.is_a?(UploadedFile)
329
+ io.metadata.dup
330
+ else
331
+ extract_metadata(io, context)
332
+ end
333
+ end
972
334
 
973
- private
335
+ # Asserts that the object is a valid IO object, specifically that it
336
+ # responds to `#read`, `#eof?`, `#rewind`, `#size` and `#close`. If the
337
+ # object doesn't respond to one of these methods, a Shrine::InvalidFile
338
+ # error is raised.
339
+ def _enforce_io(io)
340
+ missing_methods = %i[read eof? rewind close].select { |m| !io.respond_to?(m) }
341
+ raise InvalidFile.new(io, missing_methods) if missing_methods.any?
342
+ end
974
343
 
975
- # Returns an opened IO object for the uploaded file by calling `#open`
976
- # on the storage.
977
- def io
978
- @io || open
979
- end
980
- end
344
+ # Generates a unique identifier that can be used for a location.
345
+ def generate_uid(io)
346
+ SecureRandom.hex
981
347
  end
982
348
  end
349
+ end
983
350
 
984
- extend Plugins::Base::ClassMethods
985
- plugin Plugins::Base
351
+ [Shrine, Shrine::UploadedFile, Shrine::Attacher, Shrine::Attachment].each do |core_class|
352
+ core_class.include core_class.const_get(:InstanceMethods)
353
+ core_class.extend core_class.const_get(:ClassMethods)
986
354
  end