shrine 2.5.0 → 2.6.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -13
  3. data/doc/attacher.md +7 -6
  4. data/doc/carrierwave.md +19 -17
  5. data/doc/design.md +1 -1
  6. data/doc/direct_s3.md +8 -5
  7. data/doc/multiple_files.md +4 -4
  8. data/doc/paperclip.md +7 -6
  9. data/doc/refile.md +67 -4
  10. data/doc/securing_uploads.md +41 -25
  11. data/doc/testing.md +6 -15
  12. data/lib/shrine.rb +19 -10
  13. data/lib/shrine/plugins/activerecord.rb +4 -4
  14. data/lib/shrine/plugins/add_metadata.rb +7 -3
  15. data/lib/shrine/plugins/background_helpers.rb +1 -1
  16. data/lib/shrine/plugins/backgrounding.rb +19 -6
  17. data/lib/shrine/plugins/cached_attachment_data.rb +4 -4
  18. data/lib/shrine/plugins/data_uri.rb +105 -31
  19. data/lib/shrine/plugins/default_url.rb +1 -1
  20. data/lib/shrine/plugins/delete_raw.rb +7 -3
  21. data/lib/shrine/plugins/determine_mime_type.rb +96 -44
  22. data/lib/shrine/plugins/direct_upload.rb +3 -1
  23. data/lib/shrine/plugins/download_endpoint.rb +14 -5
  24. data/lib/shrine/plugins/logging.rb +4 -4
  25. data/lib/shrine/plugins/metadata_attributes.rb +61 -0
  26. data/lib/shrine/plugins/migration_helpers.rb +1 -1
  27. data/lib/shrine/plugins/rack_file.rb +54 -30
  28. data/lib/shrine/plugins/recache.rb +1 -1
  29. data/lib/shrine/plugins/refresh_metadata.rb +29 -0
  30. data/lib/shrine/plugins/remote_url.rb +26 -4
  31. data/lib/shrine/plugins/remove_invalid.rb +5 -4
  32. data/lib/shrine/plugins/restore_cached_data.rb +10 -13
  33. data/lib/shrine/plugins/sequel.rb +4 -4
  34. data/lib/shrine/plugins/signature.rb +146 -0
  35. data/lib/shrine/plugins/store_dimensions.rb +68 -24
  36. data/lib/shrine/plugins/validation_helpers.rb +48 -29
  37. data/lib/shrine/plugins/versions.rb +16 -8
  38. data/lib/shrine/storage/file_system.rb +27 -16
  39. data/lib/shrine/storage/s3.rb +99 -58
  40. data/lib/shrine/version.rb +1 -1
  41. data/shrine.gemspec +1 -1
  42. metadata +9 -6
@@ -29,7 +29,7 @@ class Shrine
29
29
  def self.configure(uploader, &block)
30
30
  if block
31
31
  uploader.opts[:default_url] = block
32
- warn "Passing a block to default_url Shrine plugin is deprecated and will probably be removed in future versions of Shrine. Use `Attacher.default_url { ... }` instead."
32
+ Shrine.deprecation("Passing a block to default_url plugin is deprecated and will probably be removed in future versions of Shrine. Use `Attacher.default_url { ... }` instead.")
33
33
  end
34
34
  end
35
35
 
@@ -21,12 +21,16 @@ class Shrine
21
21
  # Deletes the file that was uploaded, unless it's an UploadedFile.
22
22
  def copy(io, context)
23
23
  super
24
- if io.respond_to?(:delete) && !io.is_a?(UploadedFile)
25
- io.delete rescue nil if delete_uploaded?(io)
24
+ if io.respond_to?(:path) && io.path && delete_raw?
25
+ begin
26
+ File.delete(io.path)
27
+ rescue Errno::ENOENT
28
+ # file might already be deleted by the moving plugin
29
+ end
26
30
  end
27
31
  end
28
32
 
29
- def delete_uploaded?(io)
33
+ def delete_raw?
30
34
  opts[:delete_raw_storages].nil? ||
31
35
  opts[:delete_raw_storages].include?(storage_key)
32
36
  end
@@ -1,16 +1,17 @@
1
1
  class Shrine
2
2
  module Plugins
3
- # The `determine_mime_type` plugin stores the actual MIME type of the
4
- # uploaded file.
3
+ # The `determine_mime_type` plugin allows you to determine and store the
4
+ # actual MIME type of the file analyzed from file content.
5
5
  #
6
6
  # plugin :determine_mime_type
7
7
  #
8
- # By default the UNIX [file] utility is used to determine the MIME type, but
9
- # you can change it:
8
+ # By default the UNIX [file] utility is used to determine the MIME type,
9
+ # and the result is automatically written to the `mime_type` metadata
10
+ # field. You can choose a different built-in MIME type analyzer:
10
11
  #
11
12
  # plugin :determine_mime_type, analyzer: :filemagic
12
13
  #
13
- # The plugin accepts the following analyzers:
14
+ # The following analyzers are accepted:
14
15
  #
15
16
  # :file
16
17
  # : (Default). Uses the [file] utility to determine the MIME type from file
@@ -33,17 +34,32 @@ class Shrine
33
34
  # guaranteed to return the actual MIME type of the file.
34
35
  #
35
36
  # :default
36
- # : Uses the default way of extracting the MIME type, and that is from the
37
- # "Content-Type" request header, which might not hold the actual MIME type
38
- # of the file.
37
+ # : Uses the default way of extracting the MIME type, and that is reading
38
+ # the `#content_type` attribute of the IO object, which might not hold
39
+ # the actual MIME type of the file.
39
40
  #
40
- # Not all analyzers can recognize all types of files. For those cases you
41
- # can build your own analyzer, where you can reuse built-in analyzers:
41
+ # A single analyzer is not going to properly recognize all types of files,
42
+ # so you can build your own custom analyzer for your requirements, where
43
+ # you can combine the built-in analyzers. For example, if you want to
44
+ # correctly determine MIME type of .css, .js, .json, .csv, .xml, or similar
45
+ # text-based files, you can combine `file` and `mime_types` analyzers:
42
46
  #
43
47
  # plugin :determine_mime_type, analyzer: ->(io, analyzers) do
44
- # analyzers[:mimemagic].call(io) || analyzers[:file].call(io)
48
+ # mime_type = analyzers[:file].call(io)
49
+ # mime_type = analyzers[:mime_types].call(io) if mime_type == "text/plain"
50
+ # mime_type
45
51
  # end
46
52
  #
53
+ # You can also use methods for determining the MIME type directly:
54
+ #
55
+ # # or YourUploader.determine_mime_type(io)
56
+ # Shrine.determine_mime_type(io) # calls the defined analyzer
57
+ # #=> "image/jpeg"
58
+ #
59
+ # # or YourUploader.mime_type_analyzers
60
+ # Shrine.mime_type_analyzers[:file].call(io) # calls a built-in analyzer
61
+ # #=> "image/jpeg"
62
+ #
47
63
  # [file]: http://linux.die.net/man/1/file
48
64
  # [Windows equivalent]: http://gnuwin32.sourceforge.net/packages/file.htm
49
65
  # [ruby-filemagic]: https://github.com/blackwinter/ruby-filemagic
@@ -52,24 +68,15 @@ class Shrine
52
68
  module DetermineMimeType
53
69
  def self.configure(uploader, opts = {})
54
70
  uploader.opts[:mime_type_analyzer] = opts.fetch(:analyzer, uploader.opts.fetch(:mime_type_analyzer, :file))
55
- uploader.opts[:mime_type_magic_header] = opts.fetch(:magic_header, uploader.opts.fetch(:mime_type_magic_header, MAGIC_NUMBER))
56
71
  end
57
72
 
58
- # How many bytes we need to read in order to determine the MIME type.
59
- MAGIC_NUMBER = 256 * 1024
60
-
61
- module InstanceMethods
62
- private
63
-
64
- # If a Shrine::UploadedFile was given, it returns its MIME type, since
65
- # that value was already determined by this analyzer. Otherwise it calls
66
- # a built-in analyzer or a custom one.
67
- def extract_mime_type(io)
73
+ module ClassMethods
74
+ # Determines the MIME type of the IO object by calling the specified
75
+ # analyzer.
76
+ def determine_mime_type(io)
68
77
  analyzer = opts[:mime_type_analyzer]
69
- return super if analyzer == :default
70
-
71
78
  analyzer = mime_type_analyzers[analyzer] if analyzer.is_a?(Symbol)
72
- args = [io, mime_type_analyzers].take(analyzer.arity.abs)
79
+ args = [io, mime_type_analyzers].take(analyzer.arity.abs)
73
80
 
74
81
  mime_type = analyzer.call(*args)
75
82
  io.rewind
@@ -77,15 +84,59 @@ class Shrine
77
84
  mime_type
78
85
  end
79
86
 
87
+ # Returns a hash of built-in MIME type analyzers, where keys are
88
+ # analyzer names and values are `#call`-able objects which accepts the
89
+ # IO object.
80
90
  def mime_type_analyzers
81
- Hash.new { |hash, key| method(:"_extract_mime_type_with_#{key}") }
91
+ @mime_type_analyzers ||= MimeTypeAnalyzer::SUPPORTED_TOOLS.inject({}) do |hash, tool|
92
+ hash.merge!(tool => MimeTypeAnalyzer.new(tool).method(:call))
93
+ end
82
94
  end
95
+ end
83
96
 
84
- def _extract_mime_type_with_file(io)
97
+ module InstanceMethods
98
+ private
99
+
100
+ # Calls default behaviour when :default analyzer was specified, which
101
+ # just reads the `#content_type` attribute, otherwise uses the specified
102
+ # MIME type analyzer.
103
+ def extract_mime_type(io)
104
+ if opts[:mime_type_analyzer] == :default
105
+ super
106
+ else
107
+ self.class.determine_mime_type(io)
108
+ end
109
+ end
110
+
111
+ # Returns a hash of built-in MIME type analyzers.
112
+ def mime_type_analyzers
113
+ self.class.mime_type_analyzers
114
+ end
115
+ end
116
+
117
+ class MimeTypeAnalyzer
118
+ SUPPORTED_TOOLS = [:file, :filemagic, :mimemagic, :mime_types]
119
+ MAGIC_NUMBER = 256 * 1024
120
+
121
+ def initialize(tool)
122
+ raise ArgumentError, "unsupported mime type analyzer tool: #{tool}" unless SUPPORTED_TOOLS.include?(tool)
123
+
124
+ @tool = tool
125
+ end
126
+
127
+ def call(io)
128
+ mime_type = send(:"extract_with_#{@tool}", io)
129
+ io.rewind
130
+ mime_type
131
+ end
132
+
133
+ private
134
+
135
+ def extract_with_file(io)
85
136
  require "open3"
86
137
 
87
138
  cmd = ["file", "--mime-type", "--brief", "-"]
88
- options = {stdin_data: magic_header(io), binmode: true}
139
+ options = {stdin_data: io.read(MAGIC_NUMBER), binmode: true}
89
140
 
90
141
  begin
91
142
  stdout, stderr, status = Open3.capture3(*cmd, options)
@@ -99,26 +150,24 @@ class Shrine
99
150
  stdout.strip
100
151
  end
101
152
 
102
- def _extract_mime_type_with_mimemagic(io)
103
- require "mimemagic"
104
-
105
- mime = MimeMagic.by_magic(io)
106
- io.rewind
107
-
108
- mime.type if mime
109
- end
110
-
111
- def _extract_mime_type_with_filemagic(io)
153
+ def extract_with_filemagic(io)
112
154
  require "filemagic"
113
155
 
114
156
  filemagic = FileMagic.new(FileMagic::MAGIC_MIME_TYPE)
115
- mime_type = filemagic.buffer(magic_header(io))
157
+ mime_type = filemagic.buffer(io.read(MAGIC_NUMBER))
116
158
  filemagic.close
117
159
 
118
160
  mime_type
119
161
  end
120
162
 
121
- def _extract_mime_type_with_mime_types(io)
163
+ def extract_with_mimemagic(io)
164
+ require "mimemagic"
165
+
166
+ mime = MimeMagic.by_magic(io)
167
+ mime.type if mime
168
+ end
169
+
170
+ def extract_with_mime_types(io)
122
171
  begin
123
172
  require "mime/types/columnar"
124
173
  rescue LoadError
@@ -131,12 +180,15 @@ class Shrine
131
180
  end
132
181
  end
133
182
 
134
- def magic_header(io)
135
- content = io.read(opts[:mime_type_magic_header])
136
- io.rewind
137
- content
183
+ def extract_filename(io)
184
+ if io.respond_to?(:original_filename)
185
+ io.original_filename
186
+ elsif io.respond_to?(:path)
187
+ File.basename(io.path)
188
+ end
138
189
  end
139
190
  end
191
+
140
192
  end
141
193
 
142
194
  register_plugin(:determine_mime_type, DetermineMimeType)
@@ -99,10 +99,12 @@ class Shrine
99
99
  #
100
100
  # plugin :direct_upload, presign_options: ->(request) do
101
101
  # filename = request.params["filename"]
102
+ # content_type = Rack::Mime.mime_type(File.extname(filename))
102
103
  #
103
104
  # {
104
105
  # content_length_range: 0..(10*1024*1024), # limit filesize to 10MB
105
106
  # content_disposition: "attachment; filename=\"#{filename}\"", # download with original filename
107
+ # content_type: content_type, # set correct content type
106
108
  # }
107
109
  # end
108
110
  #
@@ -230,7 +232,7 @@ class Shrine
230
232
  context = {action: :cache, phase: :cache}
231
233
 
232
234
  if name != "upload"
233
- warn "The \"POST /:storage/:name\" route of the direct_upload Shrine plugin is deprecated, and it will be removed in Shrine 3. Use \"POST /:storage/upload\" instead."
235
+ Shrine.deprecation("The \"POST /:storage/:name\" route of the direct_upload plugin is deprecated, and it will be removed in Shrine 3. Use \"POST /:storage/upload\" instead.")
234
236
  context[:name] = name
235
237
  end
236
238
 
@@ -41,13 +41,22 @@ class Shrine
41
41
  # prompted to download the file when visiting the download URL.
42
42
  # The default is "inline".
43
43
  #
44
- # This plugin is also suitable on Heroku when using FileSystem storage for
45
- # cache. On Heroku files cannot be stored to the "public" folder but rather
46
- # to the "tmp" folder, which means that by default it's not possible to
47
- # show the URL to the cached file. The download endpoint generates the URL
48
- # to any file, regardless of its location.
44
+ # Note that streaming the file through your app might impact the request
45
+ # throughput of your app, because on most popular web servers (Puma,
46
+ # Unicorn, Passenger) workers handling this endpoint will not be able to
47
+ # serve new requests until the client has fully downloaded the response
48
+ # body.
49
+ #
50
+ # To prevent download endpoint from impacting your request throughput, use
51
+ # a web server that handles streaming responses and slow clients well, like
52
+ # [Thin], [Rainbows] or any other [EventMachine]-based web server that
53
+ # implements `async.callback`.
49
54
  #
50
55
  # [Roda]: https://github.com/jeremyevans/roda
56
+ # [Thin]: https://github.com/macournoyer/thin
57
+ # [Rainbows]: https://rubygems.org/gems/rainbows
58
+ # [Reel]: https://github.com/celluloid/reel
59
+ # [EventMachine]: https://github.com/eventmachine
51
60
  module DownloadEndpoint
52
61
  def self.configure(uploader, opts = {})
53
62
  uploader.opts[:download_endpoint_storages] = opts.fetch(:storages, uploader.opts[:download_endpoint_storages])
@@ -1,5 +1,4 @@
1
1
  require "logger"
2
- require "benchmark"
3
2
  require "json"
4
3
 
5
4
  class Shrine
@@ -157,9 +156,10 @@ class Shrine
157
156
  end
158
157
 
159
158
  def benchmark
160
- result = nil
161
- duration = Benchmark.realtime { result = yield }
162
- [result, duration]
159
+ start = Time.now
160
+ result = yield
161
+ finish = Time.now
162
+ [result, finish - start]
163
163
  end
164
164
  end
165
165
  end
@@ -0,0 +1,61 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The `metadata_attributes` plugin allows you to sync attachment metadata
4
+ # to additional record attributes.
5
+ #
6
+ # plugin :metadata_values
7
+ #
8
+ # It provides `Attacher.metadata_attributes` method which allows you to
9
+ # specify mappings between metadata fields on the attachment and attribute
10
+ # names on the record.
11
+ #
12
+ # Attacher.metadata_attributes :size => :size, :mime_type => :type
13
+ #
14
+ # The above configuration will sync `size` metadata field to
15
+ # `<attachment>_size` record attribute, and `mime_type` metadata field to
16
+ # `<attachment>_type` record attribute.
17
+ #
18
+ # user.avatar = image
19
+ # user.avatar.metadata["size"] #=> 95724
20
+ # user.avatar_size #=> 95724
21
+ # user.avatar.metadata["mime_type"] #=> "image/jpeg"
22
+ # user.avatar_type #=> "image/jpeg"
23
+ #
24
+ # user.avatar = nil
25
+ # user.avatar_size #=> nil
26
+ # user.avatar_type #=> nil
27
+ #
28
+ # If any corresponding metadata attribute doesn't exist on the record, that
29
+ # metadata sync will be silently skipped.
30
+ module MetadataAttributes
31
+ def self.configure(uploader)
32
+ uploader.opts[:metadata_attributes_mappings] ||= {}
33
+ end
34
+
35
+ module AttacherClassMethods
36
+ def metadata_attributes(mappings)
37
+ shrine_class.opts[:metadata_attributes_mappings].merge!(mappings)
38
+ end
39
+ end
40
+
41
+ module AttacherMethods
42
+ def assign(value)
43
+ super
44
+ cached_file = get
45
+
46
+ shrine_class.opts[:metadata_attributes_mappings].each do |source, destination|
47
+ next unless record.respond_to?(:"#{name}_#{destination}=")
48
+
49
+ if cached_file
50
+ record.send(:"#{name}_#{destination}=", cached_file.metadata[source.to_s])
51
+ else
52
+ record.send(:"#{name}_#{destination}=", nil)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ register_plugin(:metadata_attributes, MetadataAttributes)
60
+ end
61
+ end
@@ -1,4 +1,4 @@
1
- warn "The migration_helpers Shrine plugin is deprecated and will be removed in Shrine 3. Attacher#cached? and Attacher#stored? have been moved to base."
1
+ Shrine.deprecation("The migration_helpers plugin is deprecated and will be removed in Shrine 3. Attacher#cached? and Attacher#stored? have been moved to base.")
2
2
 
3
3
  class Shrine
4
4
  module Plugins
@@ -11,37 +11,44 @@ class Shrine
11
11
  # `multipart/form-data` parameter encoding, Rack converts the uploaded file
12
12
  # to a hash.
13
13
  #
14
- # params[:file] #=>
14
+ # file_hash #=>
15
15
  # # {
16
- # # name: "file"
17
- # # filename: "cats.png",
18
- # # type: "image/png",
19
- # # tempfile: #<Tempfile:/var/folders/3n/3asd/-Tmp-/RackMultipart201-1476-nfw2-0>,
20
- # # head: "Content-Disposition: form-data; ...",
16
+ # # :name => "file",
17
+ # # :filename => "cats.png",
18
+ # # :type => "image/png",
19
+ # # :tempfile => #<Tempfile:/var/folders/3n/3asd/-Tmp-/RackMultipart201-1476-nfw2-0>,
20
+ # # :head => "Content-Disposition: form-data; ...",
21
21
  # # }
22
22
  #
23
23
  # Since Shrine only accepts IO objects, you would normally need to fetch
24
24
  # the `:tempfile` object and pass it directly. This plugin enables the
25
- # uploader and attacher to accept the Rack uploaded file hash as a whole,
26
- # which is then internally converted into an IO object.
25
+ # attacher to accept the Rack uploaded file hash directly, which is
26
+ # convenient when doing mass attribute assignment.
27
27
  #
28
- # uploader.upload(params[:file])
28
+ # user.avatar = file_hash
29
29
  # # or
30
- # attacher.assign(params[:file])
31
- # # or
32
- # user.avatar = params[:file]
30
+ # attacher.assign(file_hash)
33
31
  #
34
- # This especially convenient when doing mass attribute assignment with
35
- # request parameters. It will also copy the received file information into
36
- # metadata.
32
+ # Internally the Rack uploaded file hash will be converted into an IO
33
+ # object using `Shrine.rack_file`, which you can also use directly:
37
34
  #
38
- # uploaded_file = uploader.upload(params[:file])
39
- # uploaded_file.original_filename #=> "cats.png"
40
- # uploaded_file.mime_type #=> "image/png"
35
+ # # or YourUploader.rack_file(file_hash)
36
+ # io = Shrine.rack_file(file_hash)
37
+ # io.original_filename #=> "cats.png"
38
+ # io.content_type #=> "image/png"
39
+ # io.size #=> 58342
41
40
  #
42
41
  # Note that this plugin is not needed in Rails applications, as Rails
43
- # already wraps Rack uploaded files in `ActionDispatch::Http::UploadedFile`.
42
+ # already wraps the Rack uploaded file hash into an
43
+ # `ActionDispatch::Http::UploadedFile` object.
44
44
  module RackFile
45
+ module ClassMethods
46
+ # Accepts a Rack uploaded file hash and wraps it in an IO object.
47
+ def rack_file(hash)
48
+ UploadedFile.new(hash)
49
+ end
50
+ end
51
+
45
52
  module InstanceMethods
46
53
  # If `io` is a Rack uploaded file hash, converts it to an IO-like
47
54
  # object and calls `super`.
@@ -62,7 +69,8 @@ class Shrine
62
69
  # hash, otherwise returns the value unchanged.
63
70
  def convert_rack_file(value)
64
71
  if rack_file?(value)
65
- UploadedFile.new(value)
72
+ Shrine.deprecation("Passing a Rack uploaded file hash to Shrine#upload is deprecated, use Shrine.rack_file to convert the Rack file hash into an IO object.")
73
+ self.class.rack_file(value)
66
74
  else
67
75
  value
68
76
  end
@@ -75,26 +83,42 @@ class Shrine
75
83
  end
76
84
  end
77
85
 
86
+ module AttacherMethods
87
+ # Checks whether a file is a Rack file hash, and in that case wraps the
88
+ # hash in an IO-like object.
89
+ def assign(value)
90
+ if rack_file?(value)
91
+ assign(shrine_class.rack_file(value))
92
+ else
93
+ super
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ # Returns whether a given value is a Rack uploaded file hash, by
100
+ # checking whether it's a hash with `:tempfile` and `:name` keys.
101
+ def rack_file?(value)
102
+ value.is_a?(Hash) && value.key?(:tempfile) && value.key?(:name)
103
+ end
104
+ end
105
+
78
106
  # This is used to wrap the Rack hash into an IO-like object which Shrine
79
107
  # can upload.
80
108
  class UploadedFile
81
- attr_reader :original_filename, :content_type
82
- attr_accessor :tempfile
109
+ attr_reader :tempfile, :original_filename, :content_type
110
+ alias :to_io :tempfile
83
111
 
84
- def initialize(tempfile:, filename: nil, type: nil, **)
85
- @tempfile = tempfile
86
- @original_filename = filename
87
- @content_type = type
112
+ def initialize(hash)
113
+ @tempfile = hash[:tempfile]
114
+ @original_filename = hash[:filename]
115
+ @content_type = hash[:type]
88
116
  end
89
117
 
90
118
  def path
91
119
  @tempfile.path
92
120
  end
93
121
 
94
- def to_io
95
- @tempfile
96
- end
97
-
98
122
  extend Forwardable
99
123
  delegate Shrine::IO_METHODS.keys => :@tempfile
100
124
  end