shrine 2.11.0 → 2.12.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 +28 -0
- data/README.md +25 -22
- data/doc/advantages.md +22 -10
- data/doc/carrierwave.md +13 -10
- data/doc/creating_storages.md +1 -1
- data/doc/direct_s3.md +6 -6
- data/doc/multiple_files.md +195 -51
- data/doc/paperclip.md +11 -8
- data/doc/processing.md +36 -29
- data/doc/refile.md +8 -7
- data/lib/shrine.rb +7 -7
- data/lib/shrine/plugins/data_uri.rb +1 -1
- data/lib/shrine/plugins/determine_mime_type.rb +3 -2
- data/lib/shrine/plugins/download_endpoint.rb +86 -37
- data/lib/shrine/plugins/dynamic_storage.rb +5 -1
- data/lib/shrine/plugins/parallelize.rb +0 -1
- data/lib/shrine/plugins/presign_endpoint.rb +1 -6
- data/lib/shrine/plugins/processing.rb +5 -9
- data/lib/shrine/plugins/rack_response.rb +1 -1
- data/lib/shrine/plugins/remote_url.rb +24 -10
- data/lib/shrine/plugins/signature.rb +1 -2
- data/lib/shrine/plugins/validation_helpers.rb +15 -2
- data/lib/shrine/plugins/versions.rb +18 -22
- data/lib/shrine/storage/file_system.rb +4 -3
- data/lib/shrine/storage/s3.rb +20 -12
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +8 -2
- metadata +18 -4
@@ -20,9 +20,13 @@ class Shrine
|
|
20
20
|
#
|
21
21
|
# This can be useful in combination with the `default_storage` plugin.
|
22
22
|
module DynamicStorage
|
23
|
+
def self.configure(uploader, options = {})
|
24
|
+
uploader.opts[:dynamic_storages] ||= {}
|
25
|
+
end
|
26
|
+
|
23
27
|
module ClassMethods
|
24
28
|
def dynamic_storages
|
25
|
-
|
29
|
+
opts[:dynamic_storages]
|
26
30
|
end
|
27
31
|
|
28
32
|
def storage(regex, &block)
|
@@ -37,7 +37,7 @@ class Shrine
|
|
37
37
|
# uploads to the temporary (`:cache`) storage.
|
38
38
|
#
|
39
39
|
# The above will create a `GET /images/presign` endpoint, which calls
|
40
|
-
# `#presign` on the storage and returns the HTTP verb, URL,
|
40
|
+
# `#presign` on the storage and returns the HTTP verb, URL, params, and
|
41
41
|
# headers needed for a single upload directly to the storage service, in
|
42
42
|
# JSON format.
|
43
43
|
#
|
@@ -56,11 +56,6 @@ class Shrine
|
|
56
56
|
# "headers": {}
|
57
57
|
# }
|
58
58
|
#
|
59
|
-
# * `method` – HTTP verb
|
60
|
-
# * `url` – request URL
|
61
|
-
# * `fields` – POST parameters
|
62
|
-
# * `headers` – request headers
|
63
|
-
#
|
64
59
|
# ## Location
|
65
60
|
#
|
66
61
|
# By default the generated location won't have any file extension, but you
|
@@ -34,15 +34,11 @@ class Shrine
|
|
34
34
|
# require "image_processing/mini_magick"
|
35
35
|
#
|
36
36
|
# process(:store) do |io, context|
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
# original.close!
|
44
|
-
#
|
45
|
-
# resized
|
37
|
+
# io.download do |original|
|
38
|
+
# ImageProcessing::MiniMagick
|
39
|
+
# .source(original)
|
40
|
+
# .resize_to_limit!(800, 800)
|
41
|
+
# end
|
46
42
|
# end
|
47
43
|
#
|
48
44
|
# The declarations are additive and inheritable, so for the same action you
|
@@ -79,7 +79,7 @@ class Shrine
|
|
79
79
|
# metadata. Also returns the correct "Content-Range" header on ranged
|
80
80
|
# requests.
|
81
81
|
def rack_headers(disposition:, range: false)
|
82
|
-
length = range ? range.
|
82
|
+
length = range ? range.size : size || io.size
|
83
83
|
type = mime_type || Rack::Mime.mime_type(".#{extension}")
|
84
84
|
filename = original_filename || id.split("/").last
|
85
85
|
|
@@ -28,6 +28,13 @@ class Shrine
|
|
28
28
|
# around the `open-uri` standard library. Note that Down expects the given
|
29
29
|
# URL to be URI-encoded.
|
30
30
|
#
|
31
|
+
# ## Dynamic options
|
32
|
+
#
|
33
|
+
# You can dynamically pass options to the downloader by using
|
34
|
+
# `Attacher#assign_remote_url`:
|
35
|
+
#
|
36
|
+
# attacher.assign_remote_url("http://example.com/cool-image.png", downloader: { 'Authorization' => 'Basic ...' })
|
37
|
+
#
|
31
38
|
# ## Maximum size
|
32
39
|
#
|
33
40
|
# It's a good practice to limit the maximum filesize of the remote file:
|
@@ -50,8 +57,10 @@ class Shrine
|
|
50
57
|
#
|
51
58
|
# require "down/http"
|
52
59
|
#
|
53
|
-
# plugin :remote_url, max_size: 20*1024*1024, downloader: ->(url, max_size
|
54
|
-
# Down::Http.download(url, max_size: max_size,
|
60
|
+
# plugin :remote_url, max_size: 20*1024*1024, downloader: -> (url, max_size:, **options) do
|
61
|
+
# Down::Http.download(url, max_size: max_size, **options) do |http|
|
62
|
+
# http.follow(max_hops: 2).timeout(connect: 2, read: 2)
|
63
|
+
# end
|
55
64
|
# end
|
56
65
|
#
|
57
66
|
# ## Errors
|
@@ -60,7 +69,7 @@ class Shrine
|
|
60
69
|
# equal to the error message. You can change the default error message:
|
61
70
|
#
|
62
71
|
# plugin :remote_url, error_message: "download failed"
|
63
|
-
# plugin :remote_url, error_message: ->(url, error) { I18n.t("errors.download_failed") }
|
72
|
+
# plugin :remote_url, error_message: -> (url, error) { I18n.t("errors.download_failed") }
|
64
73
|
#
|
65
74
|
# ## Background
|
66
75
|
#
|
@@ -109,11 +118,11 @@ class Shrine
|
|
109
118
|
# Downloads the remote file and assigns it. If download failed, sets
|
110
119
|
# the error message and assigns the url to an instance variable so that
|
111
120
|
# it shows up in the form.
|
112
|
-
def
|
113
|
-
return if url == ""
|
121
|
+
def assign_remote_url(url, downloader: {})
|
122
|
+
return if url == "" || url.nil?
|
114
123
|
|
115
124
|
begin
|
116
|
-
downloaded_file = download(url)
|
125
|
+
downloaded_file = download(url, downloader)
|
117
126
|
rescue => error
|
118
127
|
download_error = error
|
119
128
|
end
|
@@ -127,6 +136,11 @@ class Shrine
|
|
127
136
|
end
|
128
137
|
end
|
129
138
|
|
139
|
+
# Alias for #assign_remote_url.
|
140
|
+
def remote_url=(url)
|
141
|
+
assign_remote_url(url)
|
142
|
+
end
|
143
|
+
|
130
144
|
# Form builders require the reader as well.
|
131
145
|
def remote_url
|
132
146
|
@remote_url
|
@@ -137,18 +151,18 @@ class Shrine
|
|
137
151
|
# Downloads the file using the "down" gem or a custom downloader.
|
138
152
|
# Checks the file size and terminates the download early if the file
|
139
153
|
# is too big.
|
140
|
-
def download(url)
|
154
|
+
def download(url, options)
|
141
155
|
downloader = shrine_class.opts[:remote_url_downloader]
|
142
156
|
downloader = method(:"download_with_#{downloader}") if downloader.is_a?(Symbol)
|
143
157
|
max_size = shrine_class.opts[:remote_url_max_size]
|
144
158
|
|
145
|
-
downloader.call(url, max_size: max_size)
|
159
|
+
downloader.call(url, { max_size: max_size }.merge(options))
|
146
160
|
end
|
147
161
|
|
148
162
|
# We silence any download errors, because for the user's point of view
|
149
163
|
# the download simply failed.
|
150
|
-
def download_with_open_uri(url,
|
151
|
-
Down.download(url,
|
164
|
+
def download_with_open_uri(url, options)
|
165
|
+
Down.download(url, options)
|
152
166
|
end
|
153
167
|
|
154
168
|
def download_error_message(url, error)
|
@@ -44,8 +44,8 @@ class Shrine
|
|
44
44
|
end
|
45
45
|
|
46
46
|
DEFAULT_MESSAGES = {
|
47
|
-
max_size: ->(max) { "is too large (max is #{max
|
48
|
-
min_size: ->(min) { "is too small (min is #{min
|
47
|
+
max_size: ->(max) { "is too large (max is #{PRETTY_FILESIZE.call(max)})" },
|
48
|
+
min_size: ->(min) { "is too small (min is #{PRETTY_FILESIZE.call(min)})" },
|
49
49
|
max_width: ->(max) { "is too wide (max is #{max} px)" },
|
50
50
|
min_width: ->(min) { "is too narrow (min is #{min} px)" },
|
51
51
|
max_height: ->(max) { "is too tall (max is #{max} px)" },
|
@@ -56,6 +56,19 @@ class Shrine
|
|
56
56
|
extension_exclusion: ->(list) { "is of forbidden format" },
|
57
57
|
}
|
58
58
|
|
59
|
+
FILESIZE_UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"].freeze
|
60
|
+
|
61
|
+
# Returns filesize in a human readable format with units.
|
62
|
+
# Uses the binary JEDEC unit system, i.e. 1.0 KB = 1024 bytes
|
63
|
+
PRETTY_FILESIZE = lambda do |bytes|
|
64
|
+
return "0.0 B" if bytes == 0
|
65
|
+
|
66
|
+
exp = Math.log(bytes, 1024).floor
|
67
|
+
max_exp = FILESIZE_UNITS.length - 1
|
68
|
+
exp = max_exp if exp > max_exp
|
69
|
+
"%.1f %s" % [bytes.to_f / 1024 ** exp, FILESIZE_UNITS[exp]]
|
70
|
+
end
|
71
|
+
|
59
72
|
module AttacherClassMethods
|
60
73
|
def default_validation_messages
|
61
74
|
@default_validation_messages ||= DEFAULT_MESSAGES.merge(
|
@@ -15,16 +15,17 @@ class Shrine
|
|
15
15
|
# plugin :processing
|
16
16
|
#
|
17
17
|
# process(:store) do |io, context|
|
18
|
-
#
|
19
|
-
# pipeline = ImageProcessing::MiniMagick.source(original)
|
18
|
+
# versions = { original: io } # retain original
|
20
19
|
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
# size_300 = pipeline.resize_to_limit!(300, 300)
|
20
|
+
# io.download do |original|
|
21
|
+
# pipeline = ImageProcessing::MiniMagick.source(original)
|
24
22
|
#
|
25
|
-
#
|
23
|
+
# versions[:large] = pipeline.resize_to_limit!(800, 800)
|
24
|
+
# versions[:medium] = pipeline.resize_to_limit!(500, 500)
|
25
|
+
# versions[:small] = pipeline.resize_to_limit!(300, 300)
|
26
|
+
# end
|
26
27
|
#
|
27
|
-
#
|
28
|
+
# versions # return the hash of processed files
|
28
29
|
# end
|
29
30
|
#
|
30
31
|
# You probably want to load the `delete_raw` plugin to automatically
|
@@ -105,17 +106,18 @@ class Shrine
|
|
105
106
|
# example, you might want to split a PDf into pages:
|
106
107
|
#
|
107
108
|
# process(:store) do |io, context|
|
108
|
-
#
|
109
|
-
# page_count = MiniMagick::Image.new(pdf.path).pages.count
|
110
|
-
# pipeline = ImageProcessing::MiniMagick.source(pdf).convert("jpg")
|
109
|
+
# versions = { pages: [] }
|
111
110
|
#
|
112
|
-
#
|
113
|
-
#
|
114
|
-
#
|
111
|
+
# io.download do |pdf|
|
112
|
+
# page_count = MiniMagick::Image.new(pdf.path).pages.count
|
113
|
+
# pipeline = ImageProcessing::MiniMagick.source(pdf).convert("jpg")
|
115
114
|
#
|
116
|
-
#
|
115
|
+
# page_count.times do |page_number|
|
116
|
+
# versions[:pages] << pipeline.loader(page: page_number).call
|
117
|
+
# end
|
118
|
+
# end
|
117
119
|
#
|
118
|
-
#
|
120
|
+
# versions
|
119
121
|
# end
|
120
122
|
#
|
121
123
|
# You can also combine Hashes and Arrays, there is no limit to the level of
|
@@ -286,13 +288,7 @@ class Shrine
|
|
286
288
|
shrine_class.opts[:versions_fallback_to_original]
|
287
289
|
end
|
288
290
|
|
289
|
-
|
290
|
-
cached_file = uploaded_file(value)
|
291
|
-
Shrine.deprecation("Assigning cached hash of files is deprecated for security reasons and will be removed in Shrine 3.") if cached_file.is_a?(Hash) || cached_file.is_a?(Array)
|
292
|
-
super(cached_file)
|
293
|
-
end
|
294
|
-
|
295
|
-
# Converts the Hash of UploadedFile objects into a Hash of data.
|
291
|
+
# Converts the Hash/Array of UploadedFile objects into a Hash/Array of data.
|
296
292
|
def convert_to_data(object)
|
297
293
|
if object.is_a?(Hash)
|
298
294
|
object.inject({}) do |hash, (name, value)|
|
@@ -151,9 +151,10 @@ class Shrine
|
|
151
151
|
(io.is_a?(UploadedFile) && io.storage.is_a?(Storage::FileSystem))
|
152
152
|
end
|
153
153
|
|
154
|
-
# Opens the file on the given location in read mode.
|
155
|
-
|
156
|
-
|
154
|
+
# Opens the file on the given location in read mode. Accepts additional
|
155
|
+
# `File.open` arguments.
|
156
|
+
def open(id, *args, &block)
|
157
|
+
path(id).open("rb", *args, &block)
|
157
158
|
end
|
158
159
|
|
159
160
|
# Returns true if the file exists on the filesystem.
|
data/lib/shrine/storage/s3.rb
CHANGED
@@ -31,10 +31,10 @@ class Shrine
|
|
31
31
|
# It can be initialized by providing the bucket name and credentials:
|
32
32
|
#
|
33
33
|
# s3 = Shrine::Storage::S3.new(
|
34
|
+
# bucket: "my-app", # required
|
34
35
|
# access_key_id: "abc",
|
35
36
|
# secret_access_key: "xyz",
|
36
37
|
# region: "eu-west-1",
|
37
|
-
# bucket: "my-app",
|
38
38
|
# )
|
39
39
|
#
|
40
40
|
# The core features of this storage requires the following AWS permissions:
|
@@ -264,8 +264,9 @@ class Shrine
|
|
264
264
|
end
|
265
265
|
end
|
266
266
|
|
267
|
-
# Downloads the file from S3
|
268
|
-
#
|
267
|
+
# Downloads the file from S3 and returns a `Tempfile`. The download will
|
268
|
+
# be automatically retried up to 3 times. Any additional options are
|
269
|
+
# forwarded to [`Aws::S3::Object#get`].
|
269
270
|
#
|
270
271
|
# [`Aws::S3::Object#get`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#get-instance_method
|
271
272
|
def download(id, **options)
|
@@ -279,13 +280,21 @@ class Shrine
|
|
279
280
|
raise
|
280
281
|
end
|
281
282
|
|
282
|
-
# Returns a `Down::ChunkedIO` object
|
283
|
-
#
|
283
|
+
# Returns a `Down::ChunkedIO` object that downloads S3 object content
|
284
|
+
# on-demand. By default, read content will be cached onto disk so that
|
285
|
+
# it can be rewinded, but if you don't need that you can pass
|
286
|
+
# `rewindable: false`.
|
287
|
+
#
|
288
|
+
# Any additional options are forwarded to [`Aws::S3::Object#get`].
|
284
289
|
#
|
285
290
|
# [`Aws::S3::Object#get`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#get-instance_method
|
286
|
-
def open(id, **options)
|
291
|
+
def open(id, rewindable: true, **options)
|
287
292
|
object = object(id)
|
288
|
-
io = Down::ChunkedIO.new(
|
293
|
+
io = Down::ChunkedIO.new(
|
294
|
+
chunks: object.enum_for(:get, **options),
|
295
|
+
rewindable: rewindable,
|
296
|
+
data: { object: object },
|
297
|
+
)
|
289
298
|
io.size = object.content_length
|
290
299
|
io
|
291
300
|
end
|
@@ -430,7 +439,7 @@ class Shrine
|
|
430
439
|
object(id).upload_file(path, **options)
|
431
440
|
File.size(path)
|
432
441
|
else
|
433
|
-
io.
|
442
|
+
io.to_io if io.is_a?(UploadedFile) # open if not already opened
|
434
443
|
|
435
444
|
if io.respond_to?(:size) && io.size
|
436
445
|
object(id).put(body: io, **options)
|
@@ -506,13 +515,12 @@ class Shrine
|
|
506
515
|
|
507
516
|
# Uploads at most 5MB of IO content into a single multipart part.
|
508
517
|
def upload_part(multipart_upload, io, part_number)
|
509
|
-
Tempfile.create("shrine-s3-part-#{part_number}") do |body|
|
510
|
-
multipart_part = multipart_upload.part(part_number)
|
511
|
-
|
518
|
+
Tempfile.create("shrine-s3-part-#{part_number}", binmode: true) do |body|
|
512
519
|
IO.copy_stream(io, body, MIN_PART_SIZE)
|
513
520
|
body.rewind
|
514
521
|
|
515
|
-
|
522
|
+
multipart_part = multipart_upload.part(part_number)
|
523
|
+
response = multipart_part.upload(body: body)
|
516
524
|
|
517
525
|
{ part_number: part_number, size: body.size, etag: response.etag }
|
518
526
|
end
|
data/lib/shrine/version.rb
CHANGED
data/shrine.gemspec
CHANGED
@@ -45,16 +45,22 @@ direct uploads for fully asynchronous user experience.
|
|
45
45
|
gem.add_development_dependency "mini_magick", "~> 4.0" unless ENV["CI"]
|
46
46
|
gem.add_development_dependency "ruby-vips", "~> 2.0" unless ENV["CI"]
|
47
47
|
gem.add_development_dependency "aws-sdk-s3", "~> 1.2"
|
48
|
+
gem.add_development_dependency "aws-sdk-core", "~> 3.23"
|
48
49
|
|
49
50
|
unless RUBY_ENGINE == "jruby" || ENV["CI"]
|
50
51
|
gem.add_development_dependency "ruby-filemagic", "~> 0.7"
|
51
52
|
end
|
52
53
|
|
53
54
|
gem.add_development_dependency "sequel"
|
54
|
-
|
55
|
+
|
56
|
+
if RUBY_VERSION >= "2.2.2"
|
57
|
+
gem.add_development_dependency "activerecord", "~> 5.0"
|
58
|
+
else
|
59
|
+
gem.add_development_dependency "activerecord", "~> 4.2"
|
60
|
+
end
|
55
61
|
|
56
62
|
if RUBY_ENGINE == "jruby"
|
57
|
-
gem.add_development_dependency "activerecord-jdbcsqlite3-adapter", "
|
63
|
+
gem.add_development_dependency "activerecord-jdbcsqlite3-adapter", "51"
|
58
64
|
else
|
59
65
|
gem.add_development_dependency "sqlite3"
|
60
66
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shrine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.12.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Janko Marohnić
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-08-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: down
|
@@ -248,6 +248,20 @@ dependencies:
|
|
248
248
|
- - "~>"
|
249
249
|
- !ruby/object:Gem::Version
|
250
250
|
version: '1.2'
|
251
|
+
- !ruby/object:Gem::Dependency
|
252
|
+
name: aws-sdk-core
|
253
|
+
requirement: !ruby/object:Gem::Requirement
|
254
|
+
requirements:
|
255
|
+
- - "~>"
|
256
|
+
- !ruby/object:Gem::Version
|
257
|
+
version: '3.23'
|
258
|
+
type: :development
|
259
|
+
prerelease: false
|
260
|
+
version_requirements: !ruby/object:Gem::Requirement
|
261
|
+
requirements:
|
262
|
+
- - "~>"
|
263
|
+
- !ruby/object:Gem::Version
|
264
|
+
version: '3.23'
|
251
265
|
- !ruby/object:Gem::Dependency
|
252
266
|
name: ruby-filemagic
|
253
267
|
requirement: !ruby/object:Gem::Requirement
|
@@ -282,14 +296,14 @@ dependencies:
|
|
282
296
|
requirements:
|
283
297
|
- - "~>"
|
284
298
|
- !ruby/object:Gem::Version
|
285
|
-
version: '
|
299
|
+
version: '5.0'
|
286
300
|
type: :development
|
287
301
|
prerelease: false
|
288
302
|
version_requirements: !ruby/object:Gem::Requirement
|
289
303
|
requirements:
|
290
304
|
- - "~>"
|
291
305
|
- !ruby/object:Gem::Version
|
292
|
-
version: '
|
306
|
+
version: '5.0'
|
293
307
|
- !ruby/object:Gem::Dependency
|
294
308
|
name: sqlite3
|
295
309
|
requirement: !ruby/object:Gem::Requirement
|