shrine 0.9.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +663 -0
- data/doc/creating_plugins.md +100 -0
- data/doc/creating_storages.md +108 -0
- data/doc/direct_s3.md +97 -0
- data/doc/migrating_storage.md +79 -0
- data/doc/regenerating_versions.md +38 -0
- data/lib/shrine.rb +806 -0
- data/lib/shrine/plugins/activerecord.rb +89 -0
- data/lib/shrine/plugins/background_helpers.rb +148 -0
- data/lib/shrine/plugins/cached_attachment_data.rb +47 -0
- data/lib/shrine/plugins/data_uri.rb +93 -0
- data/lib/shrine/plugins/default_storage.rb +39 -0
- data/lib/shrine/plugins/delete_invalid.rb +25 -0
- data/lib/shrine/plugins/determine_mime_type.rb +119 -0
- data/lib/shrine/plugins/direct_upload.rb +274 -0
- data/lib/shrine/plugins/dynamic_storage.rb +57 -0
- data/lib/shrine/plugins/hooks.rb +123 -0
- data/lib/shrine/plugins/included.rb +48 -0
- data/lib/shrine/plugins/keep_files.rb +54 -0
- data/lib/shrine/plugins/logging.rb +158 -0
- data/lib/shrine/plugins/migration_helpers.rb +61 -0
- data/lib/shrine/plugins/moving.rb +75 -0
- data/lib/shrine/plugins/multi_delete.rb +47 -0
- data/lib/shrine/plugins/parallelize.rb +62 -0
- data/lib/shrine/plugins/pretty_location.rb +32 -0
- data/lib/shrine/plugins/recache.rb +36 -0
- data/lib/shrine/plugins/remote_url.rb +127 -0
- data/lib/shrine/plugins/remove_attachment.rb +59 -0
- data/lib/shrine/plugins/restore_cached.rb +36 -0
- data/lib/shrine/plugins/sequel.rb +94 -0
- data/lib/shrine/plugins/store_dimensions.rb +82 -0
- data/lib/shrine/plugins/validation_helpers.rb +168 -0
- data/lib/shrine/plugins/versions.rb +177 -0
- data/lib/shrine/storage/file_system.rb +165 -0
- data/lib/shrine/storage/linter.rb +94 -0
- data/lib/shrine/storage/s3.rb +118 -0
- data/lib/shrine/version.rb +14 -0
- data/shrine.gemspec +46 -0
- metadata +364 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
require "active_record"
|
2
|
+
|
3
|
+
class Shrine
|
4
|
+
module Plugins
|
5
|
+
# The activerecord plugin extends the "attachment" interface with support
|
6
|
+
# for ActiveRecord.
|
7
|
+
#
|
8
|
+
# plugin :activerecord
|
9
|
+
#
|
10
|
+
# Now whenever an "attachment" module is included, additional callbacks are
|
11
|
+
# added to the model:
|
12
|
+
#
|
13
|
+
# * `before_save` -- Currently only used by the recache plugin.
|
14
|
+
# * `after_commit on: [:create, :update]` -- Promotes the attachment, deletes replaced ones.
|
15
|
+
# * `after_commit on: [:destroy]` -- Deletes the attachment.
|
16
|
+
#
|
17
|
+
# Note that if your tests are wrapped in transactions, the `after_commit`
|
18
|
+
# callbacks won't get called, so in order to test uploading you should first
|
19
|
+
# disable these transactions for those tests.
|
20
|
+
#
|
21
|
+
# If you want to put some parts of this lifecycle into a background job, see
|
22
|
+
# the background_helpers plugin.
|
23
|
+
#
|
24
|
+
# Additionally, any Shrine validation errors will added to ActiveRecord's
|
25
|
+
# errors upon validation. Note that if you want to validate presence of the
|
26
|
+
# attachment, you can do it directly on the model.
|
27
|
+
#
|
28
|
+
# class User < ActiveRecord::Base
|
29
|
+
# include ImageUploader[:avatar]
|
30
|
+
# validates_presence_of :avatar
|
31
|
+
# end
|
32
|
+
module Activerecord
|
33
|
+
module AttachmentMethods
|
34
|
+
def included(model)
|
35
|
+
super
|
36
|
+
|
37
|
+
model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
38
|
+
validate do
|
39
|
+
#{@name}_attacher.errors.each do |message|
|
40
|
+
errors.add(:#{@name}, message)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
before_save do
|
45
|
+
#{@name}_attacher.save
|
46
|
+
end
|
47
|
+
|
48
|
+
after_commit on: [:create, :update] do
|
49
|
+
#{@name}_attacher.replace
|
50
|
+
#{@name}_attacher._promote
|
51
|
+
end
|
52
|
+
|
53
|
+
after_commit on: :destroy do
|
54
|
+
#{@name}_attacher.destroy
|
55
|
+
end
|
56
|
+
RUBY
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
module AttacherClassMethods
|
61
|
+
# Needed by the background_helpers plugin.
|
62
|
+
def find_record(record_class, record_id)
|
63
|
+
record_class.find(record_id)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module AttacherMethods
|
68
|
+
private
|
69
|
+
|
70
|
+
# We save the record after updating, raising any validation errors.
|
71
|
+
def update(uploaded_file)
|
72
|
+
super
|
73
|
+
record.save!
|
74
|
+
end
|
75
|
+
|
76
|
+
# If we're in a transaction, then promoting is happening inline. If
|
77
|
+
# we're not, then this is happening in a background job. In that case
|
78
|
+
# when we're checking that the attachment changed during storing, we
|
79
|
+
# need to first reload the record to pick up new columns.
|
80
|
+
def changed?(uploaded_file)
|
81
|
+
record.reload
|
82
|
+
super
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
register_plugin(:activerecord, Activerecord)
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The background_helpers plugin enables you to intercept phases of
|
4
|
+
# uploading and put them into background jobs. This doesn't require any
|
5
|
+
# additional columns.
|
6
|
+
#
|
7
|
+
# plugin :background_helpers
|
8
|
+
#
|
9
|
+
# ## Promoting
|
10
|
+
#
|
11
|
+
# If you're doing processing, or your `:store` is something other than
|
12
|
+
# Storage::FileSystem, it's recommended to put promoting (moving to store)
|
13
|
+
# into a background job. This plugin allows you to do that by calling
|
14
|
+
# `Shrine::Attacher.promote`:
|
15
|
+
#
|
16
|
+
# Shrine::Attacher.promote { |data| UploadJob.perform_async(data) }
|
17
|
+
#
|
18
|
+
# When you call `Shrine::Attacher.promote` with a block, it will save the
|
19
|
+
# block and call it on every promotion. Then in your background job you can
|
20
|
+
# again call `Shrine::Attacher.promote` with the data, and internally it
|
21
|
+
# will resolve all necessary objects, do the promoting and update the
|
22
|
+
# record.
|
23
|
+
#
|
24
|
+
# class UploadJob
|
25
|
+
# include Sidekiq::Worker
|
26
|
+
#
|
27
|
+
# def perform(data)
|
28
|
+
# Shrine::Attacher.promote(data)
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# Shrine automatically handles all concurrency issues, such as canceling
|
33
|
+
# promoting if the attachment has changed in the meanwhile.
|
34
|
+
#
|
35
|
+
# ## Deleting
|
36
|
+
#
|
37
|
+
# If your `:store` is something other than Storage::FileSystem, it's
|
38
|
+
# recommended to put deleting files into a background job. This plugin
|
39
|
+
# allows you to do that by calling `Shrine::Attacher.delete`:
|
40
|
+
#
|
41
|
+
# Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) }
|
42
|
+
#
|
43
|
+
# When you call `Shrine::Attacher.delete` with a block, it will save the
|
44
|
+
# block and call it on every delete. Then in your background job you can
|
45
|
+
# again call `Shrine::Attacher.delete` with the data, and internally it
|
46
|
+
# will resolve all necessary objects, and delete the file.
|
47
|
+
#
|
48
|
+
# class DeleteJob
|
49
|
+
# include Sidekiq::Worker
|
50
|
+
#
|
51
|
+
# def perform(data)
|
52
|
+
# Shrine::Attacher.delete(data)
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# ## Conclusion
|
57
|
+
#
|
58
|
+
# The examples above used Sidekiq, but obviously you can just as well use
|
59
|
+
# any other backgrounding library. Also, if you want you can use
|
60
|
+
# backgrounding just for certain uploaders:
|
61
|
+
#
|
62
|
+
# class ImageUploader < Shrine
|
63
|
+
# Attacher.promote { |data| UploadJob.perform_async(data) }
|
64
|
+
# Attacher.delete { |data| DeleteJob.perform_async(data) }
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# If you would like to speed up your uploads and deletes, you can use the
|
68
|
+
# parallelize plugin, either as a replacement or an addition to
|
69
|
+
# background_helpers.
|
70
|
+
module BackgroundHelpers
|
71
|
+
module AttacherClassMethods
|
72
|
+
# If block is passed in, stores it to be called on promotion. Otherwise
|
73
|
+
# resolves data into objects and calls Attacher#promote.
|
74
|
+
def promote(data = nil, &block)
|
75
|
+
if block
|
76
|
+
shrine_class.opts[:background_promote] = block
|
77
|
+
else
|
78
|
+
record_class, record_id = data["record"]
|
79
|
+
record_class = Object.const_get(record_class)
|
80
|
+
record = find_record(record_class, record_id)
|
81
|
+
|
82
|
+
name = data["attachment"]
|
83
|
+
attacher = record.send("#{name}_attacher")
|
84
|
+
cached_file = attacher.uploaded_file(data["uploaded_file"])
|
85
|
+
|
86
|
+
attacher.promote(cached_file)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# If block is passed in, stores it to be called on deletion. Otherwise
|
91
|
+
# resolves data into objects and calls Shrine.delete.
|
92
|
+
def delete(data = nil, &block)
|
93
|
+
if block
|
94
|
+
shrine_class.opts[:background_delete] = block
|
95
|
+
else
|
96
|
+
record_class, record_id = data["record"]
|
97
|
+
record = Object.const_get(record_class).new
|
98
|
+
record.id = record_id
|
99
|
+
|
100
|
+
name, phase = data["attachment"], data["phase"]
|
101
|
+
shrine_class = record.send("#{name}_attacher").shrine_class
|
102
|
+
uploaded_file = shrine_class.uploaded_file(data["uploaded_file"])
|
103
|
+
context = {name: name.to_sym, record: record, phase: phase.to_sym}
|
104
|
+
|
105
|
+
shrine_class.delete(uploaded_file, context)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
module AttacherMethods
|
111
|
+
# Calls the promoting block with the data if it's been registered.
|
112
|
+
def _promote
|
113
|
+
if background_promote = shrine_class.opts[:background_promote]
|
114
|
+
data = {
|
115
|
+
"uploaded_file" => get.to_json,
|
116
|
+
"record" => [record.class.to_s, record.id],
|
117
|
+
"attachment" => name,
|
118
|
+
}
|
119
|
+
|
120
|
+
instance_exec(data, &background_promote) if promote?(get)
|
121
|
+
else
|
122
|
+
super
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# Calls the deleting block with the data if it's been registered.
|
129
|
+
def delete!(uploaded_file, phase:)
|
130
|
+
if background_delete = shrine_class.opts[:background_delete]
|
131
|
+
data = {
|
132
|
+
"uploaded_file" => uploaded_file.to_json,
|
133
|
+
"record" => [record.class.to_s, record.id],
|
134
|
+
"attachment" => name,
|
135
|
+
"phase" => phase,
|
136
|
+
}
|
137
|
+
|
138
|
+
instance_exec(data, &background_delete)
|
139
|
+
else
|
140
|
+
super
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
register_plugin(:background_helpers, BackgroundHelpers)
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The cached_attachment_data adds a method for assigning cached files that
|
4
|
+
# is more convenient for forms.
|
5
|
+
#
|
6
|
+
# plugin :cached_attachment_data
|
7
|
+
#
|
8
|
+
# If for example your attachment is called "avatar", this plugin will add
|
9
|
+
# `#cached_avatar_data` and `#cached_avatar_data=` methods to your model.
|
10
|
+
# This allows you to write your hidden field without explicitly setting
|
11
|
+
# `:value`:
|
12
|
+
#
|
13
|
+
# <%= form_for @user do |f| %>
|
14
|
+
# <%= f.hidden_field :cached_avatar_data %>
|
15
|
+
# <%= f.field_field :avatar %>
|
16
|
+
# <% end %>
|
17
|
+
#
|
18
|
+
# Additionally, the hidden field will only be set when the attachment is
|
19
|
+
# cached (as opposed to the default where `user.avatar_data` will return
|
20
|
+
# both cached and stored files). This keeps Rails logs cleaner.
|
21
|
+
module CachedAttachmentData
|
22
|
+
module AttachmentMethods
|
23
|
+
def initialize(name)
|
24
|
+
super
|
25
|
+
|
26
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
27
|
+
def cached_#{name}_data
|
28
|
+
#{name}_attacher.read_cached
|
29
|
+
end
|
30
|
+
|
31
|
+
def cached_#{name}_data=(value)
|
32
|
+
#{name}_attacher.assign(value)
|
33
|
+
end
|
34
|
+
RUBY
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module AttacherMethods
|
39
|
+
def read_cached
|
40
|
+
get.to_json if get && cache.uploaded?(get)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
register_plugin(:cached_attachment_data, CachedAttachmentData)
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require "base64"
|
2
|
+
|
3
|
+
class Shrine
|
4
|
+
module Plugins
|
5
|
+
# The data_uri plugin enables you to upload files as [data URIs].
|
6
|
+
# This plugin is useful for example when using [HTML5 Canvas].
|
7
|
+
#
|
8
|
+
# plugin :data_uri
|
9
|
+
#
|
10
|
+
# If for example your attachment is called "avatar", this plugin will add
|
11
|
+
# `#avatar_data_uri` and `#avatar_data_uri=` methods to your model.
|
12
|
+
#
|
13
|
+
# user.avatar #=> nil
|
14
|
+
# user.avatar_data_uri = "data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA"
|
15
|
+
# user.avatar #=> #<Shrine::UploadedFile>
|
16
|
+
#
|
17
|
+
# user.avatar.mime_type #=> "image/jpeg"
|
18
|
+
# user.avatar.size #=> 43423
|
19
|
+
# user.avatar.original_filename #=> nil
|
20
|
+
#
|
21
|
+
# If the data URI wasn't correctly parsed, an error message will added to
|
22
|
+
# the attachment column. You can change the default error message:
|
23
|
+
#
|
24
|
+
# plugin :data_uri, error_message: "data URI was invalid"
|
25
|
+
# plugin :data_uri, error_message: ->(uri) { I18n.t("errors.data_uri_invalid") }
|
26
|
+
#
|
27
|
+
# [data URIs]: https://tools.ietf.org/html/rfc2397
|
28
|
+
# [HTML5 Canvas]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
|
29
|
+
module DataUri
|
30
|
+
DEFAULT_ERROR_MESSAGE = "data URI was invalid"
|
31
|
+
DEFAULT_CONTENT_TYPE = "text/plain"
|
32
|
+
DATA_URI_REGEXP = /\Adata:([-\w]+\/[-\w\+\.]+)?;base64,(.*)/m
|
33
|
+
|
34
|
+
def self.configure(uploader, error_message: DEFAULT_ERROR_MESSAGE)
|
35
|
+
uploader.opts[:data_uri_error_message] = error_message
|
36
|
+
end
|
37
|
+
|
38
|
+
module AttachmentMethods
|
39
|
+
def initialize(name)
|
40
|
+
super
|
41
|
+
|
42
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
43
|
+
def #{name}_data_uri=(uri)
|
44
|
+
#{name}_attacher.data_uri = uri
|
45
|
+
end
|
46
|
+
|
47
|
+
def #{name}_data_uri
|
48
|
+
#{name}_attacher.data_uri
|
49
|
+
end
|
50
|
+
RUBY
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
module AttacherMethods
|
55
|
+
# Handles assignment of a data URI. If the regexp matches, it extracts
|
56
|
+
# the content type, decodes it, wrappes it in a StringIO and assigns it.
|
57
|
+
# If it fails, it sets the error message and assigns the uri in an
|
58
|
+
# instance variable so that it shows up on the UI.
|
59
|
+
def data_uri=(uri)
|
60
|
+
return if uri == ""
|
61
|
+
|
62
|
+
if match = uri.match(DATA_URI_REGEXP)
|
63
|
+
content_type = match[1] || DEFAULT_CONTENT_TYPE
|
64
|
+
content = Base64.decode64(match[2])
|
65
|
+
|
66
|
+
assign DataFile.new(content, content_type: content_type)
|
67
|
+
else
|
68
|
+
message = shrine_class.opts[:data_uri_error_message]
|
69
|
+
message = message.call(uri) if message.respond_to?(:call)
|
70
|
+
errors << message
|
71
|
+
@data_uri = uri
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Form builders require the reader as well.
|
76
|
+
def data_uri
|
77
|
+
@data_uri
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class DataFile < StringIO
|
82
|
+
attr_reader :content_type
|
83
|
+
|
84
|
+
def initialize(content, content_type: nil)
|
85
|
+
@content_type = content_type
|
86
|
+
super(content)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
register_plugin(:data_uri, DataUri)
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The default_storage plugin enables you to change which storages are going
|
4
|
+
# to be used for this uploader's attacher (the default is `:cache` and
|
5
|
+
# `:store`).
|
6
|
+
#
|
7
|
+
# plugin :default_storage, cache: :special_cache, store: :special_store
|
8
|
+
#
|
9
|
+
# You can also pass a block and choose the values depending on the record
|
10
|
+
# values and the name of the attachment. This is useful if you're using the
|
11
|
+
# dynamic_storage plugin. Example:
|
12
|
+
#
|
13
|
+
# plugin :default_storage, store: ->(record, name) { :"store_#{record.username}" }
|
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
|
18
|
+
end
|
19
|
+
|
20
|
+
module AttacherMethods
|
21
|
+
def initialize(record, name, **options)
|
22
|
+
if cache = shrine_class.opts[:default_storage_cache]
|
23
|
+
cache = cache.call(record, name) if cache.respond_to?(:call)
|
24
|
+
options[:cache] = cache
|
25
|
+
end
|
26
|
+
|
27
|
+
if store = shrine_class.opts[:default_storage_store]
|
28
|
+
store = store.call(record, name) if store.respond_to?(:call)
|
29
|
+
options[:store] = store
|
30
|
+
end
|
31
|
+
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
register_plugin(:default_storage, DefaultStorage)
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The delete_invalid plugin immediately deletes the assigned attachment if
|
4
|
+
# it failed validation.
|
5
|
+
#
|
6
|
+
# plugin :delete_invalid
|
7
|
+
#
|
8
|
+
# By default an attachment is always cached before it's validated. This
|
9
|
+
# way the attachment will persist when the form is resubmitted, which is
|
10
|
+
# consistent with the other fields in the form. However, if this is a
|
11
|
+
# concern, you can load this plugin.
|
12
|
+
module DeleteInvalid
|
13
|
+
module AttacherMethods
|
14
|
+
# Delete the assigned uploaded file if it was invalid.
|
15
|
+
def validate
|
16
|
+
super
|
17
|
+
ensure
|
18
|
+
delete!(get, phase: :invalid) if !errors.empty?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
register_plugin(:delete_invalid, DeleteInvalid)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The determine_mime_type plugin stores the actual MIME type of the
|
4
|
+
# uploaded file.
|
5
|
+
#
|
6
|
+
# plugin :determine_mime_type
|
7
|
+
#
|
8
|
+
# The plugin accepts the following analyzers:
|
9
|
+
#
|
10
|
+
# :file
|
11
|
+
# : (Default). Uses the UNIX [file] utility to determine the MIME type
|
12
|
+
# from file contents.
|
13
|
+
#
|
14
|
+
# :filemagic
|
15
|
+
# : Uses the [ruby-filemagic] gem to determine the MIME type from file
|
16
|
+
# contents, using a similar MIME database as the `file` utility.
|
17
|
+
# Unlike the `file` utility, ruby-filemagic should work on Windows.
|
18
|
+
#
|
19
|
+
# :mimemagic
|
20
|
+
# : Uses the [mimemagic] gem to determine the MIME type from file contents.
|
21
|
+
# Unlike ruby-filemagic, mimemagic is a pure-ruby solution, so it will
|
22
|
+
# work across all Ruby implementations.
|
23
|
+
#
|
24
|
+
# :mime_types
|
25
|
+
# : Uses the [mime-types] gem to determine the MIME type from the file
|
26
|
+
# *extension*. Note that unlike other solutions, this analyzer is not
|
27
|
+
# guaranteed to return the actual MIME type of the file.
|
28
|
+
#
|
29
|
+
# By default the UNIX [file] utility is used to detrmine the MIME type, but
|
30
|
+
# you can change it:
|
31
|
+
#
|
32
|
+
# plugin :determine_mime_type, analyzer: :filemagic
|
33
|
+
#
|
34
|
+
# If none of these quite suit your needs, you can use a custom analyzer:
|
35
|
+
#
|
36
|
+
# plugin :determine_mime_type, analyzer: ->(io) do
|
37
|
+
# if io.path.end_with?(".odt")
|
38
|
+
# "application/vnd.oasis.opendocument.text"
|
39
|
+
# else
|
40
|
+
# MimeMagic.by_magic(io).type
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# [file]: http://linux.die.net/man/1/file
|
45
|
+
# [ruby-filemagic]: https://github.com/blackwinter/ruby-filemagic
|
46
|
+
# [mimemagic]: https://github.com/minad/mimemagic
|
47
|
+
# [mime-types]: https://github.com/mime-types/ruby-mime-types
|
48
|
+
module DetermineMimeType
|
49
|
+
def self.load_dependencies(uploader, analyzer: :file)
|
50
|
+
case analyzer
|
51
|
+
when :file then require "open3"
|
52
|
+
when :filemagic then require "filemagic"
|
53
|
+
when :mimemagic then require "mimemagic"
|
54
|
+
when :mime_types
|
55
|
+
begin
|
56
|
+
require "mime/types/columnar"
|
57
|
+
rescue LoadError
|
58
|
+
require "mime/types"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.configure(uploader, analyzer: :file)
|
64
|
+
uploader.opts[:mime_type_analyzer] = analyzer
|
65
|
+
end
|
66
|
+
|
67
|
+
module InstanceMethods
|
68
|
+
# If a Shrine::UploadedFile was given, it returns its MIME type, since
|
69
|
+
# that value was already determined by this analyzer. Otherwise it calls
|
70
|
+
# a built-in analyzer or a custom one.
|
71
|
+
def extract_mime_type(io)
|
72
|
+
analyzer = opts[:mime_type_analyzer]
|
73
|
+
|
74
|
+
if io.respond_to?(:mime_type)
|
75
|
+
io.mime_type
|
76
|
+
elsif analyzer.is_a?(Symbol)
|
77
|
+
send(:"_extract_mime_type_with_#{analyzer}", io)
|
78
|
+
else
|
79
|
+
analyzer.call(io)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
# Uses the UNIX file utility to extract the MIME type. It does so only
|
86
|
+
# if it's a file, because even though the utility accepts standard
|
87
|
+
# input, it would mean that we have to read the whole file in memory.
|
88
|
+
def _extract_mime_type_with_file(io)
|
89
|
+
if io.respond_to?(:path)
|
90
|
+
mime_type, _ = Open3.capture2("file", "-b", "--mime-type", io.path)
|
91
|
+
mime_type.strip unless mime_type.empty?
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Uses the ruby-filemagic gem to magically extract the MIME type.
|
96
|
+
def _extract_mime_type_with_filemagic(io)
|
97
|
+
filemagic = FileMagic.new(FileMagic::MAGIC_MIME_TYPE)
|
98
|
+
data = io.read(1024); io.rewind
|
99
|
+
filemagic.buffer(data)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Uses the mimemagic gem to extract the MIME type.
|
103
|
+
def _extract_mime_type_with_mimemagic(io)
|
104
|
+
MimeMagic.by_magic(io).type
|
105
|
+
end
|
106
|
+
|
107
|
+
# Uses the mime-types gem to determine MIME type from file extension.
|
108
|
+
def _extract_mime_type_with_mime_types(io)
|
109
|
+
if filename = extract_filename(io)
|
110
|
+
mime_type = MIME::Types.of(filename).first
|
111
|
+
mime_type.to_s if mime_type
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
register_plugin(:determine_mime_type, DetermineMimeType)
|
118
|
+
end
|
119
|
+
end
|