shrine 3.0.0.beta2 → 3.0.0.beta3

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -1
  3. data/README.md +100 -106
  4. data/doc/advantages.md +90 -88
  5. data/doc/attacher.md +322 -152
  6. data/doc/carrierwave.md +105 -113
  7. data/doc/changing_derivatives.md +308 -0
  8. data/doc/changing_location.md +92 -21
  9. data/doc/changing_storage.md +107 -0
  10. data/doc/creating_plugins.md +1 -1
  11. data/doc/design.md +8 -9
  12. data/doc/direct_s3.md +3 -2
  13. data/doc/metadata.md +97 -78
  14. data/doc/multiple_files.md +3 -3
  15. data/doc/paperclip.md +89 -88
  16. data/doc/plugins/activerecord.md +3 -12
  17. data/doc/plugins/backgrounding.md +126 -100
  18. data/doc/plugins/derivation_endpoint.md +4 -5
  19. data/doc/plugins/derivatives.md +63 -32
  20. data/doc/plugins/download_endpoint.md +54 -1
  21. data/doc/plugins/entity.md +1 -0
  22. data/doc/plugins/form_assign.md +53 -0
  23. data/doc/plugins/mirroring.md +37 -16
  24. data/doc/plugins/multi_cache.md +22 -0
  25. data/doc/plugins/presign_endpoint.md +1 -1
  26. data/doc/plugins/remote_url.md +19 -4
  27. data/doc/plugins/validation.md +83 -0
  28. data/doc/processing.md +149 -133
  29. data/doc/refile.md +68 -63
  30. data/doc/release_notes/3.0.0.md +835 -0
  31. data/doc/securing_uploads.md +56 -36
  32. data/doc/storage/s3.md +2 -2
  33. data/doc/testing.md +104 -120
  34. data/doc/upgrading_to_3.md +538 -0
  35. data/doc/validation.md +48 -87
  36. data/lib/shrine.rb +7 -4
  37. data/lib/shrine/attacher.rb +16 -6
  38. data/lib/shrine/plugins/activerecord.rb +33 -14
  39. data/lib/shrine/plugins/atomic_helpers.rb +1 -1
  40. data/lib/shrine/plugins/backgrounding.rb +23 -89
  41. data/lib/shrine/plugins/data_uri.rb +13 -2
  42. data/lib/shrine/plugins/derivation_endpoint.rb +7 -11
  43. data/lib/shrine/plugins/derivatives.rb +44 -20
  44. data/lib/shrine/plugins/download_endpoint.rb +26 -0
  45. data/lib/shrine/plugins/form_assign.rb +6 -3
  46. data/lib/shrine/plugins/keep_files.rb +2 -2
  47. data/lib/shrine/plugins/mirroring.rb +62 -22
  48. data/lib/shrine/plugins/model.rb +2 -2
  49. data/lib/shrine/plugins/multi_cache.rb +27 -0
  50. data/lib/shrine/plugins/remote_url.rb +25 -10
  51. data/lib/shrine/plugins/remove_invalid.rb +1 -1
  52. data/lib/shrine/plugins/sequel.rb +39 -20
  53. data/lib/shrine/plugins/validation.rb +3 -0
  54. data/lib/shrine/storage/s3.rb +16 -1
  55. data/lib/shrine/uploaded_file.rb +1 -0
  56. data/lib/shrine/version.rb +1 -1
  57. data/shrine.gemspec +1 -1
  58. metadata +12 -7
  59. data/doc/migrating_storage.md +0 -76
  60. data/doc/regenerating_versions.md +0 -143
  61. data/lib/shrine/plugins/attacher_options.rb +0 -55
@@ -43,11 +43,11 @@ class Shrine
43
43
  super if defined?(super)
44
44
 
45
45
  define_method :"#{name}_data_uri=" do |uri|
46
- send(:"#{name}_attacher").assign_data_uri(uri)
46
+ send(:"#{name}_attacher").data_uri = uri
47
47
  end
48
48
 
49
49
  define_method :"#{name}_data_uri" do
50
- # form builders require the reader method
50
+ send(:"#{name}_attacher").data_uri
51
51
  end
52
52
  end
53
53
  end
@@ -115,6 +115,17 @@ class Shrine
115
115
  false
116
116
  end
117
117
 
118
+ # Used by `<name>_data_uri=` attachment method.
119
+ def data_uri=(uri)
120
+ assign_data_uri(uri)
121
+ @data_uri = uri
122
+ end
123
+
124
+ # Used by `<name>_data_uri` attachment method.
125
+ def data_uri
126
+ @data_uri
127
+ end
128
+
118
129
  private
119
130
 
120
131
  # Generates an error message for failed data URI parse.
@@ -605,18 +605,14 @@ class Shrine
605
605
 
606
606
  # Massages the derivation result, ensuring it's opened in binary mode,
607
607
  # rewinded and flushed to disk.
608
- def normalize(derivative)
609
- derivative =
610
- case derivative
611
- when Tempfile then derivative.tap(&:open)
612
- when File then File.open(derivative.tap(&:close))
613
- when String, Pathname then File.open(derivative)
614
- else
615
- fail Error, "unexpected derivation result: #{derivation.inspect} (expected File, Tempfile, String, or Pathname object)"
616
- end
608
+ def normalize(file)
609
+ unless file.is_a?(File) || file.is_a?(Tempfile)
610
+ fail Error, "expected File or Tempfile object as derivation result, got #{file.inspect}"
611
+ end
617
612
 
618
- derivative.binmode
619
- derivative
613
+ file.open if file.is_a?(Tempfile) # refresh file descriptor
614
+ file.binmode # ensure binary mode
615
+ file
620
616
  end
621
617
 
622
618
  def with_downloaded(file, &block)
@@ -6,6 +6,8 @@ class Shrine
6
6
  #
7
7
  # [doc/plugins/derivatives.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/derivatives.md
8
8
  module Derivatives
9
+ NOOP_PROCESSOR = -> (*) { Hash.new }
10
+
9
11
  LOG_SUBSCRIBER = -> (event) do
10
12
  Shrine.logger.info "Derivatives (#{event.duration}ms) – #{{
11
13
  processor: event[:processor],
@@ -52,12 +54,16 @@ class Shrine
52
54
  # Attacher.derivatives_processor :thumbnails do |original|
53
55
  # # ...
54
56
  # end
55
- def derivatives_processor(name, &block)
57
+ def derivatives_processor(name = :default, &block)
56
58
  if block
57
59
  shrine_class.opts[:derivatives][:processors][name.to_sym] = block
58
60
  else
59
- shrine_class.opts[:derivatives][:processors][name.to_sym] or
60
- fail Error, "derivatives processor #{name.inspect} not registered"
61
+ processor = shrine_class.opts[:derivatives][:processors][name.to_sym]
62
+ processor ||= NOOP_PROCESSOR if name == :default
63
+
64
+ fail Error, "derivatives processor #{name.inspect} not registered" unless processor
65
+
66
+ processor
61
67
  end
62
68
  end
63
69
 
@@ -139,9 +145,9 @@ class Shrine
139
145
  # attacher.add_derivative(:thumb, file, storage: :cache)
140
146
  # attacher.promote
141
147
  # attacher.stored?(attacher.derivatives[:thumb]) #=> true
142
- def promote(background: false, **options)
148
+ def promote(**options)
143
149
  super
144
- promote_derivatives unless background
150
+ promote_derivatives
145
151
  end
146
152
 
147
153
  # Uploads any cached derivatives to permanent storage.
@@ -163,9 +169,9 @@ class Shrine
163
169
  # attacher.derivatives[:thumb].exists? #=> true
164
170
  # attacher.destroy
165
171
  # attacher.derivatives[:thumb].exists? #=> false
166
- def destroy(background: false, **options)
172
+ def destroy
167
173
  super
168
- delete_derivatives unless background
174
+ delete_derivatives
169
175
  end
170
176
 
171
177
  # Calls processor and adds returned derivatives.
@@ -235,6 +241,9 @@ class Shrine
235
241
  def upload_derivative(path, file, storage: nil, **options)
236
242
  storage ||= derivative_storage(path)
237
243
 
244
+ file.open if file.is_a?(Tempfile) # refresh file descriptor
245
+ file.binmode if file.respond_to?(:binmode) # ensure binary mode
246
+
238
247
  upload(file, storage, derivative: path, delete: true, **options)
239
248
  end
240
249
 
@@ -252,22 +261,22 @@ class Shrine
252
261
  #
253
262
  # attacher.process_derivatives(:thumbnails)
254
263
  # #=> { small: #<File:...>, medium: #<File:...>, large: #<File:...> }
255
- def process_derivatives(processor_name, source = file!, **options)
256
- processor = self.class.derivatives_processor(processor_name)
257
- fetch_source = source.is_a?(UploadedFile) ? source.method(:download) : source.method(:tap)
258
- result = nil
259
-
260
- fetch_source.call do |source_file|
261
- instrument_derivatives(processor_name, options) do
262
- result = instance_exec(source_file, **options, &processor)
263
- end
264
+ def process_derivatives(processor_name = :default, source = nil, **options)
265
+ # handle receiving only source file without a processor
266
+ unless processor_name.respond_to?(:to_sym)
267
+ source = processor_name
268
+ processor_name = :default
264
269
  end
265
270
 
266
- unless result.is_a?(Hash)
267
- fail Error, "expected derivatives processor #{processor_name.inspect} to return a Hash, got #{result.inspect}"
268
- end
271
+ source ||= file!
269
272
 
270
- result
273
+ if source.is_a?(UploadedFile)
274
+ source.download do |file|
275
+ _process_derivatives(processor_name, file, **options)
276
+ end
277
+ else
278
+ _process_derivatives(processor_name, source, **options)
279
+ end
271
280
  end
272
281
 
273
282
  # Deep merges given uploaded derivatives with current derivatives.
@@ -461,6 +470,21 @@ class Shrine
461
470
 
462
471
  private
463
472
 
473
+ # Calls the derivatives processor with the source file and options.
474
+ def _process_derivatives(processor_name, source, **options)
475
+ processor = self.class.derivatives_processor(processor_name)
476
+
477
+ result = instrument_derivatives(processor_name, options) do
478
+ instance_exec(source, **options, &processor)
479
+ end
480
+
481
+ unless result.is_a?(Hash)
482
+ fail Error, "expected derivatives processor #{processor_name.inspect} to return a Hash, got #{result.inspect}"
483
+ end
484
+
485
+ result
486
+ end
487
+
464
488
  # Sends a `derivatives.shrine` event for instrumentation plugin.
465
489
  def instrument_derivatives(processor_name, processor_options, &block)
466
490
  return yield unless shrine_class.respond_to?(:instrument)
@@ -25,6 +25,32 @@ class Shrine
25
25
  **options,
26
26
  )
27
27
  end
28
+
29
+ # Calls the download endpoint passing the request information, and
30
+ # returns the Rack response triple.
31
+ #
32
+ # It uses a trick where it removes the download path prefix from the
33
+ # path info before calling the Rack app, which is what web framework
34
+ # routers do before they're calling a mounted Rack app.
35
+ def download_response(env, **options)
36
+ script_name = env["SCRIPT_NAME"]
37
+ path_info = env["PATH_INFO"]
38
+
39
+ prefix = opts[:download_endpoint][:prefix]
40
+ match = path_info.match(/^\/#{prefix}/)
41
+
42
+ fail Error, "request path must start with \"/#{prefix}\", but is \"#{path_info}\"" unless match
43
+
44
+ begin
45
+ env["SCRIPT_NAME"] += match.to_s
46
+ env["PATH_INFO"] = match.post_match
47
+
48
+ download_endpoint(**options).call(env)
49
+ ensure
50
+ env["SCRIPT_NAME"] = script_name
51
+ env["PATH_INFO"] = path_info
52
+ end
53
+ end
28
54
  end
29
55
 
30
56
  module FileMethods
@@ -2,6 +2,9 @@
2
2
 
3
3
  class Shrine
4
4
  module Plugins
5
+ # Documentation lives in [doc/plugins/form_assign.md] on GitHub.
6
+ #
7
+ # [doc/plugins/form_assign.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/form_assign.md
5
8
  module FormAssign
6
9
  def self.load_dependencies(uploader)
7
10
  uploader.plugin :entity
@@ -77,8 +80,8 @@ class Shrine
77
80
  return fields unless changed?
78
81
 
79
82
  case result_type
80
- when :params then fields[name] = file&.to_json
81
- when :attributes then fields[:"#{name}_data"] = column_data
83
+ when :params then fields[name] = file&.to_json
84
+ when :attributes then fields[attribute] = column_data
82
85
  else
83
86
  fail ArgumentError, "unrecognized result type: #{result_type.inspect}"
84
87
  end
@@ -93,7 +96,7 @@ class Shrine
93
96
  shrine_subclass.plugin :model
94
97
 
95
98
  # create a model class with attachment methods
96
- form_class = Struct.new(:"#{name}_data")
99
+ form_class = Struct.new(attribute)
97
100
  form_class.include shrine_subclass::Attachment(name)
98
101
 
99
102
  # instantiate form object
@@ -7,8 +7,8 @@ class Shrine
7
7
  # [doc/plugins/keep_files.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/keep_files.md
8
8
  module KeepFiles
9
9
  module AttacherMethods
10
- def destroy_attached(*)
11
- # don't delete files
10
+ def destroy?
11
+ false
12
12
  end
13
13
  end
14
14
  end
@@ -3,11 +3,8 @@
3
3
  class Shrine
4
4
  module Plugins
5
5
  module Mirroring
6
- UPLOAD = -> (uploaded_file) { uploaded_file.mirror_upload }
7
- DELETE = -> (uploaded_file) { uploaded_file.mirror_delete }
8
-
9
6
  def self.configure(uploader, **opts)
10
- uploader.opts[:mirroring] ||= { upload: UPLOAD, delete: DELETE }
7
+ uploader.opts[:mirroring] ||= { upload: true, delete: true }
11
8
  uploader.opts[:mirroring].merge!(opts)
12
9
 
13
10
  fail Error, ":mirror option is required for mirroring plugin" unless uploader.opts[:mirroring][:mirror]
@@ -26,36 +23,61 @@ class Shrine
26
23
  end
27
24
  end
28
25
 
29
- def mirror_upload(&block)
26
+ def mirror_upload_block(&block)
30
27
  if block
31
- opts[:mirroring][:upload] = block
28
+ opts[:mirroring][:upload_block] = block
32
29
  else
33
- opts[:mirroring][:upload]
30
+ opts[:mirroring][:upload_block]
34
31
  end
35
32
  end
36
33
 
37
- def mirror_delete(&block)
34
+ def mirror_delete_block(&block)
38
35
  if block
39
- opts[:mirroring][:delete] = block
36
+ opts[:mirroring][:delete_block] = block
40
37
  else
41
- opts[:mirroring][:delete]
38
+ opts[:mirroring][:delete_block]
42
39
  end
43
40
  end
41
+
42
+ def mirror_upload?
43
+ opts[:mirroring][:upload]
44
+ end
45
+
46
+ def mirror_delete?
47
+ opts[:mirroring][:delete]
48
+ end
44
49
  end
45
50
 
46
51
  module InstanceMethods
47
- def upload(io, **options)
48
- uploaded_file = super
52
+ # Mirrors upload to other mirror storages.
53
+ def upload(io, mirror: true, **options)
54
+ file = super(io, **options)
55
+ file.trigger_mirror_upload if mirror
56
+ file
57
+ end
58
+ end
59
+
60
+ module FileMethods
61
+ # Mirrors upload if mirrors are defined. Calls mirror block if
62
+ # registered, otherwise mirrors synchronously.
63
+ def trigger_mirror_upload
64
+ return unless shrine_class.mirrors[storage_key] && shrine_class.mirror_upload?
49
65
 
50
- if self.class.mirrors[storage_key] && self.class.mirror_upload
51
- self.class.mirror_upload.call(uploaded_file)
66
+ if shrine_class.mirror_upload_block
67
+ mirror_upload_background
68
+ else
69
+ mirror_upload
52
70
  end
71
+ end
53
72
 
54
- uploaded_file
73
+ # Calls mirror upload block.
74
+ def mirror_upload_background
75
+ fail Error, "mirror upload block is not registered" unless shrine_class.mirror_upload_block
76
+
77
+ shrine_class.mirror_upload_block.call(self)
55
78
  end
56
- end
57
79
 
58
- module FileMethods
80
+ # Uploads the file to each mirror storage.
59
81
  def mirror_upload
60
82
  previously_opened = opened?
61
83
 
@@ -71,16 +93,33 @@ class Shrine
71
93
  end
72
94
  end
73
95
 
74
- def delete
75
- result = super
96
+ # Mirrors delete to other mirror storages.
97
+ def delete(mirror: true)
98
+ result = super()
99
+ trigger_mirror_delete if mirror
100
+ result
101
+ end
102
+
103
+ # Mirrors delete if mirrors are defined. Calls mirror block if
104
+ # registered, otherwise mirrors synchronously.
105
+ def trigger_mirror_delete
106
+ return unless shrine_class.mirrors[storage_key] && shrine_class.mirror_delete?
76
107
 
77
- if shrine_class.mirrors[storage_key] && shrine_class.mirror_delete
78
- shrine_class.mirror_delete.call(self)
108
+ if shrine_class.mirror_delete_block
109
+ mirror_delete_background
110
+ else
111
+ mirror_delete
79
112
  end
113
+ end
80
114
 
81
- result
115
+ # Calls mirror delete block.
116
+ def mirror_delete_background
117
+ fail Error, "mirror delete block is not registered" unless shrine_class.mirror_delete_block
118
+
119
+ shrine_class.mirror_delete_block.call(self)
82
120
  end
83
121
 
122
+ # Deletes the file from each mirror storage.
84
123
  def mirror_delete
85
124
  each_mirror do |mirror|
86
125
  self.class.new(id: id, storage: mirror).delete
@@ -89,6 +128,7 @@ class Shrine
89
128
 
90
129
  private
91
130
 
131
+ # Iterates over mirror storages.
92
132
  def each_mirror(&block)
93
133
  mirrors = shrine_class.mirrors(storage_key)
94
134
  mirrors.map(&block)
@@ -18,8 +18,8 @@ class Shrine
18
18
  module AttachmentMethods
19
19
  # Allows disabling model behaviour:
20
20
  #
21
- # Shrine::Attachment.new(:image) # model (default)
22
- # Shrine::Attachment.new(:image, model: false) # entity
21
+ # Shrine::Attachment(:image) # model (default)
22
+ # Shrine::Attachment(:image, model: false) # entity
23
23
  def initialize(name, model: true, **options)
24
24
  super(name, **options)
25
25
  @model = model
@@ -0,0 +1,27 @@
1
+ class Shrine
2
+ module Plugins
3
+ # Documentation lives in [doc/plugins/multi_cache.md] on GitHub.
4
+ #
5
+ # [doc/plugins/multi_cache.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/multi_cache.md
6
+ module MultiCache
7
+ def self.configure(uploader, **opts)
8
+ uploader.opts[:multi_cache] ||= {}
9
+ uploader.opts[:multi_cache].merge!(opts)
10
+ end
11
+
12
+ module AttacherMethods
13
+ def cached?(file = self.file)
14
+ super || additional_cache.any? { |key| uploaded?(file, key) }
15
+ end
16
+
17
+ private
18
+
19
+ def additional_cache
20
+ Array(shrine_class.opts[:multi_cache][:additional_cache])
21
+ end
22
+ end
23
+ end
24
+
25
+ register_plugin(:multi_cache, MultiCache)
26
+ end
27
+ end
@@ -18,13 +18,7 @@ class Shrine
18
18
  }.inspect}"
19
19
  end
20
20
 
21
- DOWNLOADER = -> (url, options) do
22
- begin
23
- Down.download(url, options)
24
- rescue Down::Error => error
25
- raise DownloadError, error.message
26
- end
27
- end
21
+ DOWNLOADER = -> (url, options) { Down.download(url, options) }
28
22
 
29
23
  def self.load_dependencies(uploader, *)
30
24
  uploader.plugin :validation
@@ -47,11 +41,11 @@ class Shrine
47
41
  super if defined?(super)
48
42
 
49
43
  define_method :"#{name}_remote_url=" do |url|
50
- send(:"#{name}_attacher").assign_remote_url(url)
44
+ send(:"#{name}_attacher").remote_url = url
51
45
  end
52
46
 
53
47
  define_method :"#{name}_remote_url" do
54
- # form builders require the reader method
48
+ send(:"#{name}_attacher").remote_url
55
49
  end
56
50
  end
57
51
  end
@@ -64,12 +58,22 @@ class Shrine
64
58
  options = { max_size: opts[:remote_url][:max_size] }.merge(options)
65
59
 
66
60
  instrument_remote_url(url, options) do
67
- opts[:remote_url][:downloader].call(url, options)
61
+ download_remote_url(url, options)
68
62
  end
69
63
  end
70
64
 
71
65
  private
72
66
 
67
+ def download_remote_url(url, options)
68
+ opts[:remote_url][:downloader].call(url, options)
69
+ rescue Down::NotFound
70
+ fail DownloadError, "remote file not found"
71
+ rescue Down::TooLarge
72
+ fail DownloadError, "remote file too large"
73
+ rescue DownloadError
74
+ fail
75
+ end
76
+
73
77
  # Sends a `remote_url.shrine` event for instrumentation plugin.
74
78
  def instrument_remote_url(url, options, &block)
75
79
  return yield unless respond_to?(:instrument)
@@ -92,6 +96,17 @@ class Shrine
92
96
  false
93
97
  end
94
98
 
99
+ # Used by `<name>_data_uri=` attachment method.
100
+ def remote_url=(url)
101
+ assign_remote_url(url)
102
+ @remote_url = url
103
+ end
104
+
105
+ # Used by `<name>_data_uri` attachment method.
106
+ def remote_url
107
+ @remote_url
108
+ end
109
+
95
110
  private
96
111
 
97
112
  # Generates an error message for failed remote URL download.