shrine 2.3.1 → 2.4.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.
- checksums.yaml +4 -4
- data/README.md +212 -89
- data/doc/attacher.md +227 -0
- data/doc/design.md +5 -19
- data/doc/paperclip.md +2 -1
- data/doc/testing.md +266 -0
- data/lib/shrine.rb +220 -168
- data/lib/shrine/plugins/activerecord.rb +23 -14
- data/lib/shrine/plugins/backgrounding.rb +3 -3
- data/lib/shrine/plugins/cached_attachment_data.rb +6 -5
- data/lib/shrine/plugins/copy.rb +10 -9
- data/lib/shrine/plugins/data_uri.rb +5 -0
- data/lib/shrine/plugins/default_url_options.rb +17 -4
- data/lib/shrine/plugins/direct_upload.rb +6 -11
- data/lib/shrine/plugins/download_endpoint.rb +8 -24
- data/lib/shrine/plugins/multi_delete.rb +1 -1
- data/lib/shrine/plugins/processing.rb +8 -0
- data/lib/shrine/plugins/remote_url.rb +5 -0
- data/lib/shrine/plugins/remove_attachment.rb +3 -10
- data/lib/shrine/plugins/sequel.rb +28 -25
- data/lib/shrine/plugins/versions.rb +12 -1
- data/lib/shrine/storage/file_system.rb +16 -14
- data/lib/shrine/storage/linter.rb +6 -0
- data/lib/shrine/storage/s3.rb +51 -25
- data/lib/shrine/version.rb +2 -2
- data/shrine.gemspec +2 -2
- metadata +8 -7
- data/lib/shrine/plugins/concatenation.rb +0 -73
data/lib/shrine.rb
CHANGED
@@ -4,9 +4,10 @@ require "securerandom"
|
|
4
4
|
require "json"
|
5
5
|
|
6
6
|
class Shrine
|
7
|
+
# A generic exception used by Shrine.
|
7
8
|
class Error < StandardError; end
|
8
9
|
|
9
|
-
# Raised when a file
|
10
|
+
# Raised when a file is not a valid IO.
|
10
11
|
class InvalidFile < Error
|
11
12
|
def initialize(io, missing_methods)
|
12
13
|
@io, @missing_methods = io, missing_methods
|
@@ -24,7 +25,7 @@ class Shrine
|
|
24
25
|
end
|
25
26
|
|
26
27
|
# Methods which an object has to respond to in order to be considered
|
27
|
-
# an IO object
|
28
|
+
# an IO object, along with their arguments.
|
28
29
|
IO_METHODS = {
|
29
30
|
:read => [:length, :outbuf],
|
30
31
|
:eof? => [],
|
@@ -33,24 +34,24 @@ class Shrine
|
|
33
34
|
:close => [],
|
34
35
|
}
|
35
36
|
|
36
|
-
# Core class that represents a file uploaded to a storage.
|
37
|
+
# Core class that represents a file uploaded to a storage. The instance
|
37
38
|
# methods for this class are added by Shrine::Plugins::Base::FileMethods, the
|
38
39
|
# class methods are added by Shrine::Plugins::Base::FileClassMethods.
|
39
40
|
class UploadedFile
|
40
41
|
@shrine_class = ::Shrine
|
41
42
|
end
|
42
43
|
|
43
|
-
# Core class which
|
44
|
-
# model classes.
|
45
|
-
# Shrine::Plugins::Base::AttachmentMethods, the class methods
|
46
|
-
# Shrine::Plugins::Base::AttachmentClassMethods.
|
44
|
+
# Core class which creates attachment modules for specified attribute names
|
45
|
+
# that are included into model classes. The instance methods for this class
|
46
|
+
# are added by Shrine::Plugins::Base::AttachmentMethods, the class methods
|
47
|
+
# are added by Shrine::Plugins::Base::AttachmentClassMethods.
|
47
48
|
class Attachment < Module
|
48
49
|
@shrine_class = ::Shrine
|
49
50
|
end
|
50
51
|
|
51
|
-
# Core class which handles attaching files
|
52
|
-
# for this class are added by Shrine::Plugins::Base::
|
53
|
-
# class methods are added by Shrine::Plugins::Base::
|
52
|
+
# Core class which handles attaching files to model instances. The instance
|
53
|
+
# methods for this class are added by Shrine::Plugins::Base::AttacherMethods,
|
54
|
+
# the class methods are added by Shrine::Plugins::Base::AttacherClassMethods.
|
54
55
|
class Attacher
|
55
56
|
@shrine_class = ::Shrine
|
56
57
|
end
|
@@ -63,8 +64,8 @@ class Shrine
|
|
63
64
|
module Plugins
|
64
65
|
@plugins = {}
|
65
66
|
|
66
|
-
# If the registered plugin already exists, use it.
|
67
|
-
# and return it.
|
67
|
+
# If the registered plugin already exists, use it. Otherwise, require it
|
68
|
+
# and return it. This raises a LoadError if such a plugin doesn't exist,
|
68
69
|
# or a Shrine::Error if it exists but it does not register itself
|
69
70
|
# correctly.
|
70
71
|
def self.load_plugin(name)
|
@@ -76,7 +77,7 @@ class Shrine
|
|
76
77
|
end
|
77
78
|
|
78
79
|
# Register the given plugin with Shrine, so that it can be loaded using
|
79
|
-
# `Shrine.plugin` with a symbol.
|
80
|
+
# `Shrine.plugin` with a symbol. Should be used by plugin files. Example:
|
80
81
|
#
|
81
82
|
# Shrine::Plugins.register_plugin(:plugin_name, PluginModule)
|
82
83
|
def self.register_plugin(name, mod)
|
@@ -91,11 +92,11 @@ class Shrine
|
|
91
92
|
# Generic options for this class, plugins store their options here.
|
92
93
|
attr_reader :opts
|
93
94
|
|
94
|
-
# A hash of storages
|
95
|
+
# A hash of storages with their symbol identifiers.
|
95
96
|
attr_accessor :storages
|
96
97
|
|
97
98
|
# When inheriting Shrine, copy the instance variables into the subclass,
|
98
|
-
# and
|
99
|
+
# and create subclasses of core classes.
|
99
100
|
def inherited(subclass)
|
100
101
|
subclass.instance_variable_set(:@opts, opts.dup)
|
101
102
|
subclass.opts.each do |key, value|
|
@@ -118,12 +119,12 @@ class Shrine
|
|
118
119
|
subclass.const_set(:Attacher, attacher_class)
|
119
120
|
end
|
120
121
|
|
121
|
-
# Load a new plugin into the current class.
|
122
|
-
# which is used directly, or a symbol
|
123
|
-
# which will be required and then
|
122
|
+
# Load a new plugin into the current class. A plugin can be a module
|
123
|
+
# which is used directly, or a symbol representing a registered plugin
|
124
|
+
# which will be required and then loaded.
|
124
125
|
#
|
125
|
-
# Shrine.plugin
|
126
|
-
# Shrine.plugin :
|
126
|
+
# Shrine.plugin MyPlugin
|
127
|
+
# Shrine.plugin :my_plugin
|
127
128
|
def plugin(plugin, *args, &block)
|
128
129
|
plugin = Plugins.load_plugin(plugin) if plugin.is_a?(Symbol)
|
129
130
|
plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies)
|
@@ -139,35 +140,33 @@ class Shrine
|
|
139
140
|
nil
|
140
141
|
end
|
141
142
|
|
142
|
-
# Retrieves the storage
|
143
|
-
#
|
143
|
+
# Retrieves the storage under the given identifier (can be a Symbol or
|
144
|
+
# a String), and raises Shrine::Error if the storage is missing.
|
144
145
|
def find_storage(name)
|
145
146
|
storages.each { |key, value| return value if key.to_s == name.to_s }
|
146
147
|
raise Error, "storage #{name.inspect} isn't registered on #{self}"
|
147
148
|
end
|
148
149
|
|
149
150
|
# Generates an instance of Shrine::Attachment to be included in the
|
150
|
-
# model class.
|
151
|
+
# model class. Example:
|
151
152
|
#
|
152
|
-
# class
|
153
|
-
# include Shrine[:
|
153
|
+
# class Photo
|
154
|
+
# include Shrine[:image] # creates a Shrine::Attachment object
|
154
155
|
# end
|
155
156
|
def attachment(name)
|
156
157
|
self::Attachment.new(name)
|
157
158
|
end
|
158
159
|
alias [] attachment
|
159
160
|
|
160
|
-
# Instantiates a Shrine::UploadedFile from a
|
161
|
-
#
|
162
|
-
# is used internally by Shrine::Attacher, but it's also useful when you
|
163
|
-
# need to deserialize the uploaded file in background jobs.
|
161
|
+
# Instantiates a Shrine::UploadedFile from a hash, and optionally
|
162
|
+
# yields the returned object.
|
164
163
|
#
|
165
|
-
#
|
166
|
-
#
|
167
|
-
# Shrine.uploaded_file(json) #=> #<Shrine::UploadedFile>
|
164
|
+
# data = {"storage" => "cache", "id" => "abc123.jpg", "metadata" => {}}
|
165
|
+
# Shrine.uploaded_file(data) #=> #<Shrine::UploadedFile>
|
168
166
|
def uploaded_file(object, &block)
|
169
167
|
case object
|
170
168
|
when String
|
169
|
+
warn "Giving a string to Shrine.uploaded_file is deprecated and won't be possible in Shrine 3. Use Attacher#uploaded_file instead."
|
171
170
|
uploaded_file(JSON.parse(object), &block)
|
172
171
|
when Hash
|
173
172
|
uploaded_file(self::UploadedFile.new(object), &block)
|
@@ -180,10 +179,10 @@ class Shrine
|
|
180
179
|
end
|
181
180
|
|
182
181
|
module InstanceMethods
|
183
|
-
# The symbol
|
182
|
+
# The symbol identifier for the storage used by the uploader.
|
184
183
|
attr_reader :storage_key
|
185
184
|
|
186
|
-
# The storage object
|
185
|
+
# The storage object used by the uploader.
|
187
186
|
attr_reader :storage
|
188
187
|
|
189
188
|
# Accepts a storage symbol registered in `Shrine.storages`.
|
@@ -192,33 +191,28 @@ class Shrine
|
|
192
191
|
@storage_key = storage_key.to_sym
|
193
192
|
end
|
194
193
|
|
195
|
-
# The class-level options hash.
|
196
|
-
#
|
194
|
+
# The class-level options hash. This should probably not be modified at
|
195
|
+
# the instance level.
|
197
196
|
def opts
|
198
197
|
self.class.opts
|
199
198
|
end
|
200
199
|
|
201
|
-
# The main method for uploading files.
|
202
|
-
# optional context (used internally by Shrine::Attacher).
|
203
|
-
# user-defined #process, and aferwards it calls #store.
|
200
|
+
# The main method for uploading files. Takes an IO-like object and an
|
201
|
+
# optional context hash (used internally by Shrine::Attacher). It calls
|
202
|
+
# user-defined #process, and aferwards it calls #store. The `io` is
|
204
203
|
# closed after upload.
|
205
204
|
def upload(io, context = {})
|
206
205
|
io = processed(io, context) || io
|
207
206
|
store(io, context)
|
208
207
|
end
|
209
208
|
|
210
|
-
# User is expected to perform processing inside
|
209
|
+
# User is expected to perform processing inside this method, and
|
211
210
|
# return the processed files. Returning nil signals that no proccessing
|
212
211
|
# has been done and that the original file should be used.
|
213
212
|
#
|
214
213
|
# class ImageUploader < Shrine
|
215
214
|
# def process(io, context)
|
216
|
-
#
|
217
|
-
# when :cache
|
218
|
-
# # do processing
|
219
|
-
# when :store
|
220
|
-
# # do processing
|
221
|
-
# end
|
215
|
+
# # do processing and return processed files
|
222
216
|
# end
|
223
217
|
# end
|
224
218
|
def process(io, context = {})
|
@@ -229,25 +223,26 @@ class Shrine
|
|
229
223
|
# \#generate_location, but you can pass in `:location` to upload to
|
230
224
|
# a specific location.
|
231
225
|
#
|
232
|
-
# uploader.store(io
|
226
|
+
# uploader.store(io)
|
233
227
|
def store(io, context = {})
|
234
228
|
_store(io, context)
|
235
229
|
end
|
236
230
|
|
237
|
-
#
|
238
|
-
#
|
231
|
+
# Returns true if the storage of the given uploaded file matches the
|
232
|
+
# storage of this uploader.
|
239
233
|
def uploaded?(uploaded_file)
|
240
234
|
uploaded_file.storage_key == storage_key.to_s
|
241
235
|
end
|
242
236
|
|
243
|
-
# Deletes the given uploaded file.
|
237
|
+
# Deletes the given uploaded file and returns it.
|
244
238
|
def delete(uploaded_file, context = {})
|
245
239
|
_delete(uploaded_file, context)
|
246
240
|
uploaded_file
|
247
241
|
end
|
248
242
|
|
249
|
-
# Generates a unique location for the uploaded file,
|
250
|
-
#
|
243
|
+
# Generates a unique location for the uploaded file, preserving the
|
244
|
+
# file extension. Can be overriden in uploaders for generating custom
|
245
|
+
# location.
|
251
246
|
def generate_location(io, context = {})
|
252
247
|
extension = ".#{io.extension}" if io.is_a?(UploadedFile) && io.extension
|
253
248
|
extension ||= File.extname(extract_filename(io).to_s)
|
@@ -257,8 +252,7 @@ class Shrine
|
|
257
252
|
end
|
258
253
|
|
259
254
|
# Extracts filename, size and MIME type from the file, which is later
|
260
|
-
# accessible through `UploadedFile#metadata`.
|
261
|
-
# is later promoted, this metadata is simply copied over.
|
255
|
+
# accessible through `UploadedFile#metadata`.
|
262
256
|
def extract_metadata(io, context = {})
|
263
257
|
{
|
264
258
|
"filename" => extract_filename(io),
|
@@ -269,7 +263,7 @@ class Shrine
|
|
269
263
|
|
270
264
|
private
|
271
265
|
|
272
|
-
#
|
266
|
+
# Attempts to extract the appropriate filename from the IO object.
|
273
267
|
def extract_filename(io)
|
274
268
|
if io.respond_to?(:original_filename)
|
275
269
|
io.original_filename
|
@@ -278,7 +272,7 @@ class Shrine
|
|
278
272
|
end
|
279
273
|
end
|
280
274
|
|
281
|
-
#
|
275
|
+
# Attempts to extract the MIME type from the IO object.
|
282
276
|
def extract_mime_type(io)
|
283
277
|
if io.respond_to?(:content_type)
|
284
278
|
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." unless opts.key?(:mime_type_analyzer)
|
@@ -286,14 +280,16 @@ class Shrine
|
|
286
280
|
end
|
287
281
|
end
|
288
282
|
|
289
|
-
# Extracts the filesize from the IO.
|
283
|
+
# Extracts the filesize from the IO object.
|
290
284
|
def extract_size(io)
|
291
285
|
io.size
|
292
286
|
end
|
293
287
|
|
294
|
-
#
|
295
|
-
#
|
296
|
-
# the
|
288
|
+
# It first asserts that `io` is a valid IO object. It then extracts
|
289
|
+
# metadata and generates the location, before calling the storage to
|
290
|
+
# upload the IO object, passing the extracted metadata and location.
|
291
|
+
# Finally it returns a Shrine::UploadedFile object which represents the
|
292
|
+
# file that was uploaded.
|
297
293
|
def _store(io, context)
|
298
294
|
_enforce_io(io)
|
299
295
|
metadata = get_metadata(io, context)
|
@@ -301,24 +297,26 @@ class Shrine
|
|
301
297
|
|
302
298
|
put(io, context.merge(location: location, metadata: metadata))
|
303
299
|
|
304
|
-
self.class
|
300
|
+
self.class.uploaded_file(
|
305
301
|
"id" => location,
|
306
302
|
"storage" => storage_key.to_s,
|
307
303
|
"metadata" => metadata,
|
308
304
|
)
|
309
305
|
end
|
310
306
|
|
311
|
-
#
|
307
|
+
# Delegates to #remove.
|
312
308
|
def _delete(uploaded_file, context)
|
313
309
|
remove(uploaded_file, context)
|
314
310
|
end
|
315
311
|
|
316
|
-
#
|
312
|
+
# Delegates to #copy.
|
317
313
|
def put(io, context)
|
318
314
|
copy(io, context)
|
319
315
|
end
|
320
316
|
|
321
|
-
#
|
317
|
+
# Calls `#upload` on the storage, passing to it the location, metadata
|
318
|
+
# and any upload options. The storage might modify the location or
|
319
|
+
# metadata that were passed in. The uploaded IO is then closed.
|
322
320
|
def copy(io, context)
|
323
321
|
location = context[:location]
|
324
322
|
metadata = context[:metadata]
|
@@ -329,24 +327,24 @@ class Shrine
|
|
329
327
|
io.close rescue nil
|
330
328
|
end
|
331
329
|
|
332
|
-
#
|
330
|
+
# Delegates to `UploadedFile#delete`.
|
333
331
|
def remove(uploaded_file, context)
|
334
332
|
uploaded_file.delete
|
335
333
|
end
|
336
334
|
|
337
|
-
#
|
335
|
+
# Delegates to #process.
|
338
336
|
def processed(io, context)
|
339
337
|
process(io, context)
|
340
338
|
end
|
341
339
|
|
342
|
-
# Retrieves the location for the given
|
340
|
+
# Retrieves the location for the given IO and context. First it looks
|
343
341
|
# for the `:location` option, otherwise it calls #generate_location.
|
344
342
|
def get_location(io, context)
|
345
343
|
context[:location] || generate_location(io, context)
|
346
344
|
end
|
347
345
|
|
348
|
-
#
|
349
|
-
# #extract_metadata.
|
346
|
+
# If the IO object is a Shrine::UploadedFile, it simply copies over its
|
347
|
+
# metadata, otherwise it calls #extract_metadata.
|
350
348
|
def get_metadata(io, context)
|
351
349
|
if io.is_a?(UploadedFile)
|
352
350
|
io.metadata.dup
|
@@ -355,22 +353,24 @@ class Shrine
|
|
355
353
|
end
|
356
354
|
end
|
357
355
|
|
358
|
-
#
|
359
|
-
# `#read`, `#eof?`, `#rewind`, `#size` and `#close
|
360
|
-
# Shrine::InvalidFile
|
356
|
+
# Asserts that the object is a valid IO object, specifically that it
|
357
|
+
# responds to `#read`, `#eof?`, `#rewind`, `#size` and `#close`. If the
|
358
|
+
# object doesn't respond to one of these methods, a Shrine::InvalidFile
|
359
|
+
# error is raised.
|
361
360
|
def _enforce_io(io)
|
362
361
|
missing_methods = IO_METHODS.select { |m, a| !io.respond_to?(m) }
|
363
362
|
raise InvalidFile.new(io, missing_methods) if missing_methods.any?
|
364
363
|
end
|
365
364
|
|
366
|
-
# Generates a
|
365
|
+
# Generates a unique identifier that can be used for a location.
|
367
366
|
def generate_uid(io)
|
368
367
|
SecureRandom.hex
|
369
368
|
end
|
370
369
|
end
|
371
370
|
|
372
371
|
module AttachmentClassMethods
|
373
|
-
#
|
372
|
+
# Returns the Shrine class that this attachment class is
|
373
|
+
# namespaced under.
|
374
374
|
attr_accessor :shrine_class
|
375
375
|
|
376
376
|
# Since Attachment is anonymously subclassed when Shrine is subclassed,
|
@@ -382,16 +382,13 @@ class Shrine
|
|
382
382
|
end
|
383
383
|
|
384
384
|
module AttachmentMethods
|
385
|
-
#
|
386
|
-
#
|
385
|
+
# Instantiates an attachment module for a given attribute name, which
|
386
|
+
# can then be included to a model class.
|
387
387
|
def initialize(name)
|
388
388
|
@name = name
|
389
389
|
|
390
|
-
# We store the attacher class so that
|
391
|
-
#
|
392
|
-
# class variable because (a) it can be accessed from the instance
|
393
|
-
# level without needing to create a class-level reader, and (b) we
|
394
|
-
# want it to be inherited when subclassing the model
|
390
|
+
# We store the attacher class so that the model can instantiate the
|
391
|
+
# correct attacher instance.
|
395
392
|
class_variable_set(:"@@#{name}_attacher_class", shrine_class::Attacher)
|
396
393
|
|
397
394
|
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
@@ -413,21 +410,30 @@ class Shrine
|
|
413
410
|
RUBY
|
414
411
|
end
|
415
412
|
|
416
|
-
#
|
413
|
+
# Includes the attachment name in the output.
|
417
414
|
#
|
418
|
-
# Shrine[:
|
415
|
+
# Shrine[:image].to_s #=> "#<Shrine::Attachment(image)>"
|
416
|
+
def to_s
|
417
|
+
"#<#{self.class.inspect}(#{@name})>"
|
418
|
+
end
|
419
|
+
|
420
|
+
# Includes the attachment name in the output.
|
421
|
+
#
|
422
|
+
# Shrine[:image].inspect #=> "#<Shrine::Attachment(image)>"
|
419
423
|
def inspect
|
420
424
|
"#<#{self.class.inspect}(#{@name})>"
|
421
425
|
end
|
422
426
|
|
423
|
-
# Returns the Shrine class
|
427
|
+
# Returns the Shrine class that this attachment's class is namespaced
|
428
|
+
# under.
|
424
429
|
def shrine_class
|
425
430
|
self.class.shrine_class
|
426
431
|
end
|
427
432
|
end
|
428
433
|
|
429
434
|
module AttacherClassMethods
|
430
|
-
#
|
435
|
+
# Returns the Shrine class that this attacher class is namespaced
|
436
|
+
# under.
|
431
437
|
attr_accessor :shrine_class
|
432
438
|
|
433
439
|
# Since Attacher is anonymously subclassed when Shrine is subclassed,
|
@@ -438,7 +444,7 @@ class Shrine
|
|
438
444
|
end
|
439
445
|
|
440
446
|
# Block that is executed in context of Shrine::Attacher during
|
441
|
-
# validation.
|
447
|
+
# validation. Example:
|
442
448
|
#
|
443
449
|
# Shrine::Attacher.validate do
|
444
450
|
# if get.size > 5*1024*1024
|
@@ -451,8 +457,22 @@ class Shrine
|
|
451
457
|
end
|
452
458
|
|
453
459
|
module AttacherMethods
|
454
|
-
|
460
|
+
# Returns the uploader that is used for the temporary storage.
|
461
|
+
attr_reader :cache
|
462
|
+
|
463
|
+
# Returns the uploader that is used for the permanent storage.
|
464
|
+
attr_reader :store
|
465
|
+
|
466
|
+
# Returns the context that will be sent to the uploader when uploading
|
467
|
+
# and deleting. Can be modified with additional data to be sent to the
|
468
|
+
# uploader.
|
469
|
+
attr_reader :context
|
455
470
|
|
471
|
+
# Returns an array of validation errors created on file assignment in
|
472
|
+
# the `Attacher.validate` block.
|
473
|
+
attr_reader :errors
|
474
|
+
|
475
|
+
# Initializes the necessary attributes.
|
456
476
|
def initialize(record, name, cache: :cache, store: :store)
|
457
477
|
@cache = shrine_class.new(cache)
|
458
478
|
@store = shrine_class.new(store)
|
@@ -462,13 +482,14 @@ class Shrine
|
|
462
482
|
|
463
483
|
# Returns the model instance associated with the attacher.
|
464
484
|
def record; context[:record]; end
|
485
|
+
|
465
486
|
# Returns the attachment name associated with the attacher.
|
466
487
|
def name; context[:name]; end
|
467
488
|
|
468
|
-
# Receives the attachment value from the form.
|
469
|
-
#
|
470
|
-
#
|
471
|
-
#
|
489
|
+
# Receives the attachment value from the form. It can receive an
|
490
|
+
# already cached file as a JSON string, otherwise it assumes that it's
|
491
|
+
# an IO object and uploads it to the temporary storage. The cached file
|
492
|
+
# is then written to the attachment attribute in the JSON format.
|
472
493
|
def assign(value)
|
473
494
|
if value.is_a?(String)
|
474
495
|
return if value == "" || value == read || !cache.uploaded?(uploaded_file(value))
|
@@ -479,8 +500,9 @@ class Shrine
|
|
479
500
|
end
|
480
501
|
end
|
481
502
|
|
482
|
-
#
|
483
|
-
#
|
503
|
+
# Accepts a Shrine::UploadedFile object and writes is to the attachment
|
504
|
+
# attribute. It then runs file validations, and records that the
|
505
|
+
# attachment has changed.
|
484
506
|
def set(uploaded_file)
|
485
507
|
@old = get
|
486
508
|
_set(uploaded_file)
|
@@ -498,57 +520,58 @@ class Shrine
|
|
498
520
|
instance_variable_defined?(:@old)
|
499
521
|
end
|
500
522
|
|
501
|
-
# Plugins can override this if they want something to be done
|
523
|
+
# Plugins can override this if they want something to be done before
|
524
|
+
# save.
|
502
525
|
def save
|
503
526
|
end
|
504
527
|
|
505
528
|
# Deletes the old file and promotes the new one. Typically this should
|
506
|
-
# be called after saving.
|
529
|
+
# be called after saving the model instance.
|
507
530
|
def finalize
|
508
531
|
replace
|
509
532
|
remove_instance_variable(:@old)
|
510
533
|
_promote(action: :store) if cached?
|
511
534
|
end
|
512
535
|
|
513
|
-
#
|
536
|
+
# Delegates to #promote, overriden for backgrounding.
|
514
537
|
def _promote(uploaded_file = get, **options)
|
515
538
|
promote(uploaded_file, **options)
|
516
539
|
end
|
517
540
|
|
518
|
-
# Uploads the cached file to store, and
|
519
|
-
#
|
541
|
+
# Uploads the cached file to store, and writes the stored file to the
|
542
|
+
# attachment attribute.
|
520
543
|
def promote(uploaded_file = get, **options)
|
521
544
|
stored_file = store!(uploaded_file, **options)
|
522
545
|
result = swap(stored_file) or _delete(stored_file, action: :abort)
|
523
546
|
result
|
524
547
|
end
|
525
548
|
|
526
|
-
# Calls #update, overriden in ORM plugins
|
549
|
+
# Calls #update, overriden in ORM plugins, and returns true if the
|
550
|
+
# attachment was successfully updated.
|
527
551
|
def swap(uploaded_file)
|
528
552
|
update(uploaded_file)
|
529
553
|
uploaded_file if uploaded_file == get
|
530
554
|
end
|
531
555
|
|
532
|
-
# Deletes the attachment that was replaced,
|
533
|
-
#
|
534
|
-
# don't get called for the current attachment anymore.
|
556
|
+
# Deletes the previous attachment that was replaced, typically called
|
557
|
+
# after the model instance is saved with the new attachment.
|
535
558
|
def replace
|
536
559
|
_delete(@old, action: :replace) if @old && !cache.uploaded?(@old)
|
537
560
|
end
|
538
561
|
|
539
|
-
# Deletes the attachment
|
540
|
-
#
|
562
|
+
# Deletes the current attachment, typically called after destroying the
|
563
|
+
# record.
|
541
564
|
def destroy
|
542
565
|
_delete(get, action: :destroy) if get && !cache.uploaded?(get)
|
543
566
|
end
|
544
567
|
|
545
|
-
#
|
568
|
+
# Delegates to #delete!, overriden for backgrounding.
|
546
569
|
def _delete(uploaded_file, **options)
|
547
570
|
delete!(uploaded_file, **options)
|
548
571
|
end
|
549
572
|
|
550
|
-
# Returns the URL to the attached file
|
551
|
-
#
|
573
|
+
# Returns the URL to the attached file if it's present. It forwards any
|
574
|
+
# given URL options to the storage.
|
552
575
|
def url(**options)
|
553
576
|
get.url(**options) if read
|
554
577
|
end
|
@@ -563,41 +586,55 @@ class Shrine
|
|
563
586
|
get && store.uploaded?(get)
|
564
587
|
end
|
565
588
|
|
566
|
-
#
|
589
|
+
# Returns a Shrine::UploadedFile instantiated from the data written to
|
590
|
+
# the attachment attribute.
|
567
591
|
def get
|
568
592
|
uploaded_file(read) if read
|
569
593
|
end
|
570
594
|
|
571
|
-
#
|
595
|
+
# Reads from the `<attachment>_data` attribute on the model instance.
|
596
|
+
# It returns nil if the value is blank.
|
572
597
|
def read
|
573
|
-
value = record.send(
|
574
|
-
value unless value.nil? || value.empty?
|
598
|
+
value = record.send(data_attribute)
|
599
|
+
convert_after_read(value) unless value.nil? || value.empty?
|
575
600
|
end
|
576
601
|
|
577
|
-
# Uploads the file
|
602
|
+
# Uploads the file using the #cache uploader, passing the #context.
|
578
603
|
def cache!(io, **options)
|
579
604
|
warn "Sending :phase to Shrine::Attacher#cache! is deprecated and will not be supported in Shrine 3. Use :action instead." if options[:phase]
|
580
605
|
cache.upload(io, context.merge(_equalize_phase_and_action(options)))
|
581
606
|
end
|
582
607
|
|
583
|
-
# Uploads the file
|
608
|
+
# Uploads the file using the #store uploader, passing the #context.
|
584
609
|
def store!(io, **options)
|
585
610
|
warn "Sending :phase to Shrine::Attacher#store! is deprecated and will not be supported in Shrine 3. Use :action instead." if options[:phase]
|
586
611
|
store.upload(io, context.merge(_equalize_phase_and_action(options)))
|
587
612
|
end
|
588
613
|
|
589
|
-
# Deletes the file passing context.
|
614
|
+
# Deletes the file using the uploader, passing the #context.
|
590
615
|
def delete!(uploaded_file, **options)
|
591
616
|
warn "Sending :phase to Shrine::Attacher#delete! is deprecated and will not be supported in Shrine 3. Use :action instead." if options[:phase]
|
592
617
|
store.delete(uploaded_file, context.merge(_equalize_phase_and_action(options)))
|
593
618
|
end
|
594
619
|
|
595
|
-
#
|
596
|
-
|
597
|
-
|
620
|
+
# Enhances `Shrine.uploaded_file` with the ability to recognize uploaded
|
621
|
+
# files as JSON strings.
|
622
|
+
def uploaded_file(object, &block)
|
623
|
+
if object.is_a?(String)
|
624
|
+
uploaded_file(JSON.parse(object), &block)
|
625
|
+
else
|
626
|
+
shrine_class.uploaded_file(object, &block)
|
627
|
+
end
|
628
|
+
end
|
629
|
+
|
630
|
+
# The name of the attribute on the model instance that is used to store
|
631
|
+
# the attachment data. Defaults to `<attachment>_data`.
|
632
|
+
def data_attribute
|
633
|
+
:"#{name}_data"
|
598
634
|
end
|
599
635
|
|
600
|
-
# Returns the Shrine class
|
636
|
+
# Returns the Shrine class that this attacher's class is namespaced
|
637
|
+
# under.
|
601
638
|
def shrine_class
|
602
639
|
self.class.shrine_class
|
603
640
|
end
|
@@ -609,24 +646,42 @@ class Shrine
|
|
609
646
|
set(cached_file)
|
610
647
|
end
|
611
648
|
|
612
|
-
#
|
649
|
+
# Writes the uploaded file the attachment attribute. Overriden in ORM
|
650
|
+
# plugins to additionally save the model instance.
|
613
651
|
def update(uploaded_file)
|
614
652
|
_set(uploaded_file)
|
615
653
|
end
|
616
654
|
|
617
|
-
# The validation block
|
655
|
+
# The validation block registered with `Attacher.validate`.
|
618
656
|
def validate_block
|
619
657
|
shrine_class.opts[:validate]
|
620
658
|
end
|
621
659
|
|
622
|
-
#
|
660
|
+
# Converts the UploadedFile to a data hash and writes it to the
|
661
|
+
# attribute.
|
623
662
|
def _set(uploaded_file)
|
624
|
-
write(uploaded_file ? uploaded_file
|
663
|
+
write(uploaded_file ? convert_to_data(uploaded_file) : nil)
|
625
664
|
end
|
626
665
|
|
627
|
-
#
|
666
|
+
# Writes to the `<attachment>_data` attribute on the model instance.
|
628
667
|
def write(value)
|
629
|
-
|
668
|
+
value = convert_before_write(value) unless value.nil?
|
669
|
+
record.send(:"#{data_attribute}=", value)
|
670
|
+
end
|
671
|
+
|
672
|
+
# Returns the data hash of the given UploadedFile.
|
673
|
+
def convert_to_data(uploaded_file)
|
674
|
+
uploaded_file.data
|
675
|
+
end
|
676
|
+
|
677
|
+
# Returns the hash value dumped to JSON.
|
678
|
+
def convert_before_write(value)
|
679
|
+
value.to_json
|
680
|
+
end
|
681
|
+
|
682
|
+
# Returns the read value unchanged.
|
683
|
+
def convert_after_read(value)
|
684
|
+
value
|
630
685
|
end
|
631
686
|
|
632
687
|
# Temporary method used for transitioning from :phase to :action.
|
@@ -638,7 +693,7 @@ class Shrine
|
|
638
693
|
end
|
639
694
|
|
640
695
|
module FileClassMethods
|
641
|
-
#
|
696
|
+
# Returns the Shrine class that this file class is namespaced under.
|
642
697
|
attr_accessor :shrine_class
|
643
698
|
|
644
699
|
# Since UploadedFile is anonymously subclassed when Shrine is subclassed,
|
@@ -650,32 +705,32 @@ class Shrine
|
|
650
705
|
end
|
651
706
|
|
652
707
|
module FileMethods
|
653
|
-
# The
|
708
|
+
# The hash of information which defines this uploaded file.
|
654
709
|
attr_reader :data
|
655
710
|
|
711
|
+
# Initializes the uploaded file with the given data hash.
|
656
712
|
def initialize(data)
|
657
713
|
@data = data
|
658
714
|
@data["metadata"] ||= {}
|
659
715
|
storage # ensure storage exists
|
660
716
|
end
|
661
717
|
|
662
|
-
# The
|
663
|
-
# file on the storage
|
718
|
+
# The location where the file was uploaded to the storage.
|
664
719
|
def id
|
665
720
|
@data.fetch("id")
|
666
721
|
end
|
667
722
|
|
668
|
-
# The storage
|
723
|
+
# The string identifier of the storage the file is uploaded to.
|
669
724
|
def storage_key
|
670
725
|
@data.fetch("storage")
|
671
726
|
end
|
672
727
|
|
673
|
-
# A hash of metadata.
|
728
|
+
# A hash of file metadata that was extracted during upload.
|
674
729
|
def metadata
|
675
730
|
@data.fetch("metadata")
|
676
731
|
end
|
677
732
|
|
678
|
-
# The filename that was extracted from the
|
733
|
+
# The filename that was extracted from the uploaded file.
|
679
734
|
def original_filename
|
680
735
|
metadata["filename"]
|
681
736
|
end
|
@@ -686,34 +741,34 @@ class Shrine
|
|
686
741
|
File.extname(id)[1..-1] || File.extname(original_filename.to_s)[1..-1]
|
687
742
|
end
|
688
743
|
|
689
|
-
# The filesize of the
|
744
|
+
# The filesize of the uploaded file.
|
690
745
|
def size
|
691
746
|
(@io && @io.size) || (metadata["size"] && Integer(metadata["size"]))
|
692
747
|
end
|
693
748
|
|
694
|
-
# The MIME type of the
|
749
|
+
# The MIME type of the uploaded file.
|
695
750
|
def mime_type
|
696
751
|
metadata["mime_type"]
|
697
752
|
end
|
698
753
|
alias content_type mime_type
|
699
754
|
|
700
|
-
# Opens the
|
701
|
-
# closing it after the block finishes.
|
702
|
-
# block.
|
755
|
+
# Opens an IO object of the uploaded file for reading and yields it to
|
756
|
+
# the block, closing it after the block finishes. For opening without
|
757
|
+
# a block #to_io can be used.
|
703
758
|
#
|
704
759
|
# uploaded_file.open do |io|
|
705
|
-
# #
|
760
|
+
# puts io.read # prints the content of the file
|
706
761
|
# end
|
707
762
|
def open
|
708
763
|
@io = storage.open(id)
|
709
764
|
yield @io
|
710
765
|
ensure
|
711
|
-
@io.close
|
766
|
+
@io.close if @io
|
712
767
|
@io = nil
|
713
768
|
end
|
714
769
|
|
715
|
-
# Calls `#download` on the storage if
|
716
|
-
#
|
770
|
+
# Calls `#download` on the storage if the storage implements it,
|
771
|
+
# otherwise uses #open to stream the underlying IO to a Tempfile.
|
717
772
|
def download
|
718
773
|
if storage.respond_to?(:download)
|
719
774
|
storage.download(id)
|
@@ -724,39 +779,37 @@ class Shrine
|
|
724
779
|
end
|
725
780
|
end
|
726
781
|
|
727
|
-
# Part of
|
728
|
-
#
|
782
|
+
# Part of complying to the IO interface. It delegates to the internally
|
783
|
+
# opened IO object.
|
729
784
|
def read(*args)
|
730
785
|
io.read(*args)
|
731
786
|
end
|
732
787
|
|
733
|
-
# Part of
|
734
|
-
#
|
788
|
+
# Part of complying to the IO interface. It delegates to the internally
|
789
|
+
# opened IO object.
|
735
790
|
def eof?
|
736
791
|
io.eof?
|
737
792
|
end
|
738
793
|
|
739
|
-
# Part of
|
740
|
-
#
|
794
|
+
# Part of complying to the IO interface. It delegates to the internally
|
795
|
+
# opened IO object.
|
741
796
|
def close
|
742
|
-
if @io
|
743
|
-
io.close
|
744
|
-
io.delete if io.class.name == "Tempfile"
|
745
|
-
end
|
797
|
+
io.close if @io
|
746
798
|
end
|
747
799
|
|
748
|
-
# Part of
|
749
|
-
#
|
800
|
+
# Part of complying to the IO interface. It delegates to the internally
|
801
|
+
# opened IO object.
|
750
802
|
def rewind
|
751
803
|
io.rewind
|
752
804
|
end
|
753
805
|
|
754
|
-
# Calls `#url` on the storage, forwarding any options.
|
806
|
+
# Calls `#url` on the storage, forwarding any given URL options.
|
755
807
|
def url(**options)
|
756
808
|
storage.url(id, **options)
|
757
809
|
end
|
758
810
|
|
759
|
-
# Calls `#exists?` on the storage, which checks
|
811
|
+
# Calls `#exists?` on the storage, which checks whether the file exists
|
812
|
+
# on the storage.
|
760
813
|
def exists?
|
761
814
|
storage.exists?(id)
|
762
815
|
end
|
@@ -766,18 +819,19 @@ class Shrine
|
|
766
819
|
uploader.upload(io, context.merge(location: id))
|
767
820
|
end
|
768
821
|
|
769
|
-
# Calls `#delete` on the storage, which deletes the
|
822
|
+
# Calls `#delete` on the storage, which deletes the file from the
|
823
|
+
# storage.
|
770
824
|
def delete
|
771
825
|
storage.delete(id)
|
772
826
|
end
|
773
827
|
|
774
|
-
# Returns the
|
828
|
+
# Returns an opened IO object for the uploaded file.
|
775
829
|
def to_io
|
776
830
|
io
|
777
831
|
end
|
778
832
|
|
779
|
-
#
|
780
|
-
# column or passing to a background job.
|
833
|
+
# Returns the data hash in the JSON format. Suitable for storing in a
|
834
|
+
# database column or passing to a background job.
|
781
835
|
def to_json(*args)
|
782
836
|
data.to_json(*args)
|
783
837
|
end
|
@@ -787,8 +841,8 @@ class Shrine
|
|
787
841
|
data
|
788
842
|
end
|
789
843
|
|
790
|
-
#
|
791
|
-
# and
|
844
|
+
# Returns true if the other UploadedFile is uploaded to the same
|
845
|
+
# storage and it has the same #id.
|
792
846
|
def ==(other)
|
793
847
|
other.is_a?(self.class) &&
|
794
848
|
self.id == other.id &&
|
@@ -796,32 +850,30 @@ class Shrine
|
|
796
850
|
end
|
797
851
|
alias eql? ==
|
798
852
|
|
853
|
+
# Enables using UploadedFile objects as hash keys.
|
799
854
|
def hash
|
800
855
|
[id, storage_key].hash
|
801
856
|
end
|
802
857
|
|
803
|
-
#
|
858
|
+
# Returns an uploader object for the corresponding storage.
|
804
859
|
def uploader
|
805
860
|
shrine_class.new(storage_key)
|
806
861
|
end
|
807
862
|
|
808
|
-
#
|
863
|
+
# Returns the storage that this file was uploaded to.
|
809
864
|
def storage
|
810
865
|
shrine_class.find_storage(storage_key)
|
811
866
|
end
|
812
867
|
|
813
|
-
# Returns the Shrine class
|
868
|
+
# Returns the Shrine class that this file's class is namespaced under.
|
814
869
|
def shrine_class
|
815
870
|
self.class.shrine_class
|
816
871
|
end
|
817
872
|
|
818
|
-
# Show only the data hash in inspect output.
|
819
|
-
def inspect
|
820
|
-
"#{to_s.chomp(">")} @data=#{data.inspect}>"
|
821
|
-
end
|
822
|
-
|
823
873
|
private
|
824
874
|
|
875
|
+
# Returns an opened IO object for the uploaded file by calling `#open`
|
876
|
+
# on the storage.
|
825
877
|
def io
|
826
878
|
@io ||= storage.open(id)
|
827
879
|
end
|