shrine 2.13.0 → 2.14.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/CHANGELOG.md +72 -0
- data/README.md +20 -16
- data/doc/creating_storages.md +0 -21
- data/doc/design.md +1 -0
- data/doc/direct_s3.md +26 -15
- data/doc/metadata.md +67 -22
- data/doc/multiple_files.md +3 -3
- data/doc/processing.md +1 -1
- data/doc/retrieving_uploads.md +184 -0
- data/lib/shrine.rb +268 -900
- data/lib/shrine/attacher.rb +271 -0
- data/lib/shrine/attachment.rb +97 -0
- data/lib/shrine/plugins.rb +29 -0
- data/lib/shrine/plugins/_urlsafe_serialization.rb +182 -0
- data/lib/shrine/plugins/activerecord.rb +16 -14
- data/lib/shrine/plugins/add_metadata.rb +58 -24
- data/lib/shrine/plugins/backgrounding.rb +6 -1
- data/lib/shrine/plugins/cached_attachment_data.rb +9 -9
- data/lib/shrine/plugins/copy.rb +12 -8
- data/lib/shrine/plugins/data_uri.rb +23 -20
- data/lib/shrine/plugins/default_url_options.rb +5 -4
- data/lib/shrine/plugins/determine_mime_type.rb +24 -23
- data/lib/shrine/plugins/download_endpoint.rb +61 -73
- data/lib/shrine/plugins/migration_helpers.rb +17 -17
- data/lib/shrine/plugins/module_include.rb +9 -8
- data/lib/shrine/plugins/presign_endpoint.rb +13 -7
- data/lib/shrine/plugins/processing.rb +1 -1
- data/lib/shrine/plugins/rack_response.rb +128 -36
- data/lib/shrine/plugins/refresh_metadata.rb +20 -5
- data/lib/shrine/plugins/remote_url.rb +8 -8
- data/lib/shrine/plugins/remove_attachment.rb +9 -9
- data/lib/shrine/plugins/sequel.rb +21 -18
- data/lib/shrine/plugins/tempfile.rb +68 -0
- data/lib/shrine/plugins/upload_endpoint.rb +3 -2
- data/lib/shrine/plugins/upload_options.rb +7 -6
- data/lib/shrine/plugins/validation_helpers.rb +2 -1
- data/lib/shrine/storage/file_system.rb +20 -17
- data/lib/shrine/storage/linter.rb +0 -7
- data/lib/shrine/storage/s3.rb +159 -50
- data/lib/shrine/uploaded_file.rb +258 -0
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +7 -19
- metadata +41 -21
@@ -65,27 +65,27 @@ class Shrine
|
|
65
65
|
|
66
66
|
return if shrine_class.opts[:migration_helpers_delegate] == false
|
67
67
|
|
68
|
-
|
69
|
-
def update_#{name}(&block)
|
70
|
-
#{name}_attacher.update_stored(&block)
|
71
|
-
end
|
68
|
+
name = attachment_name
|
72
69
|
|
73
|
-
|
74
|
-
|
75
|
-
|
70
|
+
define_method :"update_#{name}" do |&block|
|
71
|
+
send(:"#{name}_attacher").update_stored(&block)
|
72
|
+
end
|
76
73
|
|
77
|
-
|
78
|
-
|
79
|
-
|
74
|
+
define_method :"#{name}_cache" do
|
75
|
+
send(:"#{name}_attacher").cache
|
76
|
+
end
|
80
77
|
|
81
|
-
|
82
|
-
|
83
|
-
|
78
|
+
define_method :"#{name}_store" do
|
79
|
+
send(:"#{name}_attacher").store
|
80
|
+
end
|
84
81
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
82
|
+
define_method :"#{name}_cached?" do
|
83
|
+
send(:"#{name}_attacher").cached?
|
84
|
+
end
|
85
|
+
|
86
|
+
define_method :"#{name}_stored?" do
|
87
|
+
send(:"#{name}_attacher").stored?
|
88
|
+
end
|
89
89
|
end
|
90
90
|
end
|
91
91
|
|
@@ -19,15 +19,16 @@ class Shrine
|
|
19
19
|
# def included(model)
|
20
20
|
# super
|
21
21
|
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
22
|
+
# name = attachment_name
|
23
|
+
#
|
24
|
+
# define_method :"#{name}_size" do |version|
|
25
|
+
# attachment = send(name)
|
26
|
+
# if attachment.is_a?(Hash)
|
27
|
+
# attachment[version].size
|
28
|
+
# elsif attachment
|
29
|
+
# attachment.size
|
29
30
|
# end
|
30
|
-
#
|
31
|
+
# end
|
31
32
|
# end
|
32
33
|
# end
|
33
34
|
#
|
@@ -75,16 +75,20 @@ class Shrine
|
|
75
75
|
# `:presign_options`, here is an example for S3 storage:
|
76
76
|
#
|
77
77
|
# plugin :presign_endpoint, presign_options: -> (request) do
|
78
|
-
#
|
79
|
-
#
|
78
|
+
# # Uppy will send the "filename" and "type" query parameters
|
79
|
+
# filename = request.params["filename"]
|
80
|
+
# type = request.params["type"]
|
80
81
|
#
|
81
82
|
# {
|
82
|
-
# content_length_range: 0..(10*1024*1024),
|
83
|
-
# content_disposition:
|
84
|
-
# content_type: type,
|
83
|
+
# content_length_range: 0..(10*1024*1024), # limit filesize to 10MB
|
84
|
+
# content_disposition: ContentDisposition.inline(filename), # download with original filename
|
85
|
+
# content_type: type, # set correct content type
|
85
86
|
# }
|
86
87
|
# end
|
87
88
|
#
|
89
|
+
# The example above uses the [content_disposition] gem to correctly format
|
90
|
+
# the `Content-Disposition` header value.
|
91
|
+
#
|
88
92
|
# The `:presign_options` can be a Proc or a Hash.
|
89
93
|
#
|
90
94
|
# ## Presign
|
@@ -116,6 +120,7 @@ class Shrine
|
|
116
120
|
# [Amazon S3]: https://aws.amazon.com/s3/
|
117
121
|
# [Google Cloud Storage]: https://cloud.google.com/storage/
|
118
122
|
# [Microsoft Azure Storage]: https://azure.microsoft.com/en-us/services/storage/
|
123
|
+
# [content_disposition]: https://github.com/shrinerb/content_disposition
|
119
124
|
module PresignEndpoint
|
120
125
|
def self.configure(uploader, opts = {})
|
121
126
|
uploader.opts[:presign_endpoint_presign_location] = opts.fetch(:presign_location, uploader.opts[:presign_endpoint_presign_location])
|
@@ -133,14 +138,15 @@ class Shrine
|
|
133
138
|
# Additional options can be given to override the options given on
|
134
139
|
# plugin initialization.
|
135
140
|
def presign_endpoint(storage_key, **options)
|
136
|
-
App.new(
|
141
|
+
App.new(
|
137
142
|
shrine_class: self,
|
138
143
|
storage_key: storage_key,
|
139
144
|
presign_location: opts[:presign_endpoint_presign_location],
|
140
145
|
presign_options: opts[:presign_endpoint_presign_options],
|
141
146
|
presign: opts[:presign_endpoint_presign],
|
142
147
|
rack_response: opts[:presign_endpoint_rack_response],
|
143
|
-
|
148
|
+
**options
|
149
|
+
)
|
144
150
|
end
|
145
151
|
end
|
146
152
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "rack"
|
4
|
+
require "content_disposition"
|
4
5
|
|
5
6
|
class Shrine
|
6
7
|
module Plugins
|
@@ -29,27 +30,52 @@ class Shrine
|
|
29
30
|
# class FilesController < ActionController::Base
|
30
31
|
# def download
|
31
32
|
# # ...
|
32
|
-
#
|
33
|
+
# set_rack_response record.attachment.to_rack_response
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# private
|
33
37
|
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
# self.
|
38
|
+
# def set_rack_response((status, headers, body))
|
39
|
+
# self.status = status
|
40
|
+
# self.headers.merge!(headers)
|
41
|
+
# self.response_body = body
|
37
42
|
# end
|
38
43
|
# end
|
39
44
|
#
|
45
|
+
# The `#each` method on the response body object will stream the uploaded
|
46
|
+
# file directly from the storage. It also works with [Rack::Sendfile] when
|
47
|
+
# using `FileSystem` storage.
|
48
|
+
#
|
49
|
+
# ## Type
|
50
|
+
#
|
51
|
+
# The response `Content-Type` header will default to the value of the
|
52
|
+
# `mime_type` metadata. A custom content type can be provided via the
|
53
|
+
# `:type` option:
|
54
|
+
#
|
55
|
+
# _, headers, _ uploaded_file.to_rack_response(type: "text/plain; charset=utf-8")
|
56
|
+
# headers["Content-Type"] #=> "text/plain; charset=utf-8"
|
57
|
+
#
|
58
|
+
# ## Filename
|
59
|
+
#
|
60
|
+
# The download filename in the `Content-Disposition` header will default to
|
61
|
+
# the value of the `filename` metadata. A custom download filename can be
|
62
|
+
# provided via the `:filename` option:
|
63
|
+
#
|
64
|
+
# _, headers, _ uploaded_file.to_rack_response(filename: "my-filename.txt")
|
65
|
+
# headers["Content-Disposition"] #=> "inline; filename=\"my-filename.txt\""
|
66
|
+
#
|
40
67
|
# ## Disposition
|
41
68
|
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
# file to be rendered inside the browser:
|
69
|
+
# The default disposition in the "Content-Disposition" header is `inline`,
|
70
|
+
# but it can be changed via the `:disposition` option:
|
45
71
|
#
|
46
|
-
#
|
72
|
+
# _, headers, _ = uploaded_file.to_rack_response(disposition: "attachment")
|
47
73
|
# headers["Content-Disposition"] #=> "attachment; filename=\"file.txt\""
|
48
74
|
#
|
49
75
|
# ## Range
|
50
76
|
#
|
51
77
|
# [Partial responses][range requests] are also supported via the `:range`
|
52
|
-
#
|
78
|
+
# option, which accepts a value of the `Range` request header.
|
53
79
|
#
|
54
80
|
# env["HTTP_RANGE"] #=> "bytes=100-200"
|
55
81
|
# status, headers, body = uploaded_file.to_rack_response(range: env["HTTP_RANGE"])
|
@@ -59,35 +85,55 @@ class Shrine
|
|
59
85
|
# body # partial content
|
60
86
|
#
|
61
87
|
# [range requests]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
|
88
|
+
# [Rack::Sendfile]: https://www.rubydoc.info/github/rack/rack/Rack/Sendfile
|
62
89
|
module RackResponse
|
63
90
|
module FileMethods
|
64
91
|
# Returns a Rack response triple for the uploaded file.
|
65
|
-
def to_rack_response(
|
66
|
-
|
92
|
+
def to_rack_response(**options)
|
93
|
+
FileResponse.new(self).call(**options)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class FileResponse
|
98
|
+
attr_reader :file
|
99
|
+
|
100
|
+
def initialize(file)
|
101
|
+
@file = file
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns a Rack response triple for the uploaded file.
|
105
|
+
def call(**options)
|
106
|
+
options[:range] = parse_content_range(options[:range]) if options[:range]
|
67
107
|
|
68
|
-
status =
|
69
|
-
headers = rack_headers(
|
70
|
-
body = rack_body(
|
108
|
+
status = rack_status(**options)
|
109
|
+
headers = rack_headers(**options)
|
110
|
+
body = rack_body(**options)
|
71
111
|
|
72
112
|
[status, headers, body]
|
73
113
|
end
|
74
114
|
|
75
115
|
private
|
76
116
|
|
117
|
+
# Returns "200 OK" on full request, and "206 Partial Content" on ranged
|
118
|
+
# request.
|
119
|
+
def rack_status(range: nil, **)
|
120
|
+
range ? 206 : 200
|
121
|
+
end
|
122
|
+
|
77
123
|
# Returns a hash of "Content-Length", "Content-Type" and
|
78
124
|
# "Content-Disposition" headers, whose values are extracted from
|
79
125
|
# metadata. Also returns the correct "Content-Range" header on ranged
|
80
126
|
# requests.
|
81
|
-
def rack_headers(disposition
|
82
|
-
length
|
83
|
-
type
|
84
|
-
filename
|
127
|
+
def rack_headers(filename: nil, type: nil, disposition: "inline", range: false)
|
128
|
+
length = range ? range.size : size
|
129
|
+
type ||= @file.mime_type || Rack::Mime.mime_type(".#{@file.extension}")
|
130
|
+
filename ||= @file.original_filename || @file.id.split("/").last
|
85
131
|
|
86
132
|
headers = {}
|
87
133
|
headers["Content-Length"] = length.to_s if length
|
88
134
|
headers["Content-Type"] = type
|
89
|
-
headers["Content-Disposition"] =
|
90
|
-
headers["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size
|
135
|
+
headers["Content-Disposition"] = content_disposition(disposition: disposition, filename: filename)
|
136
|
+
headers["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}" if range
|
91
137
|
headers["Accept-Ranges"] = "bytes" unless range == false
|
92
138
|
|
93
139
|
headers
|
@@ -95,19 +141,69 @@ class Shrine
|
|
95
141
|
|
96
142
|
# Returns an object that responds to #each and #close, which yields
|
97
143
|
# contents of the file.
|
98
|
-
def rack_body(range: nil)
|
144
|
+
def rack_body(range: nil, **)
|
145
|
+
FileBody.new(file, range: range)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Parses the value of a "Range" HTTP header.
|
149
|
+
def parse_content_range(range_header)
|
150
|
+
if Rack.release >= "2.0"
|
151
|
+
ranges = Rack::Utils.get_byte_ranges(range_header, size)
|
152
|
+
else
|
153
|
+
ranges = Rack::Utils.byte_ranges({"HTTP_RANGE" => range_header}, size)
|
154
|
+
end
|
155
|
+
|
156
|
+
ranges.first if ranges && ranges.one?
|
157
|
+
end
|
158
|
+
|
159
|
+
def content_disposition(disposition:, filename:)
|
160
|
+
ContentDisposition.format(disposition: disposition, filename: filename)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Read size from metadata, otherwise retrieve the size from the storage.
|
164
|
+
def size
|
165
|
+
@file.size || @file.to_io.size
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Implements the interface of a Rack response body object.
|
170
|
+
class FileBody
|
171
|
+
attr_reader :file, :range
|
172
|
+
|
173
|
+
def initialize(file, range: nil)
|
174
|
+
@file = file
|
175
|
+
@range = range
|
176
|
+
end
|
177
|
+
|
178
|
+
# Streams the uploaded file directly from the storage.
|
179
|
+
def each(&block)
|
99
180
|
if range
|
100
|
-
|
181
|
+
read_partial_chunks(&block)
|
101
182
|
else
|
102
|
-
|
183
|
+
read_chunks(&block)
|
103
184
|
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Closes the file when response body is closed by the web server.
|
188
|
+
def close
|
189
|
+
file.close
|
190
|
+
end
|
104
191
|
|
105
|
-
|
192
|
+
# Rack::Sendfile is activated when response body responds to #to_path.
|
193
|
+
def respond_to_missing?(name, include_private = false)
|
194
|
+
name == :to_path && path
|
106
195
|
end
|
107
196
|
|
197
|
+
# Rack::Sendfile is activated when response body responds to #to_path.
|
198
|
+
def method_missing(name, *args, &block)
|
199
|
+
name == :to_path && path or super
|
200
|
+
end
|
201
|
+
|
202
|
+
private
|
203
|
+
|
108
204
|
# Yields reasonably sized chunks of uploaded file's partial content
|
109
205
|
# specified by the given index range.
|
110
|
-
def read_partial_chunks
|
206
|
+
def read_partial_chunks
|
111
207
|
bytes_read = 0
|
112
208
|
|
113
209
|
read_chunks do |chunk|
|
@@ -130,22 +226,18 @@ class Shrine
|
|
130
226
|
|
131
227
|
# Yields reasonably sized chunks of uploaded file's content.
|
132
228
|
def read_chunks
|
133
|
-
if
|
134
|
-
|
229
|
+
if file.to_io.respond_to?(:each_chunk) # Down::ChunkedIO
|
230
|
+
file.to_io.each_chunk { |chunk| yield chunk }
|
135
231
|
else
|
136
|
-
yield
|
232
|
+
yield file.read(16*1024) until file.eof?
|
137
233
|
end
|
138
234
|
end
|
139
235
|
|
140
|
-
#
|
141
|
-
def
|
142
|
-
if
|
143
|
-
|
144
|
-
else
|
145
|
-
ranges = Rack::Utils.byte_ranges({"HTTP_RANGE" => range_header}, size || io.size)
|
236
|
+
# Returns actual path on disk when FileSystem storage is used.
|
237
|
+
def path
|
238
|
+
if defined?(Storage::FileSystem) && file.storage.is_a?(Storage::FileSystem)
|
239
|
+
file.storage.path(file.id)
|
146
240
|
end
|
147
|
-
|
148
|
-
ranges.first if ranges && ranges.one?
|
149
241
|
end
|
150
242
|
end
|
151
243
|
end
|
@@ -7,9 +7,10 @@ class Shrine
|
|
7
7
|
#
|
8
8
|
# plugin :refresh_metadata
|
9
9
|
#
|
10
|
-
# It provides `UploadedFile#refresh_metadata!` method, which
|
11
|
-
# `Shrine#extract_metadata` with the uploaded
|
12
|
-
# and updates the existing metadata hash with the
|
10
|
+
# It provides `UploadedFile#refresh_metadata!` method, which triggers
|
11
|
+
# metadata extraction (calls `Shrine#extract_metadata`) with the uploaded
|
12
|
+
# file opened for reading, and updates the existing metadata hash with the
|
13
|
+
# results.
|
13
14
|
#
|
14
15
|
# uploaded_file.refresh_metadata!
|
15
16
|
# uploaded_file.metadata # re-extracted metadata
|
@@ -17,11 +18,25 @@ class Shrine
|
|
17
18
|
# For remote storages this will make an HTTP request to open the file for
|
18
19
|
# reading, but only the portion of the file needed for extracting each
|
19
20
|
# metadata value will be downloaded.
|
21
|
+
#
|
22
|
+
# If the uploaded file is already open, it is passed to metadata extraction
|
23
|
+
# as is.
|
24
|
+
#
|
25
|
+
# uploaded_file.open do
|
26
|
+
# uploaded_file.refresh_metadata!
|
27
|
+
# # ...
|
28
|
+
# end
|
20
29
|
module RefreshMetadata
|
21
30
|
module FileMethods
|
22
31
|
def refresh_metadata!(context = {})
|
23
|
-
refreshed_metadata =
|
24
|
-
|
32
|
+
refreshed_metadata =
|
33
|
+
if @io
|
34
|
+
uploader.extract_metadata(self, context)
|
35
|
+
else
|
36
|
+
open { uploader.extract_metadata(self, context) }
|
37
|
+
end
|
38
|
+
|
39
|
+
@data = @data.merge("metadata" => metadata.merge(refreshed_metadata))
|
25
40
|
end
|
26
41
|
end
|
27
42
|
end
|
@@ -102,15 +102,15 @@ class Shrine
|
|
102
102
|
def initialize(*)
|
103
103
|
super
|
104
104
|
|
105
|
-
|
106
|
-
def #{@name}_remote_url=(url)
|
107
|
-
#{@name}_attacher.remote_url = url
|
108
|
-
end
|
105
|
+
name = attachment_name
|
109
106
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
107
|
+
define_method :"#{name}_remote_url=" do |url|
|
108
|
+
send(:"#{name}_attacher").remote_url = url
|
109
|
+
end
|
110
|
+
|
111
|
+
define_method :"#{name}_remote_url" do
|
112
|
+
send(:"#{name}_attacher").remote_url
|
113
|
+
end
|
114
114
|
end
|
115
115
|
end
|
116
116
|
|
@@ -17,15 +17,15 @@ class Shrine
|
|
17
17
|
def initialize(*)
|
18
18
|
super
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
20
|
+
name = attachment_name
|
21
|
+
|
22
|
+
define_method :"remove_#{name}=" do |value|
|
23
|
+
send(:"#{name}_attacher").remove = value
|
24
|
+
end
|
25
|
+
|
26
|
+
define_method :"remove_#{name}" do
|
27
|
+
send(:"#{name}_attacher").remove
|
28
|
+
end
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|