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,48 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The included plugin allows you to hook up to the `.included` hook when
|
4
|
+
# of the "attachment" module. This allows you to add additonal methods to
|
5
|
+
# the model whenever an attachment is included.
|
6
|
+
#
|
7
|
+
# plugin :included do |name|
|
8
|
+
# define_method("#{name}_width") do
|
9
|
+
# send(name).width if send(name)
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# define_method("#{name}_height") do
|
13
|
+
# send(name).height if send(name)
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# The block is evaluated in the context of the model, but note that you
|
18
|
+
# cannot use keywords like `def`, instead you should use the
|
19
|
+
# metaprogramming equivalents like `define_method`. Now when an attachment
|
20
|
+
# is included to a model, it will receive the appropriate methods:
|
21
|
+
#
|
22
|
+
# class User
|
23
|
+
# include ImageUploader[:avatar]
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# user = User.new
|
27
|
+
# user.avatar_width #=> nil
|
28
|
+
# user.avatar_height #=> nil
|
29
|
+
#
|
30
|
+
# user.avatar = File.open("avatar.jpg")
|
31
|
+
# user.avatar_width #=> 300
|
32
|
+
# user.avatar_height #=> 500
|
33
|
+
module Included
|
34
|
+
def self.configure(uploader, &block)
|
35
|
+
uploader.opts[:included_block] = block
|
36
|
+
end
|
37
|
+
|
38
|
+
module AttachmentMethods
|
39
|
+
def included(model)
|
40
|
+
super
|
41
|
+
model.instance_exec(@name, &shrine_class.opts[:included_block])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
register_plugin(:included, Included)
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The keep_files plugin gives you the ability to prevent files from being
|
4
|
+
# deleted. This functionality is useful when implementing soft deletes, or
|
5
|
+
# when implementing some kind of [event store] where you need to track
|
6
|
+
# history.
|
7
|
+
#
|
8
|
+
# The plugin accepts the following options:
|
9
|
+
#
|
10
|
+
# :destroyed
|
11
|
+
# : If set to `true`, destroying the record won't delete the associated
|
12
|
+
# attachment.
|
13
|
+
#
|
14
|
+
# :replaced
|
15
|
+
# : If set to `true`, uploading a new attachment won't delete the old one.
|
16
|
+
#
|
17
|
+
# :cached
|
18
|
+
# : If set to `true`, cached files that are uploaded to store won't be
|
19
|
+
# deleted.
|
20
|
+
#
|
21
|
+
# For example, the following will keep destroyed and replaced files:
|
22
|
+
#
|
23
|
+
# plugin :keep_files, destroyed: true, :replaced: true
|
24
|
+
#
|
25
|
+
# [event store]: http://docs.geteventstore.com/introduction/event-sourcing-basics/
|
26
|
+
module KeepFiles
|
27
|
+
def self.configure(uploader, destroyed: nil, replaced: nil, cached: nil)
|
28
|
+
uploader.opts[:keep_files] = []
|
29
|
+
uploader.opts[:keep_files] << :destroyed if destroyed
|
30
|
+
uploader.opts[:keep_files] << :replaced if replaced
|
31
|
+
uploader.opts[:keep_files] << :cached if cached
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
def keep?(type)
|
36
|
+
opts[:keep_files].include?(type)
|
37
|
+
end
|
38
|
+
|
39
|
+
# We hook to the generic deleting, and check the appropriate phases.
|
40
|
+
def delete(io, context)
|
41
|
+
case context[:phase]
|
42
|
+
when :cached then super unless keep?(:cached)
|
43
|
+
when :replaced then super unless keep?(:replaced)
|
44
|
+
when :destroyed then super unless keep?(:destroyed)
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
register_plugin(:keep_files, KeepFiles)
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "benchmark"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
class Shrine
|
6
|
+
module Plugins
|
7
|
+
# The logging plugin logs any storing/processing/deleting that is performed.
|
8
|
+
#
|
9
|
+
# plugin :logging
|
10
|
+
#
|
11
|
+
# This plugin is useful when you want to have overview of what exactly is
|
12
|
+
# going on, or you simply want to have it logged for future debugging.
|
13
|
+
# By default the logging output looks something like this:
|
14
|
+
#
|
15
|
+
# 2015-10-09T20:06:06.676Z #25602: UPLOAD[direct] ImageUploader[:avatar] User[29543] 1 file (0.1s)
|
16
|
+
# 2015-10-09T20:06:06.854Z #25602: PROCESS[promote]: ImageUploader[:avatar] User[29543] 3 files (0.22s)
|
17
|
+
# 2015-10-09T20:06:07.133Z #25602: DELETE[destroyed]: ImageUploader[:avatar] User[29543] 3 files (0.07s)
|
18
|
+
#
|
19
|
+
# The plugin accepts the following options:
|
20
|
+
#
|
21
|
+
# :format
|
22
|
+
# : This allows you to change the logging output into something that may be
|
23
|
+
# easier to grep. Accepts `:human` (default), `:json` and `:heroku`.
|
24
|
+
#
|
25
|
+
# :stream
|
26
|
+
# : The default logging stream is `$stdout`, but you may want to change it,
|
27
|
+
# e.g. if you log into a file. This option is passed directly to
|
28
|
+
# `Logger.new` (from the "logger" Ruby standard library).
|
29
|
+
#
|
30
|
+
# :logger
|
31
|
+
# : This allows you to change the logger entirely. This is useful for example
|
32
|
+
# in Rails applications, where you might want to assign this option to
|
33
|
+
# `Rails.logger`.
|
34
|
+
#
|
35
|
+
# The default format is probably easiest to read, but may not be easiest to
|
36
|
+
# grep. If this is important to you, you can switch to another format:
|
37
|
+
#
|
38
|
+
# plugin :logging, format: :json
|
39
|
+
# # {"action":"upload","phase":"direct","uploader":"ImageUploader","attachment":"avatar",...}
|
40
|
+
#
|
41
|
+
# plugin :logging, format: :heroku
|
42
|
+
# # action=upload phase=direct uploader=ImageUploader attachment=avatar record_class=User ...
|
43
|
+
#
|
44
|
+
# Logging is by default disabled in tests, but you can enable it by setting
|
45
|
+
# `Shrine.logger.level = Logger::INFO`.
|
46
|
+
module Logging
|
47
|
+
def self.configure(uploader, logger: nil, stream: $stdout, format: :human)
|
48
|
+
uploader.logger = logger if logger
|
49
|
+
uploader.opts[:logging_stream] = stream
|
50
|
+
uploader.opts[:logging_format] = format
|
51
|
+
end
|
52
|
+
|
53
|
+
module ClassMethods
|
54
|
+
def logger=(logger)
|
55
|
+
@logger = logger || Logger.new(nil)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Initializes a new logger if it hasn't been initialized.
|
59
|
+
def logger
|
60
|
+
@logger ||= (
|
61
|
+
logger = Logger.new(opts[:logging_stream])
|
62
|
+
logger.level = Logger::INFO
|
63
|
+
logger.level = Logger::WARN if ENV["RACK_ENV"] == "test"
|
64
|
+
logger.formatter = pretty_formatter
|
65
|
+
logger
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
# It makes logging preamble simpler than the default logger. Also, it
|
70
|
+
# doesn't output timestamps if on Heroku.
|
71
|
+
def pretty_formatter
|
72
|
+
proc do |severity, time, program_name, message|
|
73
|
+
output = "#{Process.pid}: #{message}\n"
|
74
|
+
output.prepend "#{time.utc.iso8601(3)} " unless ENV["DYNO"]
|
75
|
+
output
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
module InstanceMethods
|
81
|
+
def store(io, context = {})
|
82
|
+
log("store", context) { super }
|
83
|
+
end
|
84
|
+
|
85
|
+
def delete(uploaded_file, context = {})
|
86
|
+
log("delete", context) { super }
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def processed(io, context = {})
|
92
|
+
log("process", context) { super }
|
93
|
+
end
|
94
|
+
|
95
|
+
# Collects the data and sends it for logging.
|
96
|
+
def log(action, context)
|
97
|
+
result, duration = benchmark { yield }
|
98
|
+
|
99
|
+
_log(
|
100
|
+
action: action,
|
101
|
+
phase: context[:phase],
|
102
|
+
uploader: self.class,
|
103
|
+
attachment: context[:name],
|
104
|
+
record_class: (context[:record].class if context[:record]),
|
105
|
+
record_id: (context[:record].id if context[:record]),
|
106
|
+
files: count(result),
|
107
|
+
duration: ("%.2f" % duration).to_f,
|
108
|
+
) unless result.nil?
|
109
|
+
|
110
|
+
result
|
111
|
+
end
|
112
|
+
|
113
|
+
def _log(data)
|
114
|
+
message = send("_log_message_#{opts[:logging_format]}", data)
|
115
|
+
self.class.logger.info(message)
|
116
|
+
end
|
117
|
+
|
118
|
+
def _log_message_human(data)
|
119
|
+
components = []
|
120
|
+
components << "#{data[:action].upcase}"
|
121
|
+
components.last << "[#{data[:phase]}]" if data[:phase]
|
122
|
+
components << "#{data[:uploader]}"
|
123
|
+
components.last << "[:#{data[:attachment]}]" if data[:attachment]
|
124
|
+
components << "#{data[:record_class]}[#{data[:record_id]}]" if data[:record_class]
|
125
|
+
components << (data[:files] > 1 ? "#{data[:files]} files" : "#{data[:files]} file")
|
126
|
+
components << "(#{data[:duration]}s)"
|
127
|
+
components.join(" ")
|
128
|
+
end
|
129
|
+
|
130
|
+
def _log_message_json(data)
|
131
|
+
data.to_json
|
132
|
+
end
|
133
|
+
|
134
|
+
def _log_message_heroku(data)
|
135
|
+
data.map { |key, value| "#{key}=#{value}" }.join(" ")
|
136
|
+
end
|
137
|
+
|
138
|
+
# We may have one file, a hash of versions, or an array of files or
|
139
|
+
# hashes.
|
140
|
+
def count(object)
|
141
|
+
case object
|
142
|
+
when Hash then object.count
|
143
|
+
when Array then object.inject(0) { |sum, o| sum += count(o) }
|
144
|
+
else 1
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def benchmark
|
149
|
+
result = nil
|
150
|
+
duration = Benchmark.realtime { result = yield }
|
151
|
+
[result, duration]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
register_plugin(:logging, Logging)
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The migration_helpers plugin gives the model additional helper methods
|
4
|
+
# which are convenient when doing attachment migrations.
|
5
|
+
#
|
6
|
+
# plugin :migration_helpers
|
7
|
+
#
|
8
|
+
# If your attachment's name is "avatar", the model will get `#avatar_cache`
|
9
|
+
# and `#avatar_store` methods.
|
10
|
+
#
|
11
|
+
# user = User.new
|
12
|
+
# user.avatar_cache #=> #<Shrine @storage_key=:cache @storage=#<Shrine::Storage::FileSystem @directory=public/uploads>>
|
13
|
+
# user.avatar_store #=> #<Shrine @storage_key=:store @storage=#<Shrine::Storage::S3:0x007fb8343397c8 @bucket=#<Aws::S3::Bucket name="foo">>>
|
14
|
+
#
|
15
|
+
# The model will also get `#update_avatar` method, which should be used
|
16
|
+
# when doing attachment migrations. It will update the record's attachment
|
17
|
+
# with the result of the passed in block.
|
18
|
+
#
|
19
|
+
# user.update_avatar do |avatar|
|
20
|
+
# user.avatar_store.upload(avatar) # saved to the record
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# This will get triggered _only_ if the attachment exists and is stored.
|
24
|
+
# The result can be anything that responds to `#to_json` and evaluates to
|
25
|
+
# uploaded files' data.
|
26
|
+
module MigrationHelpers
|
27
|
+
module AttachmentMethods
|
28
|
+
def initialize(name)
|
29
|
+
super
|
30
|
+
|
31
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
32
|
+
def update_#{name}(&block)
|
33
|
+
#{name}_attacher.update_stored(&block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def #{name}_cache
|
37
|
+
#{name}_attacher.cache
|
38
|
+
end
|
39
|
+
|
40
|
+
def #{name}_store
|
41
|
+
#{name}_attacher.store
|
42
|
+
end
|
43
|
+
RUBY
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module AttacherMethods
|
48
|
+
# Updates the attachment with the result of the block. It will get
|
49
|
+
# called only if the attachment exists and is stored.
|
50
|
+
def update_stored(&block)
|
51
|
+
attachment = get
|
52
|
+
return if attachment.nil? || cache.uploaded?(attachment)
|
53
|
+
new_attachment = block.call(attachment)
|
54
|
+
update(new_attachment) unless changed?(get)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
register_plugin(:migration_helpers, MigrationHelpers)
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The moving plugin enables you to move files to specified storages. On
|
4
|
+
# the filesystem moving is istantaneous, since the OS only changes the
|
5
|
+
# pointer, so this plugin is useful when dealing with large files.
|
6
|
+
#
|
7
|
+
# This plugin is also recommended if you're doing processing, since by
|
8
|
+
# default temporary files won't immediately get deleted (Ruby's Tempfiles
|
9
|
+
# usually get deleted only when the process ends).
|
10
|
+
#
|
11
|
+
# plugin :moving, storages: [:cache]
|
12
|
+
#
|
13
|
+
# The `:storages` option specifies which storages the file will be moved
|
14
|
+
# to. The above will move Rails's uploaded files to cache (without this
|
15
|
+
# plugin it's simply copied over). However, you may want to move cached
|
16
|
+
# files to `:store` as well:
|
17
|
+
#
|
18
|
+
# plugin :moving, storages: [:cache, :store]
|
19
|
+
#
|
20
|
+
# What exactly means "moving"? Usually this means that the file which is
|
21
|
+
# being uploaded will be deleted afterwards. However, if both the file
|
22
|
+
# being uploaded and the destination are on the filesystem, a `mv` command
|
23
|
+
# will be executed instead. Some other storages may implement moving as
|
24
|
+
# well, usually if also both the `:cache` and `:store` are using the same
|
25
|
+
# storage.
|
26
|
+
module Moving
|
27
|
+
def self.configure(uploader, storages:)
|
28
|
+
uploader.opts[:move_files_to_storages] = storages
|
29
|
+
end
|
30
|
+
|
31
|
+
module InstanceMethods
|
32
|
+
private
|
33
|
+
|
34
|
+
# If the file is movable (usually this means that both the file and
|
35
|
+
# the destination are on the filesystem), use the underlying storage's
|
36
|
+
# ability to move. Otherwise we "imitate" moving by deleting the file
|
37
|
+
# after it was uploaded.
|
38
|
+
def put(io, context)
|
39
|
+
if move?(io, context)
|
40
|
+
if movable?(io, context)
|
41
|
+
move(io, context)
|
42
|
+
else
|
43
|
+
super
|
44
|
+
io.delete if io.respond_to?(:delete)
|
45
|
+
end
|
46
|
+
# Promoting cached files will by default always delete the cached
|
47
|
+
# file. But, if moving plugin is enabled we want the cached file to
|
48
|
+
# be moved instead. However, there is no good way of letting the
|
49
|
+
# Attacher know that it shouldn't attempt to delete the file, so we
|
50
|
+
# make this instance variable hack.
|
51
|
+
io.instance_variable_set("@shrine_deleted", true)
|
52
|
+
else
|
53
|
+
super
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Don't delete the file if it has been moved.
|
58
|
+
def remove(io, context)
|
59
|
+
super unless io.instance_variable_get("@shrine_deleted")
|
60
|
+
end
|
61
|
+
|
62
|
+
# Ask the storage if the given file is movable.
|
63
|
+
def movable?(io, context)
|
64
|
+
storage.respond_to?(:move) && storage.movable?(io, context[:location])
|
65
|
+
end
|
66
|
+
|
67
|
+
def move?(io, context)
|
68
|
+
opts[:move_files_to_storages].include?(storage_key)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
register_plugin(:moving, Moving)
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The multi_delete plugins allows you to leverage your storage's multi
|
4
|
+
# delete capabilities.
|
5
|
+
#
|
6
|
+
# plugin :multi_delete
|
7
|
+
#
|
8
|
+
# This plugin allows you pass an array of files to `Shrine.delete`.
|
9
|
+
#
|
10
|
+
# Shrine.delete([file1, file2, file3])
|
11
|
+
#
|
12
|
+
# Now if you're using Storage::S3, deleting an array of files will issue a
|
13
|
+
# single HTTP request. Some other storages may support multi deletes as
|
14
|
+
# well. The versions plugin uses this plugin for deleting multiple versions
|
15
|
+
# at once.
|
16
|
+
module MultiDelete
|
17
|
+
module InstanceMethods
|
18
|
+
# This allows `Shrine.delete` to accept an array of files.
|
19
|
+
def uploaded?(uploaded_file)
|
20
|
+
if uploaded_file.is_a?(Array)
|
21
|
+
uploaded_file.all? { |file| super(file) }
|
22
|
+
else
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Adds the ability to upload multiple files, leveraging the underlying
|
30
|
+
# storage's potential multi delete capability.
|
31
|
+
def _delete(uploaded_file, context)
|
32
|
+
if uploaded_file.is_a?(Array)
|
33
|
+
if storage.respond_to?(:multi_delete)
|
34
|
+
storage.multi_delete(uploaded_file.map(&:id))
|
35
|
+
else
|
36
|
+
uploaded_file.map { |file| super(file, context) }
|
37
|
+
end
|
38
|
+
else
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
register_plugin(:multi_delete, MultiDelete)
|
46
|
+
end
|
47
|
+
end
|