shrine 1.4.2 → 2.0.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +236 -234
  3. data/doc/changing_location.md +6 -4
  4. data/doc/creating_storages.md +4 -4
  5. data/doc/design.md +223 -0
  6. data/doc/migrating_storage.md +6 -11
  7. data/doc/regenerating_versions.md +22 -40
  8. data/lib/shrine.rb +60 -77
  9. data/lib/shrine/plugins/activerecord.rb +37 -14
  10. data/lib/shrine/plugins/background_helpers.rb +1 -0
  11. data/lib/shrine/plugins/backgrounding.rb +49 -37
  12. data/lib/shrine/plugins/backup.rb +6 -4
  13. data/lib/shrine/plugins/cached_attachment_data.rb +5 -5
  14. data/lib/shrine/plugins/data_uri.rb +9 -9
  15. data/lib/shrine/plugins/default_storage.rb +4 -4
  16. data/lib/shrine/plugins/default_url.rb +7 -1
  17. data/lib/shrine/plugins/default_url_options.rb +1 -1
  18. data/lib/shrine/plugins/delete_promoted.rb +2 -2
  19. data/lib/shrine/plugins/delete_raw.rb +4 -4
  20. data/lib/shrine/plugins/determine_mime_type.rb +50 -43
  21. data/lib/shrine/plugins/direct_upload.rb +10 -20
  22. data/lib/shrine/plugins/download_endpoint.rb +16 -13
  23. data/lib/shrine/plugins/dynamic_storage.rb +4 -12
  24. data/lib/shrine/plugins/included.rb +6 -19
  25. data/lib/shrine/plugins/keep_files.rb +4 -4
  26. data/lib/shrine/plugins/logging.rb +4 -4
  27. data/lib/shrine/plugins/migration_helpers.rb +37 -34
  28. data/lib/shrine/plugins/moving.rb +19 -32
  29. data/lib/shrine/plugins/parallelize.rb +5 -5
  30. data/lib/shrine/plugins/pretty_location.rb +2 -6
  31. data/lib/shrine/plugins/remote_url.rb +31 -43
  32. data/lib/shrine/plugins/remove_attachment.rb +5 -5
  33. data/lib/shrine/plugins/remove_invalid.rb +1 -1
  34. data/lib/shrine/plugins/restore_cached_data.rb +4 -10
  35. data/lib/shrine/plugins/sequel.rb +46 -21
  36. data/lib/shrine/plugins/store_dimensions.rb +19 -20
  37. data/lib/shrine/plugins/upload_options.rb +11 -9
  38. data/lib/shrine/plugins/validation_helpers.rb +3 -3
  39. data/lib/shrine/plugins/versions.rb +18 -3
  40. data/lib/shrine/storage/file_system.rb +9 -11
  41. data/lib/shrine/storage/linter.rb +1 -7
  42. data/lib/shrine/storage/s3.rb +25 -19
  43. data/lib/shrine/version.rb +3 -3
  44. data/shrine.gemspec +13 -3
  45. metadata +28 -9
  46. data/lib/shrine/plugins/delete_uploaded.rb +0 -3
  47. data/lib/shrine/plugins/keep_location.rb +0 -46
  48. data/lib/shrine/plugins/restore_cached.rb +0 -3
@@ -7,10 +7,11 @@ class Shrine
7
7
  #
8
8
  # plugin :activerecord
9
9
  #
10
- # Now whenever an "attachment" module is included, additional callbacks are
11
- # added to the model:
10
+ # ## Callbacks
12
11
  #
13
- # * `before_save` -- Currently only used by the recache plugin.
12
+ # Now the attachment module will add additional callbacks to the model:
13
+ #
14
+ # * `before_save` -- Used by the recache plugin.
14
15
  # * `after_commit on: [:create, :update]` -- Promotes the attachment, deletes replaced ones.
15
16
  # * `after_commit on: [:destroy]` -- Deletes the attachment.
16
17
  #
@@ -25,8 +26,27 @@ class Shrine
25
26
  # `after_commit` callbacks won't get called, so in order to test uploading
26
27
  # you should first disable transactions for those tests.
27
28
  #
28
- # If you want to put some parts of this lifecycle into a background job,
29
- # see the backgrounding plugin.
29
+ # If you want to put promoting/deleting into a background job, see the
30
+ # backgrounding plugin.
31
+ #
32
+ # Since attaching first saves the record with a cached attachment, then
33
+ # saves again with a stored attachment, you can detect this in callbacks:
34
+ #
35
+ # class User < ActiveRecord::Base
36
+ # include ImageUploader[:avatar]
37
+ #
38
+ # before_save do
39
+ # if avatar_data_changed? && avatar_attacher.cached?
40
+ # # cached
41
+ # end
42
+ #
43
+ # if avatar_data_changed? && avatar_attacher.stored?
44
+ # # promoted
45
+ # end
46
+ # end
47
+ # end
48
+ #
49
+ # ## Validations
30
50
  #
31
51
  # Additionally, any Shrine validation errors will be added to
32
52
  # ActiveRecord's errors upon validation. If you want to validate presence
@@ -36,13 +56,13 @@ class Shrine
36
56
  # include ImageUploader[:avatar]
37
57
  # validates_presence_of :avatar
38
58
  # end
39
- #
40
- # [optimistic locking]: http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
41
59
  module Activerecord
42
60
  module AttachmentMethods
43
61
  def included(model)
44
62
  super
45
63
 
64
+ return unless model < ::ActiveRecord::Base
65
+
46
66
  model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
47
67
  validate do
48
68
  #{@name}_attacher.errors.each do |message|
@@ -75,15 +95,18 @@ class Shrine
75
95
  module AttacherMethods
76
96
  private
77
97
 
78
- # Updates the current attachment with the new one, unless the current
79
- # attachment has changed.
80
- def update(uploaded_file)
81
- if record.send("#{name}_data") == record.reload.send("#{name}_data")
82
- record.send("#{name}_data=", uploaded_file.to_json)
83
- record.save(validate: false)
84
- end
98
+ # Proceeds with updating the record unless the attachment has changed.
99
+ def swap(uploaded_file)
100
+ return if record.send(:"#{name}_data") != record.reload.send(:"#{name}_data")
101
+ super
85
102
  rescue ::ActiveRecord::RecordNotFound
86
103
  end
104
+
105
+ # Saves the record after assignment, skipping validations.
106
+ def update(uploaded_file)
107
+ super
108
+ record.save(validate: false)
109
+ end
87
110
  end
88
111
  end
89
112
 
@@ -1,2 +1,3 @@
1
+ warn "The background_helpers Shrine plugin has been renamed to \"backgrounding\". Loading the plugin through \"background_helpers\" will stop working in Shrine 3."
1
2
  require "shrine/plugins/backgrounding"
2
3
  Shrine::Plugins.register_plugin(:background_helpers, Shrine::Plugins::Backgrounding)
@@ -32,19 +32,30 @@ class Shrine
32
32
  #
33
33
  # Internally these methods will resolve all necessary objects, do the
34
34
  # promotion/deletion, and in case of promotion update the record with the
35
- # stored attachment. Concurrency issues, like record being deleted or
36
- # attachment being changed, are handled automatically.
35
+ # stored attachment.
37
36
  #
38
37
  # The examples above used Sidekiq, but obviously you can just as well use
39
38
  # any other backgrounding library. This setup will work globally for all
40
39
  # uploaders.
41
40
  #
42
- # Both methods return the record (if it exists and the action didn't
41
+ # The backgrounding plugin affects the `Shrine::Attacher` in a way that
42
+ # `#_promote` and `#_delete` spawn background jobs, while `#promote` and
43
+ # `#delete!` are always synchronous:
44
+ #
45
+ # # asynchronous (spawn background jobs)
46
+ # attacher._promote
47
+ # attacher._delete(attachment)
48
+ #
49
+ # # synchronous
50
+ # attacher.promote
51
+ # attacher.delete!(attachment)
52
+ #
53
+ # Both methods return the `Shrine::Attacher` instance (if the action didn't
43
54
  # abort), so you can use it to do additional actions:
44
55
  #
45
56
  # def perform(data)
46
- # record = Shrine::Attacher.promote(data)
47
- # record.update(published: true) if record.is_a?(Post)
57
+ # attacher = Shrine::Attacher.promote(data)
58
+ # attacher.record.update(published: true) if attacher && attacher.record.is_a?(Post)
48
59
  # end
49
60
  #
50
61
  # You can also write custom background jobs with `Attacher.dump` and
@@ -52,6 +63,7 @@ class Shrine
52
63
  #
53
64
  # class User < Sequel::Model
54
65
  # def after_commit
66
+ # super
55
67
  # if some_condition
56
68
  # data = Shrine::Attacher.dump(avatar_attacher)
57
69
  # SomethingJob.perform_async(data)
@@ -69,7 +81,7 @@ class Shrine
69
81
  #
70
82
  # If you're generating versions, and you want to process some versions in
71
83
  # the foreground before kicking off a background job, you can use the
72
- # `recache` plugin.
84
+ # recache plugin.
73
85
  module Backgrounding
74
86
  module AttacherClassMethods
75
87
  # If block is passed in, stores it to be called on promotion. Otherwise
@@ -79,12 +91,13 @@ class Shrine
79
91
  shrine_class.opts[:backgrounding_promote] = block
80
92
  else
81
93
  attacher = load(data)
82
- cached_file = attacher.uploaded_file(data["uploaded_file"])
83
- phase = data["phase"].to_sym
94
+ cached_file = attacher.uploaded_file(data["attachment"])
95
+ phase = data["phase"].to_sym if data["phase"]
84
96
 
97
+ return if cached_file != attacher.get
85
98
  attacher.promote(cached_file, phase: phase) or return
86
99
 
87
- attacher.record
100
+ attacher
88
101
  end
89
102
  end
90
103
 
@@ -95,23 +108,18 @@ class Shrine
95
108
  shrine_class.opts[:backgrounding_delete] = block
96
109
  else
97
110
  attacher = load(data)
98
- uploaded_file = attacher.uploaded_file(data["uploaded_file"])
99
- context = {name: attacher.name, record: attacher.record, phase: data["phase"].to_sym}
111
+ uploaded_file = attacher.uploaded_file(data["attachment"])
112
+ phase = data["phase"].to_sym if data["phase"]
100
113
 
101
- attacher.store.delete(uploaded_file, context)
114
+ attacher.delete!(uploaded_file, phase: phase)
102
115
 
103
- attacher.record
116
+ attacher
104
117
  end
105
118
  end
106
119
 
107
- # Dumps all the information about the attacher in a serializable hash
108
- # suitable for passing as an argument to background jobs.
120
+ # Delegates to `Attacher#dump`.
109
121
  def dump(attacher)
110
- {
111
- "uploaded_file" => attacher.get && attacher.get.to_json,
112
- "record" => [attacher.record.class.to_s, attacher.record.id],
113
- "attachment" => attacher.name.to_s,
114
- }
122
+ attacher.dump
115
123
  end
116
124
 
117
125
  # Loads the data created by #dump, resolving the record and returning
@@ -122,7 +130,7 @@ class Shrine
122
130
  record = find_record(record_class, record_id) ||
123
131
  record_class.new.tap { |object| object.id = record_id }
124
132
 
125
- name = data["attachment"]
133
+ name = data["name"]
126
134
  attacher = record.send("#{name}_attacher")
127
135
 
128
136
  attacher
@@ -132,38 +140,42 @@ class Shrine
132
140
  module AttacherMethods
133
141
  # Calls the promoting block (if registered) with a serializable data
134
142
  # hash.
135
- def _promote
143
+ def _promote(uploaded_file = get, phase: nil)
136
144
  if background_promote = shrine_class.opts[:backgrounding_promote]
137
- data = self.class.dump(self).merge("phase" => "store")
138
- instance_exec(data, &background_promote) if promote?(get)
145
+ data = self.class.dump(self).merge(
146
+ "attachment" => uploaded_file.to_json,
147
+ "phase" => (phase.to_s if phase),
148
+ )
149
+ instance_exec(data, &background_promote)
139
150
  else
140
151
  super
141
152
  end
142
153
  end
143
154
 
144
- # Returns early if attachments don't match.
145
- def promote(cached_file, *)
146
- return if cached_file != get
147
- super
148
- end
149
-
150
- private
151
-
152
155
  # Calls the deleting block (if registered) with a serializable data
153
156
  # hash.
154
- def delete!(uploaded_file, phase:)
157
+ def _delete(uploaded_file, phase: nil)
155
158
  if background_delete = shrine_class.opts[:backgrounding_delete]
156
159
  data = self.class.dump(self).merge(
157
- "uploaded_file" => uploaded_file.to_json,
158
- "phase" => phase.to_s,
160
+ "attachment" => uploaded_file.to_json,
161
+ "phase" => (phase.to_s if phase),
159
162
  )
160
163
  instance_exec(data, &background_delete)
161
-
162
164
  uploaded_file
163
165
  else
164
- super(uploaded_file, phase: phase)
166
+ super
165
167
  end
166
168
  end
169
+
170
+ # Dumps all the information about the attacher in a serializable hash
171
+ # suitable for passing as an argument to background jobs.
172
+ def dump
173
+ {
174
+ "attachment" => (get && get.to_json),
175
+ "record" => [record.class.to_s, record.id.to_s],
176
+ "name" => name.to_s,
177
+ }
178
+ end
167
179
  end
168
180
  end
169
181
 
@@ -23,9 +23,11 @@ class Shrine
23
23
  # you can set `delete: false` until you manually back up the existing
24
24
  # stored files.
25
25
  module Backup
26
- def self.configure(uploader, storage:, delete: true)
27
- uploader.opts[:backup_storage] = storage
28
- uploader.opts[:backup_delete] = delete
26
+ def self.configure(uploader, opts = {})
27
+ uploader.opts[:backup_storage] = opts.fetch(:storage, uploader.opts[:backup_storage])
28
+ uploader.opts[:backup_delete] = opts.fetch(:delete, uploader.opts.fetch(:backup_delete, true))
29
+
30
+ raise Error, "The :storage option is required for backup plugin" if uploader.opts[:backup_storage].nil?
29
31
  end
30
32
 
31
33
  module AttacherMethods
@@ -67,7 +69,7 @@ class Shrine
67
69
 
68
70
  # Deleted the stored file from the backup storage.
69
71
  def delete_backup!(deleted_file)
70
- delete!(backup_file(deleted_file), phase: :backup)
72
+ _delete(backup_file(deleted_file), phase: :backup)
71
73
  end
72
74
 
73
75
  def backup_store
@@ -20,16 +20,16 @@ class Shrine
20
20
  # both cached and stored files). This keeps Rails logs cleaner.
21
21
  module CachedAttachmentData
22
22
  module AttachmentMethods
23
- def initialize(name)
23
+ def initialize(*)
24
24
  super
25
25
 
26
26
  module_eval <<-RUBY, __FILE__, __LINE__ + 1
27
- def cached_#{name}_data
28
- #{name}_attacher.read_cached
27
+ def cached_#{@name}_data
28
+ #{@name}_attacher.read_cached
29
29
  end
30
30
 
31
- def cached_#{name}_data=(value)
32
- #{name}_attacher.assign(value)
31
+ def cached_#{@name}_data=(value)
32
+ #{@name}_attacher.assign(value)
33
33
  end
34
34
  RUBY
35
35
  end
@@ -45,22 +45,22 @@ class Shrine
45
45
  DEFAULT_CONTENT_TYPE = "text/plain"
46
46
  DATA_URI_REGEXP = /\Adata:([-\w.+]+\/[-\w.+]+)?(;base64)?,(.*)\z/m
47
47
 
48
- def self.configure(uploader, filename: nil, error_message: DEFAULT_ERROR_MESSAGE)
49
- uploader.opts[:data_uri_filename] = filename
50
- uploader.opts[:data_uri_error_message] = error_message
48
+ def self.configure(uploader, opts = {})
49
+ uploader.opts[:data_uri_filename] = opts.fetch(:filename, uploader.opts[:data_uri_filename])
50
+ uploader.opts[:data_uri_error_message] = opts.fetch(:error_message, uploader.opts[:data_uri_error_message])
51
51
  end
52
52
 
53
53
  module AttachmentMethods
54
- def initialize(name)
54
+ def initialize(*)
55
55
  super
56
56
 
57
57
  module_eval <<-RUBY, __FILE__, __LINE__ + 1
58
- def #{name}_data_uri=(uri)
59
- #{name}_attacher.data_uri = uri
58
+ def #{@name}_data_uri=(uri)
59
+ #{@name}_attacher.data_uri = uri
60
60
  end
61
61
 
62
- def #{name}_data_uri
63
- #{name}_attacher.data_uri
62
+ def #{@name}_data_uri
63
+ #{@name}_attacher.data_uri
64
64
  end
65
65
  RUBY
66
66
  end
@@ -82,7 +82,7 @@ class Shrine
82
82
 
83
83
  assign DataFile.new(content, content_type: content_type, filename: filename)
84
84
  else
85
- message = shrine_class.opts[:data_uri_error_message]
85
+ message = shrine_class.opts[:data_uri_error_message] || DEFAULT_ERROR_MESSAGE
86
86
  message = message.call(uri) if message.respond_to?(:call)
87
87
  errors.replace [message]
88
88
  @data_uri = uri
@@ -12,9 +12,9 @@ class Shrine
12
12
  #
13
13
  # plugin :default_storage, store: ->(record, name) { :"store_#{record.username}" }
14
14
  module DefaultStorage
15
- def self.configure(uploader, cache: nil, store: nil)
16
- uploader.opts[:default_storage_cache] = cache
17
- uploader.opts[:default_storage_store] = store
15
+ def self.configure(uploader, opts = {})
16
+ uploader.opts[:default_storage_cache] = opts.fetch(:cache, uploader.opts[:default_storage_cache])
17
+ uploader.opts[:default_storage_store] = opts.fetch(:store, uploader.opts[:default_storage_store])
18
18
  end
19
19
 
20
20
  module AttacherMethods
@@ -29,7 +29,7 @@ class Shrine
29
29
  options[:store] = store
30
30
  end
31
31
 
32
- super(record, name, **options)
32
+ super
33
33
  end
34
34
  end
35
35
  end
@@ -20,10 +20,16 @@ class Shrine
20
20
  end
21
21
 
22
22
  module AttacherMethods
23
+ def url(**options)
24
+ super || default_url(**options)
25
+ end
26
+
23
27
  private
24
28
 
25
29
  def default_url(**options)
26
- default_url_block.call(context.merge(options){|k,old,new|old})
30
+ if default_url_block
31
+ default_url_block.call(context.merge(options){|k,old,new|old})
32
+ end
27
33
  end
28
34
 
29
35
  def default_url_block
@@ -9,7 +9,7 @@ class Shrine
9
9
  # and the latter will always have precedence over default options.
10
10
  module DefaultUrlOptions
11
11
  def self.configure(uploader, **options)
12
- uploader.opts[:default_url_options] = options
12
+ uploader.opts[:default_url_options] = (uploader.opts[:default_url_options] || {}).merge(options)
13
13
  end
14
14
 
15
15
  module FileMethods
@@ -8,9 +8,9 @@ class Shrine
8
8
  # plugin :delete_promoted
9
9
  module DeletePromoted
10
10
  module AttacherMethods
11
- def promote(uploaded_file, *)
11
+ def promote(uploaded_file = get, **options)
12
12
  result = super
13
- delete!(uploaded_file, phase: :promote)
13
+ _delete(uploaded_file, phase: :promote)
14
14
  result
15
15
  end
16
16
  end
@@ -11,8 +11,8 @@ class Shrine
11
11
  #
12
12
  # plugin :delete_raw, storages: [:store]
13
13
  module DeleteRaw
14
- def self.configure(uploader, storages: nil)
15
- uploader.opts[:delete_uploaded_storages] = storages
14
+ def self.configure(uploader, opts = {})
15
+ uploader.opts[:delete_raw_storages] = opts.fetch(:storages, uploader.opts[:delete_raw_storages])
16
16
  end
17
17
 
18
18
  module InstanceMethods
@@ -27,8 +27,8 @@ class Shrine
27
27
  end
28
28
 
29
29
  def delete_uploaded?(io)
30
- opts[:delete_uploaded_storages].nil? ||
31
- opts[:delete_uploaded_storages].include?(storage_key)
30
+ opts[:delete_raw_storages].nil? ||
31
+ opts[:delete_raw_storages].include?(storage_key)
32
32
  end
33
33
  end
34
34
  end
@@ -15,12 +15,12 @@ class Shrine
15
15
  # :file
16
16
  # : (Default). Uses the [file] utility to determine the MIME type from file
17
17
  # contents. It is installed by default on most operating systems, but the
18
- # [Windows equivalent] you need to install separately.
18
+ # [Windows equivalent] needs to be installed separately.
19
19
  #
20
20
  # :filemagic
21
21
  # : Uses the [ruby-filemagic] gem to determine the MIME type from file
22
- # contents, using a similar MIME database as the `file` utility.
23
- # Unlike the `file` utility, ruby-filemagic should work on Windows.
22
+ # contents, using a similar MIME database as the `file` utility. Unlike
23
+ # the `file` utility, ruby-filemagic works on Windows without any setup.
24
24
  #
25
25
  # :mimemagic
26
26
  # : Uses the [mimemagic] gem to determine the MIME type from file contents.
@@ -32,10 +32,15 @@ class Shrine
32
32
  # *extension*. Note that unlike other solutions, this analyzer is not
33
33
  # guaranteed to return the actual MIME type of the file.
34
34
  #
35
- # If none of these quite suit your needs, you can use a custom analyzer:
35
+ # :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.
36
39
  #
37
- # plugin :determine_mime_type, analyzer: ->(io) do
38
- # # returns the extracted MIME type
40
+ # If none of these quite suit your needs, you can build a custom analyzer:
41
+ #
42
+ # plugin :determine_mime_type, analyzer: ->(io, analyzers) do
43
+ # analyzers[:mimemagic].call(io) || analyzers[:file].call(io)
39
44
  # end
40
45
  #
41
46
  # [file]: http://linux.die.net/man/1/file
@@ -44,22 +49,8 @@ class Shrine
44
49
  # [mimemagic]: https://github.com/minad/mimemagic
45
50
  # [mime-types]: https://github.com/mime-types/ruby-mime-types
46
51
  module DetermineMimeType
47
- def self.load_dependencies(uploader, analyzer: :file)
48
- case analyzer
49
- when :file then require "open3"
50
- when :filemagic then require "filemagic"
51
- when :mimemagic then require "mimemagic"
52
- when :mime_types
53
- begin
54
- require "mime/types/columnar"
55
- rescue LoadError
56
- require "mime/types"
57
- end
58
- end
59
- end
60
-
61
- def self.configure(uploader, analyzer: :file)
62
- uploader.opts[:mime_type_analyzer] = analyzer
52
+ def self.configure(uploader, opts = {})
53
+ uploader.opts[:mime_type_analyzer] = opts.fetch(:analyzer, :file)
63
54
  end
64
55
 
65
56
  # How many bytes we have to read to get the magic file header which
@@ -67,55 +58,71 @@ class Shrine
67
58
  MAGIC_NUMBER = 1024
68
59
 
69
60
  module InstanceMethods
61
+ private
62
+
70
63
  # If a Shrine::UploadedFile was given, it returns its MIME type, since
71
64
  # that value was already determined by this analyzer. Otherwise it calls
72
65
  # a built-in analyzer or a custom one.
73
66
  def extract_mime_type(io)
74
67
  analyzer = opts[:mime_type_analyzer]
68
+ return super if analyzer == :default
75
69
 
76
- mime_type = if io.respond_to?(:mime_type)
77
- io.mime_type
78
- elsif analyzer.is_a?(Symbol)
79
- send(:"_extract_mime_type_with_#{analyzer}", io)
80
- else
81
- analyzer.call(io)
82
- end
70
+ analyzer = mime_type_analyzers[analyzer] if analyzer.is_a?(Symbol)
71
+ args = [io, mime_type_analyzers].take(analyzer.arity.abs)
83
72
 
73
+ mime_type = analyzer.call(*args)
84
74
  io.rewind
85
75
 
86
76
  mime_type
87
77
  end
88
78
 
89
- private
79
+ def mime_type_analyzers
80
+ Hash.new { |hash, key| method(:"_extract_mime_type_with_#{key}") }
81
+ end
90
82
 
91
- # Uses the UNIX file utility to extract the MIME type. It does so only
92
- # if it's a file, because even though the utility accepts standard
93
- # input, it would mean that we have to read the whole file in memory.
94
83
  def _extract_mime_type_with_file(io)
95
- cmd = ["file", "--mime-type", "--brief"]
84
+ require "open3"
85
+
86
+ cmd = ["file", "--mime-type", "--brief", "--"]
96
87
 
97
88
  if io.respond_to?(:path)
98
- mime_type, _ = Open3.capture2(*cmd, io.path)
89
+ mime_type, * = Open3.capture2(*cmd, io.path)
99
90
  else
100
- mime_type, _ = Open3.capture2(*cmd, "-", stdin_data: io.read(MAGIC_NUMBER), binmode: true)
91
+ mime_type, * = Open3.capture2(*cmd, "-", stdin_data: io.read(MAGIC_NUMBER), binmode: true)
92
+ io.rewind
101
93
  end
102
94
 
103
95
  mime_type.strip unless mime_type.empty?
104
96
  end
105
97
 
106
- # Uses the ruby-filemagic gem to magically extract the MIME type.
98
+ def _extract_mime_type_with_mimemagic(io)
99
+ require "mimemagic"
100
+
101
+ mime = MimeMagic.by_magic(io)
102
+ io.rewind
103
+
104
+ mime.type if mime
105
+ end
106
+
107
107
  def _extract_mime_type_with_filemagic(io)
108
+ require "filemagic"
109
+
108
110
  filemagic = FileMagic.new(FileMagic::MAGIC_MIME_TYPE)
109
- filemagic.buffer(io.read(MAGIC_NUMBER))
110
- end
111
+ mime_type = filemagic.buffer(io.read(MAGIC_NUMBER))
111
112
 
112
- # Uses the mimemagic gem to extract the MIME type.
113
- def _extract_mime_type_with_mimemagic(io)
114
- MimeMagic.by_magic(io).type
113
+ io.rewind
114
+ filemagic.close
115
+
116
+ mime_type
115
117
  end
116
118
 
117
- # Uses the mime-types gem to determine MIME type from file extension.
118
119
  def _extract_mime_type_with_mime_types(io)
120
+ begin
121
+ require "mime/types/columnar"
122
+ rescue LoadError
123
+ require "mime/types"
124
+ end
125
+
119
126
  if filename = extract_filename(io)
120
127
  mime_type = MIME::Types.of(filename).first
121
128
  mime_type.to_s if mime_type