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.
- checksums.yaml +4 -4
- data/README.md +236 -234
- data/doc/changing_location.md +6 -4
- data/doc/creating_storages.md +4 -4
- data/doc/design.md +223 -0
- data/doc/migrating_storage.md +6 -11
- data/doc/regenerating_versions.md +22 -40
- data/lib/shrine.rb +60 -77
- data/lib/shrine/plugins/activerecord.rb +37 -14
- data/lib/shrine/plugins/background_helpers.rb +1 -0
- data/lib/shrine/plugins/backgrounding.rb +49 -37
- data/lib/shrine/plugins/backup.rb +6 -4
- data/lib/shrine/plugins/cached_attachment_data.rb +5 -5
- data/lib/shrine/plugins/data_uri.rb +9 -9
- data/lib/shrine/plugins/default_storage.rb +4 -4
- data/lib/shrine/plugins/default_url.rb +7 -1
- data/lib/shrine/plugins/default_url_options.rb +1 -1
- data/lib/shrine/plugins/delete_promoted.rb +2 -2
- data/lib/shrine/plugins/delete_raw.rb +4 -4
- data/lib/shrine/plugins/determine_mime_type.rb +50 -43
- data/lib/shrine/plugins/direct_upload.rb +10 -20
- data/lib/shrine/plugins/download_endpoint.rb +16 -13
- data/lib/shrine/plugins/dynamic_storage.rb +4 -12
- data/lib/shrine/plugins/included.rb +6 -19
- data/lib/shrine/plugins/keep_files.rb +4 -4
- data/lib/shrine/plugins/logging.rb +4 -4
- data/lib/shrine/plugins/migration_helpers.rb +37 -34
- data/lib/shrine/plugins/moving.rb +19 -32
- data/lib/shrine/plugins/parallelize.rb +5 -5
- data/lib/shrine/plugins/pretty_location.rb +2 -6
- data/lib/shrine/plugins/remote_url.rb +31 -43
- data/lib/shrine/plugins/remove_attachment.rb +5 -5
- data/lib/shrine/plugins/remove_invalid.rb +1 -1
- data/lib/shrine/plugins/restore_cached_data.rb +4 -10
- data/lib/shrine/plugins/sequel.rb +46 -21
- data/lib/shrine/plugins/store_dimensions.rb +19 -20
- data/lib/shrine/plugins/upload_options.rb +11 -9
- data/lib/shrine/plugins/validation_helpers.rb +3 -3
- data/lib/shrine/plugins/versions.rb +18 -3
- data/lib/shrine/storage/file_system.rb +9 -11
- data/lib/shrine/storage/linter.rb +1 -7
- data/lib/shrine/storage/s3.rb +25 -19
- data/lib/shrine/version.rb +3 -3
- data/shrine.gemspec +13 -3
- metadata +28 -9
- data/lib/shrine/plugins/delete_uploaded.rb +0 -3
- data/lib/shrine/plugins/keep_location.rb +0 -46
- data/lib/shrine/plugins/restore_cached.rb +0 -3
@@ -7,10 +7,11 @@ class Shrine
|
|
7
7
|
#
|
8
8
|
# plugin :activerecord
|
9
9
|
#
|
10
|
-
#
|
11
|
-
# added to the model:
|
10
|
+
# ## Callbacks
|
12
11
|
#
|
13
|
-
#
|
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
|
29
|
-
#
|
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
|
-
#
|
79
|
-
|
80
|
-
|
81
|
-
|
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.
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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["
|
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
|
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["
|
99
|
-
|
111
|
+
uploaded_file = attacher.uploaded_file(data["attachment"])
|
112
|
+
phase = data["phase"].to_sym if data["phase"]
|
100
113
|
|
101
|
-
attacher.
|
114
|
+
attacher.delete!(uploaded_file, phase: phase)
|
102
115
|
|
103
|
-
attacher
|
116
|
+
attacher
|
104
117
|
end
|
105
118
|
end
|
106
119
|
|
107
|
-
#
|
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["
|
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(
|
138
|
-
|
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
|
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
|
-
"
|
158
|
-
"phase"
|
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
|
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,
|
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
|
-
|
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(
|
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,
|
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(
|
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,
|
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
|
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
|
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
|
-
|
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,
|
15
|
-
uploader.opts[:
|
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[:
|
31
|
-
opts[:
|
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]
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
38
|
-
#
|
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.
|
48
|
-
|
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
|
-
|
77
|
-
|
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
|
-
|
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
|
-
|
84
|
+
require "open3"
|
85
|
+
|
86
|
+
cmd = ["file", "--mime-type", "--brief", "--"]
|
96
87
|
|
97
88
|
if io.respond_to?(:path)
|
98
|
-
mime_type,
|
89
|
+
mime_type, * = Open3.capture2(*cmd, io.path)
|
99
90
|
else
|
100
|
-
mime_type,
|
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
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
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
|