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,62 @@
|
|
1
|
+
require "thread/pool"
|
2
|
+
|
3
|
+
Thread::Pool.abort_on_exception = true
|
4
|
+
|
5
|
+
class Shrine
|
6
|
+
module Plugins
|
7
|
+
# The parallelize plugin parallelizes your uploads and deletes using the
|
8
|
+
# [thread] gem.
|
9
|
+
#
|
10
|
+
# plugin :parallelize
|
11
|
+
#
|
12
|
+
# This plugin is generally only useful as an addition to the versions
|
13
|
+
# plugin, where multiple files are being uploaded and deleted at once. Note
|
14
|
+
# that it's not possible for this plugin to parallelize processing, but it
|
15
|
+
# should be easy to do that manually.
|
16
|
+
#
|
17
|
+
# By default a pool of 3 threads will be used, but you can change that:
|
18
|
+
#
|
19
|
+
# plugin :parallelize, threads: 5
|
20
|
+
#
|
21
|
+
# [thread]: https://github.com/meh/ruby-thread
|
22
|
+
module Parallelize
|
23
|
+
def self.configure(uploader, threads: 3)
|
24
|
+
uploader.opts[:parallelize_threads] = threads
|
25
|
+
end
|
26
|
+
|
27
|
+
module InstanceMethods
|
28
|
+
def store(io, context = {})
|
29
|
+
with_pool { |pool| super(io, thread_pool: pool, **context) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete(uploaded_file, context = {})
|
33
|
+
with_pool { |pool| super(uploaded_file, thread_pool: pool, **context) }
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def copy(io, context)
|
39
|
+
context[:thread_pool].process { super }
|
40
|
+
end
|
41
|
+
|
42
|
+
def move(io, context)
|
43
|
+
context[:thread_pool].process { super }
|
44
|
+
end
|
45
|
+
|
46
|
+
def remove(uploaded_file, context)
|
47
|
+
context[:thread_pool].process { super }
|
48
|
+
end
|
49
|
+
|
50
|
+
# We initialize a thread pool with configured number of threads.
|
51
|
+
def with_pool
|
52
|
+
pool = Thread.pool(opts[:parallelize_threads])
|
53
|
+
result = yield pool
|
54
|
+
pool.shutdown
|
55
|
+
result
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
register_plugin(:parallelize, Parallelize)
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The pretty_location plugin attempts to generate a nicer folder structure
|
4
|
+
# for uploaded files.
|
5
|
+
#
|
6
|
+
# plugin :pretty_location
|
7
|
+
#
|
8
|
+
# This plugin uses the context information from the Attacher to try to
|
9
|
+
# generate a nested folder structure which separates files for each record.
|
10
|
+
# The newly generated locations will typically look like this:
|
11
|
+
#
|
12
|
+
# "user/564/avatar/thumb-493g82jf23.jpg"
|
13
|
+
# # :model/:id/:attachment/:version-:uid.:extension
|
14
|
+
module PrettyLocation
|
15
|
+
module InstanceMethods
|
16
|
+
def generate_location(io, context)
|
17
|
+
type = context[:record].class.name.downcase if context[:record] && context[:record].class.name
|
18
|
+
id = context[:record].id if context[:record].respond_to?(:id)
|
19
|
+
name = context[:name]
|
20
|
+
|
21
|
+
dirname, slash, basename = super.rpartition("/")
|
22
|
+
basename = "#{context[:version]}-#{basename}" if context[:version]
|
23
|
+
original = dirname + slash + basename
|
24
|
+
|
25
|
+
[type, id, name, original].compact.join("/")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
register_plugin(:pretty_location, PrettyLocation)
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The recache plugin allows you to process your attachment after
|
4
|
+
# validations succeed, but before the attachment is promoted. This is
|
5
|
+
# useful for example when you want to generate some versions upfront (so
|
6
|
+
# the user immediately sees them) and other versions you want to generate
|
7
|
+
# in the promotion phase in a background job.
|
8
|
+
#
|
9
|
+
# The phase will be set to `:recache`:
|
10
|
+
#
|
11
|
+
# class ImageUploader
|
12
|
+
# plugin :recache
|
13
|
+
#
|
14
|
+
# def process(io, context)
|
15
|
+
# case context[:phase]
|
16
|
+
# when :recache
|
17
|
+
# # generate cheap versions
|
18
|
+
# when :store
|
19
|
+
# # generate more expensive versions
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
module Recache
|
24
|
+
module AttacherMethods
|
25
|
+
def save
|
26
|
+
if get && defined?(@old_attachment) # new file was assigned
|
27
|
+
_set cache!(get, phase: :recache)
|
28
|
+
end
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
register_plugin(:recache, Recache)
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The remote_url plugin allows you to attach files from a remote location.
|
4
|
+
#
|
5
|
+
# plugin :remote_url, max_size: 20*1024*1024
|
6
|
+
#
|
7
|
+
# If for example your attachment is called "avatar", this plugin will add
|
8
|
+
# `#avatar_remote_url` and `#avatar_remote_url=` methods to your model.
|
9
|
+
#
|
10
|
+
# user.avatar #=> nil
|
11
|
+
# user.avatar_remote_url = "http://example.com/cool-image.png"
|
12
|
+
# user.avatar #=> #<Shrine::UploadedFile>
|
13
|
+
#
|
14
|
+
# user.avatar.mime_type #=> "image/png"
|
15
|
+
# user.avatar.size #=> 43423
|
16
|
+
# user.avatar.original_filename #=> "cool-image.png"
|
17
|
+
#
|
18
|
+
# The file will by default be downloaded using Ruby's open-uri standard
|
19
|
+
# library. Following redirects is disabled for security reasons. It's also
|
20
|
+
# good practice to limit the filesize of the remote file:
|
21
|
+
#
|
22
|
+
# plugin :remote_url, max_size: 20*1024*1024 # 20 MB
|
23
|
+
#
|
24
|
+
# Now if a file that is bigger than 20MB is assigned, Shrine will terminate
|
25
|
+
# the download as soon as it gets the "Content-Length" header, or the
|
26
|
+
# buffer size surpasses the maximum size. However, if for whatever reason
|
27
|
+
# you don't want to limit the maximum file size, you can set `:max_size` to
|
28
|
+
# nil:
|
29
|
+
#
|
30
|
+
# plugin :remote_url, max_size: nil
|
31
|
+
#
|
32
|
+
# If download fails, either because the remote file wasn't found, was too
|
33
|
+
# large, or the request redirected, an error will be added to the
|
34
|
+
# attachment. You can change the default error message:
|
35
|
+
#
|
36
|
+
# plugin :remote_url, error_message: "download failed"
|
37
|
+
# plugin :remote_url, error_message: ->(url) { I18n.t("errors.download_failed") }
|
38
|
+
#
|
39
|
+
# Finally, if for some reason the way the file is downloaded doesn't suit
|
40
|
+
# your needs, you can provide a custom downloader:
|
41
|
+
#
|
42
|
+
# plugin :remote_url, downloader: ->(url) do
|
43
|
+
# request = RestClient::Request.new(method: :get, url: url, raw_response: true)
|
44
|
+
# response = request.execute
|
45
|
+
# response.file
|
46
|
+
# end
|
47
|
+
module RemoteUrl
|
48
|
+
DEFAULT_ERROR_MESSAGE = "file was not found or was too large"
|
49
|
+
|
50
|
+
def self.load_dependencies(uploader, downloader: :open_uri, **)
|
51
|
+
case downloader
|
52
|
+
when :open_uri then require "down"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.configure(uploader, downloader: :open_uri, error_message: nil, max_size:)
|
57
|
+
uploader.opts[:remote_url_downloader] = downloader
|
58
|
+
uploader.opts[:remote_url_error_message] = error_message || DEFAULT_ERROR_MESSAGE
|
59
|
+
uploader.opts[:remote_url_max_size] = max_size
|
60
|
+
end
|
61
|
+
|
62
|
+
module AttachmentMethods
|
63
|
+
def initialize(name)
|
64
|
+
super
|
65
|
+
|
66
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
67
|
+
def #{name}_remote_url=(url)
|
68
|
+
#{name}_attacher.remote_url = url
|
69
|
+
end
|
70
|
+
|
71
|
+
def #{name}_remote_url
|
72
|
+
#{name}_attacher.remote_url
|
73
|
+
end
|
74
|
+
RUBY
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
module AttacherMethods
|
79
|
+
# Downloads the remote file and assigns it. If download failed, sets
|
80
|
+
# the error message and assigns the url to an instance variable so that
|
81
|
+
# it shows up in the form.
|
82
|
+
def remote_url=(url)
|
83
|
+
return if url == ""
|
84
|
+
|
85
|
+
if downloaded_file = download(url)
|
86
|
+
assign(downloaded_file)
|
87
|
+
else
|
88
|
+
message = shrine_class.opts[:remote_url_error_message]
|
89
|
+
message = message.call(url) if message.respond_to?(:call)
|
90
|
+
errors << message
|
91
|
+
@remote_url = url
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Form builders require the reader as well.
|
96
|
+
def remote_url
|
97
|
+
@remote_url
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
# Downloads the file using the "down" gem or a custom downloader.
|
103
|
+
# Checks the file size and terminates the download early if the file
|
104
|
+
# is too big.
|
105
|
+
def download(url)
|
106
|
+
downloader = shrine_class.opts[:remote_url_downloader]
|
107
|
+
max_size = shrine_class.opts[:remote_url_max_size]
|
108
|
+
|
109
|
+
if downloader.is_a?(Symbol)
|
110
|
+
send(:"download_with_#{downloader}", url, max_size: max_size)
|
111
|
+
else
|
112
|
+
downloader.call(url, max_size: max_size)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# We silence any download errors, because for the user's point of view
|
117
|
+
# the download simply failed.
|
118
|
+
def download_with_open_uri(url, max_size:)
|
119
|
+
Down.download(url, max_size: max_size)
|
120
|
+
rescue Down::Error
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
register_plugin(:remote_url, RemoteUrl)
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The remove_attachment plugin allows you to delete attachments through
|
4
|
+
# checkboxes on the web form.
|
5
|
+
#
|
6
|
+
# plugin :remove_attachment
|
7
|
+
#
|
8
|
+
# If for example your attachment is called "avatar", this plugin will add
|
9
|
+
# `#remove_avatar` and `#remove_avatar=` methods to your model. This allows
|
10
|
+
# you to easily enable deleting attached files through the form:
|
11
|
+
#
|
12
|
+
# <%= form_for @user do |f| %>
|
13
|
+
# <%= f.hidden_field :avatar, value: @user.avatar_data %>
|
14
|
+
# <%= f.file_field :avatar %>
|
15
|
+
# Remove attachment: <%= f.check_box :remove_avatar %>
|
16
|
+
# <% end %>
|
17
|
+
#
|
18
|
+
# Now when the checkbox is ticked and the form is submitted, the attached
|
19
|
+
# file will be removed.
|
20
|
+
module RemoveAttachment
|
21
|
+
module AttachmentMethods
|
22
|
+
def initialize(name)
|
23
|
+
super
|
24
|
+
|
25
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
26
|
+
def remove_#{name}=(value)
|
27
|
+
#{name}_attacher.remove = value
|
28
|
+
end
|
29
|
+
|
30
|
+
def remove_#{name}
|
31
|
+
#{name}_attacher.remove
|
32
|
+
end
|
33
|
+
RUBY
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module AttacherMethods
|
38
|
+
# We remove the attachment if the value evaluates to true.
|
39
|
+
def remove=(value)
|
40
|
+
@remove = value
|
41
|
+
set(nil) if remove?
|
42
|
+
end
|
43
|
+
|
44
|
+
def remove
|
45
|
+
@remove
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# Rails sends "0" or "false" if the checkbox hasn't been ticked.
|
51
|
+
def remove?
|
52
|
+
remove && remove != "" && remove !~ /\A0|false$\z/
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
register_plugin(:remove_attachment, RemoveAttachment)
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
class Shrine
|
4
|
+
module Plugins
|
5
|
+
# The restore_cached plugin ensures the cached file data hasn't been
|
6
|
+
# tampered with, by restoring its metadata after assignment. The user can
|
7
|
+
# tamper with the cached file data by modifying the hidden field before
|
8
|
+
# submitting the form.
|
9
|
+
#
|
10
|
+
# Firstly the assignment is terminated if the cached file doesn't exist,
|
11
|
+
# which can happen if the user changes the "id" or "storage" data. If the
|
12
|
+
# cached file exists, the metadata is reextracted from the original file
|
13
|
+
# and replaced with the potentially tampered with ones.
|
14
|
+
#
|
15
|
+
# plugin :restore_cached
|
16
|
+
module RestoreCached
|
17
|
+
module AttacherMethods
|
18
|
+
private
|
19
|
+
|
20
|
+
def assign_cached(value)
|
21
|
+
uploaded_file = uploaded_file(value) do |file|
|
22
|
+
next unless cache.uploaded?(file)
|
23
|
+
return unless file.exists?
|
24
|
+
uploader = shrine_class.uploader_for(file)
|
25
|
+
real_metadata = uploader.extract_metadata(file.to_io, context)
|
26
|
+
file.metadata.update(real_metadata)
|
27
|
+
end
|
28
|
+
|
29
|
+
super(uploaded_file)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
register_plugin(:restore_cached, RestoreCached)
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require "sequel"
|
2
|
+
|
3
|
+
class Shrine
|
4
|
+
module Plugins
|
5
|
+
# The sequel plugin extends the "attachment" interface with support for
|
6
|
+
# Sequel.
|
7
|
+
#
|
8
|
+
# plugin :sequel
|
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` -- Promotes the attachment, deletes replaced ones.
|
15
|
+
# * `after_destroy_commit` -- Deletes the attachment.
|
16
|
+
#
|
17
|
+
# Note that if your tests are wrapped in transactions, the `after_commit`
|
18
|
+
# and `after_destroy_commit` callbacks won't get called, so in order to
|
19
|
+
# test uploading you should first disable these transactions for those
|
20
|
+
# tests.
|
21
|
+
#
|
22
|
+
# If you want to put some parts of this lifecycle into a background job, see
|
23
|
+
# the background_helpers plugin.
|
24
|
+
#
|
25
|
+
# Additionally, any Shrine validation errors will added to Sequel's
|
26
|
+
# errors upon validation. Note that if you want to validate presence of the
|
27
|
+
# attachment, you can do it directly on the model.
|
28
|
+
#
|
29
|
+
# class User < Sequel::Model
|
30
|
+
# include ImageUploader[:avatar]
|
31
|
+
# validates_presence_of :avatar
|
32
|
+
# end
|
33
|
+
module Sequel
|
34
|
+
module AttachmentMethods
|
35
|
+
def initialize(name)
|
36
|
+
super
|
37
|
+
|
38
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
39
|
+
def validate
|
40
|
+
super
|
41
|
+
#{name}_attacher.errors.each do |message|
|
42
|
+
errors.add(:#{name}, message)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def before_save
|
47
|
+
super
|
48
|
+
#{name}_attacher.save
|
49
|
+
end
|
50
|
+
|
51
|
+
def after_commit
|
52
|
+
super
|
53
|
+
#{name}_attacher.replace
|
54
|
+
#{name}_attacher._promote
|
55
|
+
end
|
56
|
+
|
57
|
+
def after_destroy_commit
|
58
|
+
super
|
59
|
+
#{name}_attacher.destroy
|
60
|
+
end
|
61
|
+
RUBY
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
module AttacherClassMethods
|
66
|
+
# Needed by the background_helpers plugin.
|
67
|
+
def find_record(record_class, record_id)
|
68
|
+
record_class.with_pk!(record_id)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module AttacherMethods
|
73
|
+
private
|
74
|
+
|
75
|
+
# We save the record after updating, raising any validation errors.
|
76
|
+
def update(uploaded_file)
|
77
|
+
super
|
78
|
+
record.save(raise_on_failure: true)
|
79
|
+
end
|
80
|
+
|
81
|
+
# If we're in a transaction, then promoting is happening inline. If
|
82
|
+
# we're not, then this is happening in a background job. In that case
|
83
|
+
# when we're checking that the attachment changed during storing, we
|
84
|
+
# need to first reload the record to pick up new columns.
|
85
|
+
def changed?(uploaded_file)
|
86
|
+
record.reload
|
87
|
+
super
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
register_plugin(:sequel, Sequel)
|
93
|
+
end
|
94
|
+
end
|