shrine 0.9.0 → 1.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 +70 -59
- data/doc/carrierwave.md +436 -0
- data/doc/direct_s3.md +20 -1
- data/doc/paperclip.md +308 -0
- data/doc/regenerating_versions.md +76 -11
- data/lib/shrine.rb +42 -46
- data/lib/shrine/plugins/activerecord.rb +12 -6
- data/lib/shrine/plugins/background_helpers.rb +5 -5
- data/lib/shrine/plugins/default_storage.rb +1 -1
- data/lib/shrine/plugins/default_url_options.rb +31 -0
- data/lib/shrine/plugins/determine_mime_type.rb +13 -3
- data/lib/shrine/plugins/direct_upload.rb +55 -106
- data/lib/shrine/plugins/hooks.rb +33 -11
- data/lib/shrine/plugins/keep_files.rb +4 -19
- data/lib/shrine/plugins/logging.rb +16 -11
- data/lib/shrine/plugins/migration_helpers.rb +4 -4
- data/lib/shrine/plugins/module_include.rb +46 -0
- data/lib/shrine/plugins/moving.rb +0 -11
- data/lib/shrine/plugins/multi_delete.rb +3 -12
- data/lib/shrine/plugins/parsed_json.rb +22 -0
- data/lib/shrine/plugins/rack_file.rb +63 -0
- data/lib/shrine/plugins/recache.rb +1 -1
- data/lib/shrine/plugins/restore_cached.rb +1 -2
- data/lib/shrine/plugins/sequel.rb +9 -3
- data/lib/shrine/plugins/validation_helpers.rb +1 -1
- data/lib/shrine/plugins/versions.rb +30 -17
- data/lib/shrine/storage/file_system.rb +62 -17
- data/lib/shrine/storage/linter.rb +8 -3
- data/lib/shrine/storage/s3.rb +84 -20
- data/lib/shrine/version.rb +2 -2
- data/shrine.gemspec +11 -8
- metadata +31 -40
- data/lib/shrine/plugins/delete_invalid.rb +0 -25
data/lib/shrine/plugins/hooks.rb
CHANGED
@@ -16,20 +16,23 @@ class Shrine
|
|
16
16
|
# end
|
17
17
|
# end
|
18
18
|
#
|
19
|
-
# Each hook will be called with 2 arguments, `io` and `context`.
|
20
|
-
#
|
21
|
-
#
|
19
|
+
# Each hook will be called with 2 arguments, `io` and `context`. You should
|
20
|
+
# always call `super` when overriding a hook, as other plugins may be using
|
21
|
+
# hooks internally, and without `super` they wouldn't get executed.
|
22
22
|
#
|
23
23
|
# Shrine calls hooks in the following order when uploading a file:
|
24
24
|
#
|
25
|
-
# * `
|
26
|
-
# * `
|
27
|
-
# *
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
# *
|
32
|
-
#
|
25
|
+
# * `around_upload`
|
26
|
+
# * `before_upload`
|
27
|
+
# * `around_process`
|
28
|
+
# * `before_process`
|
29
|
+
# * PROCESS
|
30
|
+
# * `after_process`
|
31
|
+
# * `around_store`
|
32
|
+
# * `before_store`
|
33
|
+
# * STORE
|
34
|
+
# * `after_store`
|
35
|
+
# * `after_upload`
|
33
36
|
#
|
34
37
|
# Shrine calls hooks in the following order when deleting a file:
|
35
38
|
#
|
@@ -59,6 +62,25 @@ class Shrine
|
|
59
62
|
# existing keys.
|
60
63
|
module Hooks
|
61
64
|
module InstanceMethods
|
65
|
+
def upload(io, context = {})
|
66
|
+
result = nil
|
67
|
+
around_upload(io, context) { result = super }
|
68
|
+
result
|
69
|
+
end
|
70
|
+
|
71
|
+
def around_upload(*args)
|
72
|
+
before_upload(*args)
|
73
|
+
yield
|
74
|
+
after_upload(*args)
|
75
|
+
end
|
76
|
+
|
77
|
+
def before_upload(*)
|
78
|
+
end
|
79
|
+
|
80
|
+
def after_upload(*)
|
81
|
+
end
|
82
|
+
|
83
|
+
|
62
84
|
def processed(io, context)
|
63
85
|
result = nil
|
64
86
|
around_process(io, context) { result = super }
|
@@ -14,37 +14,22 @@ class Shrine
|
|
14
14
|
# :replaced
|
15
15
|
# : If set to `true`, uploading a new attachment won't delete the old one.
|
16
16
|
#
|
17
|
-
# :cached
|
18
|
-
# : If set to `true`, cached files that are uploaded to store won't be
|
19
|
-
# deleted.
|
20
|
-
#
|
21
17
|
# For example, the following will keep destroyed and replaced files:
|
22
18
|
#
|
23
19
|
# plugin :keep_files, destroyed: true, :replaced: true
|
24
20
|
#
|
25
21
|
# [event store]: http://docs.geteventstore.com/introduction/event-sourcing-basics/
|
26
22
|
module KeepFiles
|
27
|
-
def self.configure(uploader, destroyed: nil, replaced: nil,
|
23
|
+
def self.configure(uploader, destroyed: nil, replaced: nil, **)
|
28
24
|
uploader.opts[:keep_files] = []
|
29
25
|
uploader.opts[:keep_files] << :destroyed if destroyed
|
30
26
|
uploader.opts[:keep_files] << :replaced if replaced
|
31
|
-
uploader.opts[:keep_files] << :cached if cached
|
32
27
|
end
|
33
28
|
|
34
|
-
module
|
35
|
-
def keep?(type)
|
36
|
-
opts[:keep_files].include?(type)
|
37
|
-
end
|
38
|
-
|
29
|
+
module InstanceMethods
|
39
30
|
# We hook to the generic deleting, and check the appropriate phases.
|
40
|
-
def delete(io, context)
|
41
|
-
|
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
|
31
|
+
def delete(io, context = {})
|
32
|
+
super unless opts[:keep_files].include?(context[:phase])
|
48
33
|
end
|
49
34
|
end
|
50
35
|
end
|
@@ -79,21 +79,21 @@ class Shrine
|
|
79
79
|
|
80
80
|
module InstanceMethods
|
81
81
|
def store(io, context = {})
|
82
|
-
log("store", context) { super }
|
82
|
+
log("store", io, context) { super }
|
83
83
|
end
|
84
84
|
|
85
|
-
def delete(
|
86
|
-
log("delete", context) { super }
|
85
|
+
def delete(io, context = {})
|
86
|
+
log("delete", io, context) { super }
|
87
87
|
end
|
88
88
|
|
89
89
|
private
|
90
90
|
|
91
91
|
def processed(io, context = {})
|
92
|
-
log("process", context) { super }
|
92
|
+
log("process", io, context) { super }
|
93
93
|
end
|
94
94
|
|
95
95
|
# Collects the data and sends it for logging.
|
96
|
-
def log(action, context)
|
96
|
+
def log(action, io, context)
|
97
97
|
result, duration = benchmark { yield }
|
98
98
|
|
99
99
|
_log(
|
@@ -102,14 +102,15 @@ class Shrine
|
|
102
102
|
uploader: self.class,
|
103
103
|
attachment: context[:name],
|
104
104
|
record_class: (context[:record].class if context[:record]),
|
105
|
-
record_id: (context[:record].id if context[:record]),
|
106
|
-
files: count(
|
105
|
+
record_id: (context[:record].id if context[:record].respond_to?(:id)),
|
106
|
+
files: count(io),
|
107
107
|
duration: ("%.2f" % duration).to_f,
|
108
108
|
) unless result.nil?
|
109
109
|
|
110
110
|
result
|
111
111
|
end
|
112
112
|
|
113
|
+
# Determines format of logging and calls appropriate method.
|
113
114
|
def _log(data)
|
114
115
|
message = send("_log_message_#{opts[:logging_format]}", data)
|
115
116
|
self.class.logger.info(message)
|
@@ -121,7 +122,8 @@ class Shrine
|
|
121
122
|
components.last << "[#{data[:phase]}]" if data[:phase]
|
122
123
|
components << "#{data[:uploader]}"
|
123
124
|
components.last << "[:#{data[:attachment]}]" if data[:attachment]
|
124
|
-
components << "#{data[:record_class]}
|
125
|
+
components << "#{data[:record_class]}" if data[:record_class]
|
126
|
+
components.last << "[#{data[:record_id]}]" if data[:record_id]
|
125
127
|
components << (data[:files] > 1 ? "#{data[:files]} files" : "#{data[:files]} file")
|
126
128
|
components << "(#{data[:duration]}s)"
|
127
129
|
components.join(" ")
|
@@ -139,9 +141,12 @@ class Shrine
|
|
139
141
|
# hashes.
|
140
142
|
def count(object)
|
141
143
|
case object
|
142
|
-
when Hash
|
143
|
-
|
144
|
-
|
144
|
+
when Hash
|
145
|
+
object.count
|
146
|
+
when Array
|
147
|
+
object.inject(0) { |sum, o| sum += count(o) }
|
148
|
+
else
|
149
|
+
1
|
145
150
|
end
|
146
151
|
end
|
147
152
|
|
@@ -20,9 +20,9 @@ class Shrine
|
|
20
20
|
# user.avatar_store.upload(avatar) # saved to the record
|
21
21
|
# end
|
22
22
|
#
|
23
|
-
# This will get triggered _only_ if the attachment
|
24
|
-
# The result can be anything that responds to `#to_json` and
|
25
|
-
# uploaded files' data.
|
23
|
+
# This will get triggered _only_ if the attachment is not nil and is
|
24
|
+
# stored. The result can be anything that responds to `#to_json` and
|
25
|
+
# evaluates to uploaded files' data.
|
26
26
|
module MigrationHelpers
|
27
27
|
module AttachmentMethods
|
28
28
|
def initialize(name)
|
@@ -51,7 +51,7 @@ class Shrine
|
|
51
51
|
attachment = get
|
52
52
|
return if attachment.nil? || cache.uploaded?(attachment)
|
53
53
|
new_attachment = block.call(attachment)
|
54
|
-
update(new_attachment) unless changed?(
|
54
|
+
update(new_attachment) unless changed?(attachment)
|
55
55
|
end
|
56
56
|
end
|
57
57
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The module_include plugin allows you to extend Shrine's core classes for
|
4
|
+
# the given uploader with modules/methods.
|
5
|
+
#
|
6
|
+
# plugin :module_include
|
7
|
+
#
|
8
|
+
# To add a module to a core class, call the appropriate method:
|
9
|
+
#
|
10
|
+
# Shrine.attachment_module CustomAttachmentMethods
|
11
|
+
# Shrine.attacher_module CustomAttacherMethods
|
12
|
+
# Shrine.file_module CustomFileMethods
|
13
|
+
#
|
14
|
+
# Alternatively you can pass in a block (which internally creates a module):
|
15
|
+
#
|
16
|
+
# Shrine.file_module do
|
17
|
+
# def base64
|
18
|
+
# Base64.encode64(read)
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
module ModuleInclude
|
22
|
+
module ClassMethods
|
23
|
+
def attachment_module(mod = nil, &block)
|
24
|
+
module_include(self::Attachment, mod, &block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def attacher_module(mod = nil, &block)
|
28
|
+
module_include(self::Attacher, mod, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def file_module(mod = nil, &block)
|
32
|
+
module_include(self::UploadedFile, mod, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def module_include(klass, mod, &block)
|
38
|
+
mod ||= Module.new(&block)
|
39
|
+
klass.include(mod)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
register_plugin(:module_include, ModuleInclude)
|
45
|
+
end
|
46
|
+
end
|
@@ -43,22 +43,11 @@ class Shrine
|
|
43
43
|
super
|
44
44
|
io.delete if io.respond_to?(:delete)
|
45
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
46
|
else
|
53
47
|
super
|
54
48
|
end
|
55
49
|
end
|
56
50
|
|
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
51
|
# Ask the storage if the given file is movable.
|
63
52
|
def movable?(io, context)
|
64
53
|
storage.respond_to?(:move) && storage.movable?(io, context[:location])
|
@@ -5,9 +5,9 @@ class Shrine
|
|
5
5
|
#
|
6
6
|
# plugin :multi_delete
|
7
7
|
#
|
8
|
-
# This plugin allows you pass an array of files to `Shrine
|
8
|
+
# This plugin allows you pass an array of files to `Shrine#delete`.
|
9
9
|
#
|
10
|
-
# Shrine.delete([file1, file2, file3])
|
10
|
+
# Shrine.new(:storage).delete([file1, file2, file3])
|
11
11
|
#
|
12
12
|
# Now if you're using Storage::S3, deleting an array of files will issue a
|
13
13
|
# single HTTP request. Some other storages may support multi deletes as
|
@@ -15,15 +15,6 @@ class Shrine
|
|
15
15
|
# at once.
|
16
16
|
module MultiDelete
|
17
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
18
|
private
|
28
19
|
|
29
20
|
# Adds the ability to upload multiple files, leveraging the underlying
|
@@ -33,7 +24,7 @@ class Shrine
|
|
33
24
|
if storage.respond_to?(:multi_delete)
|
34
25
|
storage.multi_delete(uploaded_file.map(&:id))
|
35
26
|
else
|
36
|
-
uploaded_file.map { |file|
|
27
|
+
uploaded_file.map { |file| _delete(file, context) }
|
37
28
|
end
|
38
29
|
else
|
39
30
|
super
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The parsed_json plugin is suitable for the case when your framework is
|
4
|
+
# automatically parsing JSON query parameters, allowing you to assign
|
5
|
+
# cached files with hashes/arrays.
|
6
|
+
#
|
7
|
+
# plugin :parsed_json
|
8
|
+
module ParsedJson
|
9
|
+
module AttacherMethods
|
10
|
+
def assign(value)
|
11
|
+
if value.is_a?(Hash) && value.keys.any? { |key| key.is_a?(String) }
|
12
|
+
assign(value.to_json)
|
13
|
+
else
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
register_plugin(:parsed_json, ParsedJson)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
3
|
+
class Shrine
|
4
|
+
module Plugins
|
5
|
+
# The rack_file plugin enables models to accept Rack file hashes as
|
6
|
+
# attachments.
|
7
|
+
#
|
8
|
+
# rack_file #=>
|
9
|
+
# # {
|
10
|
+
# # filename: "cats.png",
|
11
|
+
# # type: "image/png",
|
12
|
+
# # tempfile: #<Tempfile:/var/folders/3n/3asd/-Tmp-/RackMultipart201-1476-nfw2-0>,
|
13
|
+
# # head: "Content-Disposition: form-data; ...",
|
14
|
+
# # }
|
15
|
+
# user.avatar = rack_file
|
16
|
+
# user.avatar.original_filename #=> "cats.png"
|
17
|
+
# user.avatar.mime_type #=> "image/png"
|
18
|
+
#
|
19
|
+
# Internally the plugin wraps the Rack file hash into an IO-like object,
|
20
|
+
# and this is what is passed to `Shrine#upload`.
|
21
|
+
#
|
22
|
+
# plugin :rack_file
|
23
|
+
module RackFile
|
24
|
+
module AttacherMethods
|
25
|
+
# Checks whether a file is a Rack file hash, and in that case wraps the
|
26
|
+
# hash in an IO-like object.
|
27
|
+
def assign(value)
|
28
|
+
if value.is_a?(Hash) && value.key?(:tempfile)
|
29
|
+
assign(UploadedFile.new(value))
|
30
|
+
else
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# This is used to wrap the Rack hash into an IO-like object which Shrine
|
37
|
+
# can upload.
|
38
|
+
class UploadedFile
|
39
|
+
attr_reader :original_filename, :content_type
|
40
|
+
attr_accessor :tempfile
|
41
|
+
|
42
|
+
def initialize(tempfile:, filename: nil, type: nil, **)
|
43
|
+
@tempfile = tempfile
|
44
|
+
@original_filename = filename
|
45
|
+
@content_type = type
|
46
|
+
end
|
47
|
+
|
48
|
+
def path
|
49
|
+
@tempfile.path
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_io
|
53
|
+
@tempfile
|
54
|
+
end
|
55
|
+
|
56
|
+
extend Forwardable
|
57
|
+
delegate Shrine::IO_METHODS.keys => :@tempfile
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
register_plugin(:rack_file, RackFile)
|
62
|
+
end
|
63
|
+
end
|
@@ -21,8 +21,7 @@ class Shrine
|
|
21
21
|
uploaded_file = uploaded_file(value) do |file|
|
22
22
|
next unless cache.uploaded?(file)
|
23
23
|
return unless file.exists?
|
24
|
-
|
25
|
-
real_metadata = uploader.extract_metadata(file.to_io, context)
|
24
|
+
real_metadata = cache.extract_metadata(file.to_io, context)
|
26
25
|
file.metadata.update(real_metadata)
|
27
26
|
end
|
28
27
|
|
@@ -45,13 +45,12 @@ class Shrine
|
|
45
45
|
|
46
46
|
def before_save
|
47
47
|
super
|
48
|
-
#{name}_attacher.save
|
48
|
+
#{name}_attacher.save if #{name}_attacher.attached?
|
49
49
|
end
|
50
50
|
|
51
51
|
def after_commit
|
52
52
|
super
|
53
|
-
#{name}_attacher.
|
54
|
-
#{name}_attacher._promote
|
53
|
+
#{name}_attacher.finalize if #{name}_attacher.attached?
|
55
54
|
end
|
56
55
|
|
57
56
|
def after_destroy_commit
|
@@ -86,6 +85,13 @@ class Shrine
|
|
86
85
|
record.reload
|
87
86
|
super
|
88
87
|
end
|
88
|
+
|
89
|
+
# Support for Postgres JSON columns.
|
90
|
+
def read
|
91
|
+
value = super
|
92
|
+
value = value.to_hash if value.respond_to?(:to_hash)
|
93
|
+
value
|
94
|
+
end
|
89
95
|
end
|
90
96
|
end
|
91
97
|
|
@@ -15,7 +15,7 @@ class Shrine
|
|
15
15
|
#
|
16
16
|
# The validation methods are instance-level, the `Attacher.validate` block
|
17
17
|
# is evaluated in context of an instance of `Shrine::Attacher`, so you can
|
18
|
-
# easily
|
18
|
+
# easily do conditional validation.
|
19
19
|
#
|
20
20
|
# If you would like to change default validation error messages, you can
|
21
21
|
# pass in the `:default_messages` option to the plugin:
|