shrine 3.1.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -0
- data/README.md +11 -4
- data/doc/advantages.md +4 -4
- data/doc/attacher.md +2 -2
- data/doc/carrierwave.md +24 -12
- data/doc/changing_derivatives.md +1 -1
- data/doc/changing_location.md +6 -5
- data/doc/design.md +134 -85
- data/doc/direct_s3.md +26 -0
- data/doc/external/articles.md +57 -45
- data/doc/external/extensions.md +41 -35
- data/doc/external/misc.md +23 -8
- data/doc/getting_started.md +156 -85
- data/doc/metadata.md +80 -44
- data/doc/multiple_files.md +1 -1
- data/doc/paperclip.md +28 -9
- data/doc/plugins/add_metadata.md +112 -35
- data/doc/plugins/atomic_helpers.md +41 -3
- data/doc/plugins/backgrounding.md +12 -2
- data/doc/plugins/column.md +36 -7
- data/doc/plugins/default_url.md +6 -3
- data/doc/plugins/derivatives.md +83 -44
- data/doc/plugins/download_endpoint.md +5 -5
- data/doc/plugins/dynamic_storage.md +1 -1
- data/doc/plugins/entity.md +12 -4
- data/doc/plugins/form_assign.md +5 -5
- data/doc/plugins/included.md +25 -5
- data/doc/plugins/infer_extension.md +9 -0
- data/doc/plugins/instrumentation.md +1 -1
- data/doc/plugins/metadata_attributes.md +1 -0
- data/doc/plugins/mirroring.md +1 -1
- data/doc/plugins/model.md +8 -3
- data/doc/plugins/persistence.md +10 -1
- data/doc/plugins/remote_url.md +6 -1
- data/doc/plugins/remove_invalid.md +9 -1
- data/doc/plugins/sequel.md +1 -1
- data/doc/plugins/store_dimensions.md +10 -0
- data/doc/plugins/type_predicates.md +96 -0
- data/doc/plugins/upload_endpoint.md +1 -1
- data/doc/plugins/upload_options.md +1 -1
- data/doc/plugins/url_options.md +4 -4
- data/doc/plugins/validation.md +14 -4
- data/doc/plugins/versions.md +7 -7
- data/doc/processing.md +287 -123
- data/doc/refile.md +9 -9
- data/doc/release_notes/2.8.0.md +1 -1
- data/doc/release_notes/3.0.0.md +1 -1
- data/doc/release_notes/3.2.0.md +96 -0
- data/doc/release_notes/3.2.1.md +31 -0
- data/doc/release_notes/3.2.2.md +14 -0
- data/doc/release_notes/3.3.0.md +105 -0
- data/doc/release_notes/3.4.0.md +35 -0
- data/doc/securing_uploads.md +2 -2
- data/doc/storage/memory.md +19 -0
- data/doc/storage/s3.md +104 -77
- data/doc/testing.md +12 -2
- data/doc/upgrading_to_3.md +99 -53
- data/lib/shrine.rb +9 -8
- data/lib/shrine/attacher.rb +20 -10
- data/lib/shrine/attachment.rb +2 -2
- data/lib/shrine/plugins.rb +22 -0
- data/lib/shrine/plugins/activerecord.rb +3 -3
- data/lib/shrine/plugins/add_metadata.rb +20 -5
- data/lib/shrine/plugins/backgrounding.rb +2 -2
- data/lib/shrine/plugins/default_url.rb +1 -1
- data/lib/shrine/plugins/derivation_endpoint.rb +13 -8
- data/lib/shrine/plugins/derivatives.rb +59 -30
- data/lib/shrine/plugins/determine_mime_type.rb +5 -3
- data/lib/shrine/plugins/entity.rb +12 -11
- data/lib/shrine/plugins/instrumentation.rb +12 -18
- data/lib/shrine/plugins/mirroring.rb +8 -8
- data/lib/shrine/plugins/model.rb +3 -3
- data/lib/shrine/plugins/presign_endpoint.rb +16 -4
- data/lib/shrine/plugins/pretty_location.rb +1 -1
- data/lib/shrine/plugins/processing.rb +1 -1
- data/lib/shrine/plugins/refresh_metadata.rb +2 -2
- data/lib/shrine/plugins/remote_url.rb +3 -3
- data/lib/shrine/plugins/remove_attachment.rb +5 -0
- data/lib/shrine/plugins/remove_invalid.rb +10 -5
- data/lib/shrine/plugins/sequel.rb +1 -1
- data/lib/shrine/plugins/store_dimensions.rb +4 -2
- data/lib/shrine/plugins/type_predicates.rb +113 -0
- data/lib/shrine/plugins/upload_endpoint.rb +10 -5
- data/lib/shrine/plugins/upload_options.rb +2 -2
- data/lib/shrine/plugins/url_options.rb +2 -2
- data/lib/shrine/plugins/validation.rb +9 -7
- data/lib/shrine/storage/linter.rb +4 -4
- data/lib/shrine/storage/memory.rb +5 -3
- data/lib/shrine/storage/s3.rb +117 -38
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +8 -8
- metadata +42 -34
@@ -69,27 +69,25 @@ class Shrine
|
|
69
69
|
|
70
70
|
# Sends a `upload.shrine` event.
|
71
71
|
def _upload(io, location:, metadata:, upload_options: {}, **options)
|
72
|
-
self.class.instrument(
|
73
|
-
:upload,
|
72
|
+
self.class.instrument(:upload, {
|
74
73
|
storage: storage_key,
|
75
74
|
location: location,
|
76
75
|
io: io,
|
77
76
|
upload_options: upload_options,
|
78
77
|
metadata: metadata,
|
79
78
|
options: options,
|
80
|
-
) { super }
|
79
|
+
}) { super }
|
81
80
|
end
|
82
81
|
|
83
82
|
# Sends a `metadata.shrine` event.
|
84
83
|
def get_metadata(io, metadata: nil, **options)
|
85
84
|
return super if io.is_a?(UploadedFile) && metadata != true || metadata == false
|
86
85
|
|
87
|
-
self.class.instrument(
|
88
|
-
:metadata,
|
86
|
+
self.class.instrument(:metadata, {
|
89
87
|
storage: storage_key,
|
90
88
|
io: io,
|
91
89
|
options: options,
|
92
|
-
) { super }
|
90
|
+
}) { super }
|
93
91
|
end
|
94
92
|
end
|
95
93
|
|
@@ -98,30 +96,27 @@ class Shrine
|
|
98
96
|
def stream(destination, **options)
|
99
97
|
return super if opened?
|
100
98
|
|
101
|
-
shrine_class.instrument(
|
102
|
-
:download,
|
99
|
+
shrine_class.instrument(:download, {
|
103
100
|
storage: storage_key,
|
104
101
|
location: id,
|
105
102
|
download_options: options,
|
106
|
-
) { super(destination, **options, instrument: false) }
|
103
|
+
}) { super(destination, **options, instrument: false) }
|
107
104
|
end
|
108
105
|
|
109
106
|
# Sends a `exists.shrine` event.
|
110
107
|
def exists?
|
111
|
-
shrine_class.instrument(
|
112
|
-
:exists,
|
108
|
+
shrine_class.instrument(:exists, {
|
113
109
|
storage: storage_key,
|
114
110
|
location: id,
|
115
|
-
) { super }
|
111
|
+
}) { super }
|
116
112
|
end
|
117
113
|
|
118
114
|
# Sends a `delete.shrine` event.
|
119
115
|
def delete
|
120
|
-
shrine_class.instrument(
|
121
|
-
:delete,
|
116
|
+
shrine_class.instrument(:delete, {
|
122
117
|
storage: storage_key,
|
123
118
|
location: id,
|
124
|
-
) { super }
|
119
|
+
}) { super }
|
125
120
|
end
|
126
121
|
|
127
122
|
private
|
@@ -130,12 +125,11 @@ class Shrine
|
|
130
125
|
def _open(instrument: true, **options)
|
131
126
|
return super(**options) unless instrument
|
132
127
|
|
133
|
-
shrine_class.instrument(
|
134
|
-
:open,
|
128
|
+
shrine_class.instrument(:open, {
|
135
129
|
storage: storage_key,
|
136
130
|
location: id,
|
137
131
|
download_options: options,
|
138
|
-
) { super(**options) }
|
132
|
+
}) { super(**options) }
|
139
133
|
end
|
140
134
|
end
|
141
135
|
|
@@ -53,7 +53,7 @@ class Shrine
|
|
53
53
|
# Mirrors upload to other mirror storages.
|
54
54
|
def upload(io, mirror: true, **options)
|
55
55
|
file = super(io, **options)
|
56
|
-
file.trigger_mirror_upload if mirror
|
56
|
+
file.trigger_mirror_upload(**options) if mirror
|
57
57
|
file
|
58
58
|
end
|
59
59
|
end
|
@@ -61,31 +61,31 @@ class Shrine
|
|
61
61
|
module FileMethods
|
62
62
|
# Mirrors upload if mirrors are defined. Calls mirror block if
|
63
63
|
# registered, otherwise mirrors synchronously.
|
64
|
-
def trigger_mirror_upload
|
64
|
+
def trigger_mirror_upload(**options)
|
65
65
|
return unless shrine_class.mirrors[storage_key] && shrine_class.mirror_upload?
|
66
66
|
|
67
67
|
if shrine_class.mirror_upload_block
|
68
|
-
mirror_upload_background
|
68
|
+
mirror_upload_background(**options)
|
69
69
|
else
|
70
|
-
mirror_upload
|
70
|
+
mirror_upload(**options)
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
74
74
|
# Calls mirror upload block.
|
75
|
-
def mirror_upload_background
|
75
|
+
def mirror_upload_background(**options)
|
76
76
|
fail Error, "mirror upload block is not registered" unless shrine_class.mirror_upload_block
|
77
77
|
|
78
|
-
shrine_class.mirror_upload_block.call(self)
|
78
|
+
shrine_class.mirror_upload_block.call(self, **options)
|
79
79
|
end
|
80
80
|
|
81
81
|
# Uploads the file to each mirror storage.
|
82
|
-
def mirror_upload
|
82
|
+
def mirror_upload(**options)
|
83
83
|
previously_opened = opened?
|
84
84
|
|
85
85
|
each_mirror do |mirror|
|
86
86
|
rewind if opened?
|
87
87
|
|
88
|
-
shrine_class.upload(self, mirror, location: id, close: false, action: :mirror)
|
88
|
+
shrine_class.upload(self, mirror, **options, location: id, close: false, action: :mirror)
|
89
89
|
end
|
90
90
|
ensure
|
91
91
|
if opened? && !previously_opened
|
data/lib/shrine/plugins/model.rb
CHANGED
@@ -55,11 +55,11 @@ class Shrine
|
|
55
55
|
end
|
56
56
|
|
57
57
|
# Memoizes the attacher instance into an instance variable.
|
58
|
-
def attacher(record, options)
|
58
|
+
def attacher(record, **options)
|
59
59
|
return super unless model?
|
60
60
|
|
61
61
|
if !record.instance_variable_get(:"@#{@name}_attacher") || options.any?
|
62
|
-
attacher = class_attacher(options)
|
62
|
+
attacher = class_attacher(**options)
|
63
63
|
attacher.load_model(record, @name)
|
64
64
|
|
65
65
|
record.instance_variable_set(:"@#{@name}_attacher", attacher)
|
@@ -118,7 +118,7 @@ class Shrine
|
|
118
118
|
end
|
119
119
|
|
120
120
|
# Writes uploaded file data into the model.
|
121
|
-
def set(*
|
121
|
+
def set(*)
|
122
122
|
result = super
|
123
123
|
write if model?
|
124
124
|
result
|
@@ -8,7 +8,7 @@ class Shrine
|
|
8
8
|
module Plugins
|
9
9
|
# Documentation can be found on https://shrinerb.com/docs/plugins/presign_endpoint
|
10
10
|
module PresignEndpoint
|
11
|
-
def self.configure(uploader, opts
|
11
|
+
def self.configure(uploader, **opts)
|
12
12
|
uploader.opts[:presign_endpoint] ||= {}
|
13
13
|
uploader.opts[:presign_endpoint].merge!(opts)
|
14
14
|
end
|
@@ -81,9 +81,14 @@ class Shrine
|
|
81
81
|
|
82
82
|
status, headers, body = catch(:halt) do
|
83
83
|
error!(404, "Not Found") unless ["", "/"].include?(request.path_info)
|
84
|
-
error!(405, "Method Not Allowed") unless request.get?
|
85
84
|
|
86
|
-
|
85
|
+
if request.get?
|
86
|
+
handle_request(request)
|
87
|
+
elsif request.options?
|
88
|
+
handle_options_request(request)
|
89
|
+
else
|
90
|
+
error!(405, "Method Not Allowed")
|
91
|
+
end
|
87
92
|
end
|
88
93
|
|
89
94
|
headers["Content-Length"] ||= body.map(&:bytesize).inject(0, :+).to_s
|
@@ -109,6 +114,13 @@ class Shrine
|
|
109
114
|
make_response(presign, request)
|
110
115
|
end
|
111
116
|
|
117
|
+
# Uppy client sends an OPTIONS request to fetch information about the
|
118
|
+
# Uppy Companion. Since our Rack app is only acting as Uppy Companion, we
|
119
|
+
# just return a successful response.
|
120
|
+
def handle_options_request(request)
|
121
|
+
[200, {}, []]
|
122
|
+
end
|
123
|
+
|
112
124
|
# Generates the location using `Shrine#generate_uid`, and extracts the
|
113
125
|
# extension from the `filename` query parameter. If `:presign_location`
|
114
126
|
# option is given, calls that instead.
|
@@ -135,7 +147,7 @@ class Shrine
|
|
135
147
|
if @presign
|
136
148
|
data = @presign.call(location, options, request)
|
137
149
|
else
|
138
|
-
data = storage.presign(location, options)
|
150
|
+
data = storage.presign(location, **options)
|
139
151
|
end
|
140
152
|
|
141
153
|
{ fields: {}, headers: {} }.merge(data.to_h)
|
@@ -11,7 +11,7 @@ class Shrine
|
|
11
11
|
|
12
12
|
module InstanceMethods
|
13
13
|
def generate_location(io, **options)
|
14
|
-
pretty_location(io, options)
|
14
|
+
pretty_location(io, **options)
|
15
15
|
end
|
16
16
|
|
17
17
|
def pretty_location(io, name: nil, record: nil, version: nil, derivative: nil, identifier: nil, metadata: {}, **)
|
@@ -33,7 +33,7 @@ class Shrine
|
|
33
33
|
def process(io, **options)
|
34
34
|
pipeline = processing_pipeline(options[:action])
|
35
35
|
pipeline.inject(io) do |input, processor|
|
36
|
-
instance_exec(input, options, &processor) || input
|
36
|
+
instance_exec(input, **options, &processor) || input
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
@@ -16,7 +16,7 @@ class Shrine
|
|
16
16
|
}.inspect}"
|
17
17
|
end
|
18
18
|
|
19
|
-
DOWNLOADER = -> (url, options) { Down.download(url, options) }
|
19
|
+
DOWNLOADER = -> (url, **options) { Down.download(url, **options) }
|
20
20
|
|
21
21
|
def self.load_dependencies(uploader, *)
|
22
22
|
uploader.plugin :validation
|
@@ -63,7 +63,7 @@ class Shrine
|
|
63
63
|
private
|
64
64
|
|
65
65
|
def download_remote_url(url, options)
|
66
|
-
opts[:remote_url][:downloader].call(url, options)
|
66
|
+
opts[:remote_url][:downloader].call(url, **options)
|
67
67
|
rescue Down::TooLarge
|
68
68
|
fail DownloadError, "remote file too large"
|
69
69
|
rescue Down::Error
|
@@ -87,7 +87,7 @@ class Shrine
|
|
87
87
|
def assign_remote_url(url, downloader: {}, **options)
|
88
88
|
return if url == "" || url.nil?
|
89
89
|
|
90
|
-
downloaded_file = shrine_class.remote_url(url, downloader)
|
90
|
+
downloaded_file = shrine_class.remote_url(url, **downloader)
|
91
91
|
attach_cached(downloaded_file, **options)
|
92
92
|
rescue DownloadError => error
|
93
93
|
errors.clear << remote_url_error_message(url, error)
|
@@ -32,6 +32,11 @@ class Shrine
|
|
32
32
|
|
33
33
|
private
|
34
34
|
|
35
|
+
# Don't override previously removed attachment that wasn't yet deleted.
|
36
|
+
def change?(file)
|
37
|
+
super && !(changed? && remove?)
|
38
|
+
end
|
39
|
+
|
35
40
|
# Rails sends "0" or "false" if the checkbox hasn't been ticked.
|
36
41
|
def remove?
|
37
42
|
remove && remove != "" && remove !~ /\A(0|false)\z/
|
@@ -9,18 +9,23 @@ class Shrine
|
|
9
9
|
end
|
10
10
|
|
11
11
|
module AttacherMethods
|
12
|
-
def
|
12
|
+
def validate(*)
|
13
13
|
super
|
14
14
|
ensure
|
15
|
-
|
15
|
+
deassign if errors.any?
|
16
16
|
end
|
17
17
|
|
18
18
|
private
|
19
19
|
|
20
|
-
def
|
20
|
+
def deassign
|
21
21
|
destroy
|
22
|
-
|
23
|
-
|
22
|
+
|
23
|
+
if changed?
|
24
|
+
load_data @previous.data
|
25
|
+
@previous = nil
|
26
|
+
else
|
27
|
+
load_data nil
|
28
|
+
end
|
24
29
|
end
|
25
30
|
end
|
26
31
|
end
|
@@ -62,7 +62,7 @@ class Shrine
|
|
62
62
|
# reload the attacher on record reload
|
63
63
|
define_method :_refresh do |*args|
|
64
64
|
result = super(*args)
|
65
|
-
|
65
|
+
send(:"#{name}_attacher").reload if instance_variable_defined?(:"@#{name}_attacher")
|
66
66
|
result
|
67
67
|
end
|
68
68
|
private :_refresh
|
@@ -12,7 +12,7 @@ class Shrine
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, **opts)
|
15
|
-
uploader.opts[:store_dimensions] ||= { analyzer: :fastimage, on_error: :warn }
|
15
|
+
uploader.opts[:store_dimensions] ||= { analyzer: :fastimage, on_error: :warn, auto_extraction: true }
|
16
16
|
uploader.opts[:store_dimensions].merge!(opts)
|
17
17
|
|
18
18
|
# resolve error strategy
|
@@ -71,8 +71,10 @@ class Shrine
|
|
71
71
|
end
|
72
72
|
|
73
73
|
module InstanceMethods
|
74
|
-
# We update the metadata with "width" and "height".
|
75
74
|
def extract_metadata(io, **options)
|
75
|
+
return super unless opts[:store_dimensions][:auto_extraction]
|
76
|
+
|
77
|
+
# We update the metadata with "width" and "height".
|
76
78
|
width, height = self.class.extract_dimensions(io)
|
77
79
|
|
78
80
|
super.merge!("width" => width, "height" => height)
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Shrine
|
4
|
+
module Plugins
|
5
|
+
# Documentation can be found on https://shrinerb.com/docs/plugins/type_predicates
|
6
|
+
module TypePredicates
|
7
|
+
def self.configure(uploader, methods: [], **opts)
|
8
|
+
uploader.opts[:type_predicates] ||= { mime: :mini_mime }
|
9
|
+
uploader.opts[:type_predicates].merge!(opts)
|
10
|
+
|
11
|
+
methods.each do |name|
|
12
|
+
uploader::UploadedFile.send(:define_method, "#{name}?") { type?(name) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def type_lookup(extension, database = nil)
|
18
|
+
database ||= opts[:type_predicates][:mime]
|
19
|
+
database = MimeDatabase.new(database) if database.is_a?(Symbol)
|
20
|
+
database.call(extension.to_s)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module FileMethods
|
25
|
+
def image?
|
26
|
+
general_type?("image")
|
27
|
+
end
|
28
|
+
|
29
|
+
def video?
|
30
|
+
general_type?("video")
|
31
|
+
end
|
32
|
+
|
33
|
+
def audio?
|
34
|
+
general_type?("audio")
|
35
|
+
end
|
36
|
+
|
37
|
+
def text?
|
38
|
+
general_type?("text")
|
39
|
+
end
|
40
|
+
|
41
|
+
def type?(type)
|
42
|
+
matching_mime_type = shrine_class.type_lookup(type)
|
43
|
+
|
44
|
+
fail Error, "type #{type.inspect} is not recognized by the MIME library" unless matching_mime_type
|
45
|
+
|
46
|
+
mime_type! == matching_mime_type
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def general_type?(type)
|
52
|
+
mime_type!.start_with?(type)
|
53
|
+
end
|
54
|
+
|
55
|
+
def mime_type!
|
56
|
+
mime_type or fail Error, "mime_type metadata value is missing"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class MimeDatabase
|
61
|
+
SUPPORTED_TOOLS = %i[mini_mime mime_types mimemagic marcel rack_mime]
|
62
|
+
|
63
|
+
def initialize(tool)
|
64
|
+
raise Error, "unknown type database #{tool.inspect}, supported databases are: #{SUPPORTED_TOOLS.join(",")}" unless SUPPORTED_TOOLS.include?(tool)
|
65
|
+
|
66
|
+
@tool = tool
|
67
|
+
end
|
68
|
+
|
69
|
+
def call(extension)
|
70
|
+
send(:"lookup_with_#{@tool}", extension)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def lookup_with_mini_mime(extension)
|
76
|
+
require "mini_mime"
|
77
|
+
|
78
|
+
info = MiniMime.lookup_by_extension(extension)
|
79
|
+
info&.content_type
|
80
|
+
end
|
81
|
+
|
82
|
+
def lookup_with_mime_types(extension)
|
83
|
+
require "mime/types"
|
84
|
+
|
85
|
+
mime_type = MIME::Types.of(".#{extension}").first
|
86
|
+
mime_type&.content_type
|
87
|
+
end
|
88
|
+
|
89
|
+
def lookup_with_mimemagic(extension)
|
90
|
+
require "mimemagic"
|
91
|
+
|
92
|
+
magic = MimeMagic.by_extension(".#{extension}")
|
93
|
+
magic&.type
|
94
|
+
end
|
95
|
+
|
96
|
+
def lookup_with_marcel(extension)
|
97
|
+
require "marcel"
|
98
|
+
|
99
|
+
type = Marcel::MimeType.for(extension: ".#{extension}")
|
100
|
+
type unless type == "application/octet-stream"
|
101
|
+
end
|
102
|
+
|
103
|
+
def lookup_with_rack_mime(extension)
|
104
|
+
require "rack/mime"
|
105
|
+
|
106
|
+
Rack::Mime.mime_type(".#{extension}", nil)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
register_plugin(:type_predicates, TypePredicates)
|
112
|
+
end
|
113
|
+
end
|