shrine 2.8.0 → 2.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 +4 -4
- data/CHANGELOG.md +681 -0
- data/README.md +73 -21
- data/doc/carrierwave.md +75 -20
- data/doc/creating_storages.md +15 -26
- data/doc/direct_s3.md +113 -31
- data/doc/multiple_files.md +4 -8
- data/doc/paperclip.md +98 -31
- data/doc/refile.md +4 -6
- data/doc/testing.md +24 -21
- data/lib/shrine.rb +32 -20
- data/lib/shrine/plugins/activerecord.rb +2 -0
- data/lib/shrine/plugins/add_metadata.rb +2 -0
- data/lib/shrine/plugins/background_helpers.rb +2 -0
- data/lib/shrine/plugins/backgrounding.rb +11 -4
- data/lib/shrine/plugins/backup.rb +2 -0
- data/lib/shrine/plugins/cached_attachment_data.rb +2 -0
- data/lib/shrine/plugins/copy.rb +2 -0
- data/lib/shrine/plugins/data_uri.rb +20 -12
- data/lib/shrine/plugins/default_storage.rb +2 -0
- data/lib/shrine/plugins/default_url.rb +2 -0
- data/lib/shrine/plugins/default_url_options.rb +2 -0
- data/lib/shrine/plugins/delete_promoted.rb +2 -0
- data/lib/shrine/plugins/delete_raw.rb +2 -0
- data/lib/shrine/plugins/determine_mime_type.rb +18 -2
- data/lib/shrine/plugins/direct_upload.rb +6 -6
- data/lib/shrine/plugins/download_endpoint.rb +2 -0
- data/lib/shrine/plugins/dynamic_storage.rb +2 -0
- data/lib/shrine/plugins/hooks.rb +2 -0
- data/lib/shrine/plugins/included.rb +2 -0
- data/lib/shrine/plugins/infer_extension.rb +131 -0
- data/lib/shrine/plugins/keep_files.rb +2 -0
- data/lib/shrine/plugins/logging.rb +6 -4
- data/lib/shrine/plugins/metadata_attributes.rb +2 -0
- data/lib/shrine/plugins/migration_helpers.rb +2 -0
- data/lib/shrine/plugins/module_include.rb +2 -0
- data/lib/shrine/plugins/moving.rb +2 -0
- data/lib/shrine/plugins/multi_delete.rb +4 -0
- data/lib/shrine/plugins/parallelize.rb +2 -0
- data/lib/shrine/plugins/parsed_json.rb +2 -0
- data/lib/shrine/plugins/presign_endpoint.rb +7 -7
- data/lib/shrine/plugins/pretty_location.rb +2 -0
- data/lib/shrine/plugins/processing.rb +2 -0
- data/lib/shrine/plugins/rack_file.rb +2 -0
- data/lib/shrine/plugins/rack_response.rb +2 -0
- data/lib/shrine/plugins/recache.rb +2 -0
- data/lib/shrine/plugins/refresh_metadata.rb +2 -0
- data/lib/shrine/plugins/remote_url.rb +12 -1
- data/lib/shrine/plugins/remove_attachment.rb +2 -0
- data/lib/shrine/plugins/remove_invalid.rb +2 -0
- data/lib/shrine/plugins/restore_cached_data.rb +2 -0
- data/lib/shrine/plugins/sequel.rb +2 -0
- data/lib/shrine/plugins/signature.rb +10 -8
- data/lib/shrine/plugins/store_dimensions.rb +5 -3
- data/lib/shrine/plugins/upload_endpoint.rb +7 -8
- data/lib/shrine/plugins/upload_options.rb +2 -0
- data/lib/shrine/plugins/validation_helpers.rb +2 -0
- data/lib/shrine/plugins/versions.rb +72 -31
- data/lib/shrine/storage/file_system.rb +11 -4
- data/lib/shrine/storage/linter.rb +5 -13
- data/lib/shrine/storage/s3.rb +16 -13
- data/lib/shrine/version.rb +3 -1
- data/shrine.gemspec +7 -6
- metadata +26 -10
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "down"
|
2
4
|
|
3
5
|
class Shrine
|
@@ -66,8 +68,17 @@ class Shrine
|
|
66
68
|
# can use the [shrine-url] storage which allows you to assign a custom URL
|
67
69
|
# as cached file ID, and pair that with the `backgrounding` plugin.
|
68
70
|
#
|
71
|
+
# ## File extension
|
72
|
+
#
|
73
|
+
# When attaching from a remote URL, the uploaded file location will have
|
74
|
+
# the extension inferred from the URL. However, some URLs might not have an
|
75
|
+
# extension, in which case the uploaded file location also won't have the
|
76
|
+
# extension. If you want the upload location to always have an extension,
|
77
|
+
# you can load the `infer_extension` plugin to infer it from the MIME type.
|
78
|
+
#
|
79
|
+
# plugin :infer_extension
|
80
|
+
#
|
69
81
|
# [Down]: https://github.com/janko-m/down
|
70
|
-
# [Addressable]: https://github.com/sporkmonger/addressable
|
71
82
|
# [shrine-url]: https://github.com/janko-m/shrine-url
|
72
83
|
module RemoteUrl
|
73
84
|
def self.configure(uploader, opts = {})
|
@@ -1,14 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Shrine
|
2
4
|
module Plugins
|
3
5
|
# The `signature` plugin provides the ability to calculate a hash from file
|
4
|
-
# content.
|
5
|
-
# signature
|
6
|
+
# content. This hash can be used as a checksum or just as a unique
|
7
|
+
# signature for the uploaded file.
|
6
8
|
#
|
7
|
-
# plugin :signature
|
9
|
+
# Shrine.plugin :signature
|
8
10
|
#
|
9
|
-
# The plugin adds a
|
10
|
-
#
|
11
|
-
# the calculated hash.
|
11
|
+
# The plugin adds a `#calculate_signature` instance and class method to the
|
12
|
+
# uploader. The method accepts an IO object and a hashing algorithm, and
|
13
|
+
# returns the calculated hash.
|
12
14
|
#
|
13
15
|
# Shrine.calculate_signature(io, :md5)
|
14
16
|
# #=> "9a0364b9e99bb480dd25e1f0284c8555"
|
@@ -102,14 +104,14 @@ class Shrine
|
|
102
104
|
def calculate_crc32(io)
|
103
105
|
require "zlib"
|
104
106
|
crc = 0
|
105
|
-
crc = Zlib.crc32(io.read(16*1024, buffer ||=
|
107
|
+
crc = Zlib.crc32(io.read(16*1024, buffer ||= String.new), crc) until io.eof?
|
106
108
|
crc.to_s
|
107
109
|
end
|
108
110
|
|
109
111
|
def calculate_digest(name, io)
|
110
112
|
require "digest"
|
111
113
|
digest = Digest.const_get(name).new
|
112
|
-
digest.update(io.read(16*1024, buffer ||=
|
114
|
+
digest.update(io.read(16*1024, buffer ||= String.new)) until io.eof?
|
113
115
|
digest.digest
|
114
116
|
end
|
115
117
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Shrine
|
2
4
|
module Plugins
|
3
5
|
# The `store_dimensions` plugin extracts and stores dimensions of the
|
@@ -25,9 +27,9 @@ class Shrine
|
|
25
27
|
#
|
26
28
|
# require "mini_magick"
|
27
29
|
#
|
28
|
-
# plugin :store_dimensions, analyzer: ->(io, analyzers) do
|
29
|
-
# dimensions = analyzers[:fastimage].call(io)
|
30
|
-
# dimensions || MiniMagick::Image.new(io).dimensions
|
30
|
+
# plugin :store_dimensions, analyzer: -> (io, analyzers) do
|
31
|
+
# dimensions = analyzers[:fastimage].call(io) # try extracting dimensions with FastImage
|
32
|
+
# dimensions || MiniMagick::Image.new(io).dimensions # otherwise fall back to MiniMagick
|
31
33
|
# end
|
32
34
|
#
|
33
35
|
# You can also use methods for extracting the dimensions directly:
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "rack"
|
2
4
|
|
3
5
|
require "json"
|
@@ -5,9 +7,8 @@ require "json"
|
|
5
7
|
class Shrine
|
6
8
|
module Plugins
|
7
9
|
# The `upload_endpoint` plugin provides a Rack endpoint which accepts file
|
8
|
-
# uploads and forwards them to specified storage.
|
9
|
-
#
|
10
|
-
# [jQuery-File-Upload] for asynchronous uploads.
|
10
|
+
# uploads and forwards them to specified storage. On the client side it's
|
11
|
+
# recommended to use [Uppy] for asynchronous uploads.
|
11
12
|
#
|
12
13
|
# plugin :upload_endpoint
|
13
14
|
#
|
@@ -65,7 +66,7 @@ class Shrine
|
|
65
66
|
#
|
66
67
|
# The upload context will *not* contain `:record` and `:name` values, as
|
67
68
|
# the upload happens independently of a database record. The endpoint will
|
68
|
-
#
|
69
|
+
# send the following upload context:
|
69
70
|
#
|
70
71
|
# * `:action` - holds the value `:upload`
|
71
72
|
# * `:request` - holds an instance of `Rack::Request`
|
@@ -100,9 +101,7 @@ class Shrine
|
|
100
101
|
#
|
101
102
|
# Shrine.upload_endpoint(:cache, max_size: 20*1024*1024)
|
102
103
|
#
|
103
|
-
# [
|
104
|
-
# [Dropzone]: https://github.com/enyo/dropzone
|
105
|
-
# [jQuery-File-Upload]: https://github.com/blueimp/jQuery-File-Upload
|
104
|
+
# [Uppy]: https://uppy.io
|
106
105
|
module UploadEndpoint
|
107
106
|
def self.load_dependencies(uploader, opts = {})
|
108
107
|
uploader.plugin :rack_file
|
@@ -135,7 +134,7 @@ class Shrine
|
|
135
134
|
end
|
136
135
|
end
|
137
136
|
|
138
|
-
# Rack application that accepts multipart
|
137
|
+
# Rack application that accepts multipart POST request to the root URL,
|
139
138
|
# calls `#upload` with the uploaded file, and returns the uploaded file
|
140
139
|
# information in JSON format.
|
141
140
|
class App
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Shrine
|
2
4
|
module Plugins
|
3
5
|
# The `versions` plugin enables your uploader to deal with versions, by
|
@@ -59,21 +61,6 @@ class Shrine
|
|
59
61
|
# # ...
|
60
62
|
# end
|
61
63
|
#
|
62
|
-
# ## Original file
|
63
|
-
#
|
64
|
-
# If you want to keep the original file, you can include the original
|
65
|
-
# `Shrine::UploadedFile` object as one of the versions:
|
66
|
-
#
|
67
|
-
# process(:store) do |io, context|
|
68
|
-
# # processing thumbnail
|
69
|
-
# {original: io, thumbnail: thumbnail}
|
70
|
-
# end
|
71
|
-
#
|
72
|
-
# If both temporary and permanent storage are Amazon S3, the cached original
|
73
|
-
# will simply be copied over to permanent storage (without any downloading
|
74
|
-
# and reuploading), so in these cases the performance impact of storing the
|
75
|
-
# original file in addition to processed versions is neglibible.
|
76
|
-
#
|
77
64
|
# ## Fallbacks
|
78
65
|
#
|
79
66
|
# If versions are processed in a background job, there will be a period
|
@@ -106,6 +93,47 @@ class Shrine
|
|
106
93
|
# user.avatar_url(:thumb_2x) # returns :thumb URL until :thumb_2x becomes available
|
107
94
|
# user.avatar_url(:large_2x) # returns :large URL until :large_2x becomes available
|
108
95
|
#
|
96
|
+
# ## Arrays
|
97
|
+
#
|
98
|
+
# In addition to Hashes, the plugin also supports Arrays of files. For
|
99
|
+
# example, you might want to split a PDf into pages:
|
100
|
+
#
|
101
|
+
# process(:store) do |io, context|
|
102
|
+
# pdf = io.download
|
103
|
+
# image = MiniMagick::Image.new(pdf.path)
|
104
|
+
# versions = []
|
105
|
+
#
|
106
|
+
# image.pages.each_with_index do |page, index|
|
107
|
+
# page_file = Tempfile.new("version-#{index}", binmode: true)
|
108
|
+
# MiniMagick::Tool::Convert.new do |convert|
|
109
|
+
# convert << page.path
|
110
|
+
# convert << page_file.path
|
111
|
+
# end
|
112
|
+
# page_file.open # refresh updated file
|
113
|
+
# versions << page_file
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# versions # array of pages
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# You can also combine Hashes and Arrays, there is no limit to the level of
|
120
|
+
# nesting.
|
121
|
+
#
|
122
|
+
# ## Original file
|
123
|
+
#
|
124
|
+
# If you want to keep the original file, you can include the original
|
125
|
+
# `Shrine::UploadedFile` object as one of the versions:
|
126
|
+
#
|
127
|
+
# process(:store) do |io, context|
|
128
|
+
# # processing thumbnail
|
129
|
+
# {original: io, thumbnail: thumbnail}
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# If both temporary and permanent storage are Amazon S3, the cached original
|
133
|
+
# will simply be copied over to permanent storage (without any downloading
|
134
|
+
# and reuploading), so in these cases the performance impact of storing the
|
135
|
+
# original file in addition to processed versions is neglibible.
|
136
|
+
#
|
109
137
|
# ## Context
|
110
138
|
#
|
111
139
|
# The version name will be available via `:version` when generating
|
@@ -122,7 +150,6 @@ class Shrine
|
|
122
150
|
# [image_processing]: https://github.com/janko-m/image_processing
|
123
151
|
module Versions
|
124
152
|
def self.load_dependencies(uploader, *)
|
125
|
-
uploader.plugin :multi_delete
|
126
153
|
uploader.plugin :default_url
|
127
154
|
end
|
128
155
|
|
@@ -152,10 +179,12 @@ class Shrine
|
|
152
179
|
|
153
180
|
# Converts a hash of data into a hash of versions.
|
154
181
|
def uploaded_file(object, &block)
|
155
|
-
if
|
156
|
-
|
157
|
-
result.
|
182
|
+
if object.is_a?(Hash) && object.values.none? { |value| value.is_a?(String) }
|
183
|
+
object.inject({}) do |result, (name, value)|
|
184
|
+
result.merge!(name.to_sym => uploaded_file(value, &block))
|
158
185
|
end
|
186
|
+
elsif object.is_a?(Array)
|
187
|
+
object.map { |value| uploaded_file(value, &block) }
|
159
188
|
else
|
160
189
|
super
|
161
190
|
end
|
@@ -164,9 +193,11 @@ class Shrine
|
|
164
193
|
|
165
194
|
module InstanceMethods
|
166
195
|
# Checks whether all versions are uploaded by this uploader.
|
167
|
-
def uploaded?(
|
168
|
-
if
|
169
|
-
|
196
|
+
def uploaded?(object)
|
197
|
+
if object.is_a?(Hash)
|
198
|
+
object.all? { |name, version| uploaded?(version) }
|
199
|
+
elsif object.is_a?(Array)
|
200
|
+
object.all? { |version| uploaded?(version) }
|
170
201
|
else
|
171
202
|
super
|
172
203
|
end
|
@@ -183,9 +214,11 @@ class Shrine
|
|
183
214
|
raise Error, ":location is not applicable to versions" if context.key?(:location)
|
184
215
|
raise Error, "detected multiple versions that point to the same IO object: given versions: #{hash.keys}, unique versions: #{hash.invert.invert.keys}" if hash.invert.invert != hash
|
185
216
|
|
186
|
-
hash.inject({}) do |result, (name,
|
187
|
-
result.
|
217
|
+
hash.inject({}) do |result, (name, value)|
|
218
|
+
result.merge!(name => _store(value, context.merge(version: name){|_, v1, v2| Array(v1) + Array(v2)}))
|
188
219
|
end
|
220
|
+
elsif (array = io).is_a?(Array)
|
221
|
+
array.map.with_index { |value, idx| _store(value, context.merge(version: idx){|_, v1, v2| Array(v1) + Array(v2)}) }
|
189
222
|
else
|
190
223
|
super
|
191
224
|
end
|
@@ -194,8 +227,14 @@ class Shrine
|
|
194
227
|
# Deletes each file individually, but uses S3's multi delete
|
195
228
|
# capabilities.
|
196
229
|
def _delete(uploaded_file, context)
|
197
|
-
if (
|
198
|
-
|
230
|
+
if (hash = uploaded_file).is_a?(Hash)
|
231
|
+
hash.each do |name, value|
|
232
|
+
_delete(value, context)
|
233
|
+
end
|
234
|
+
elsif (array = uploaded_file).is_a?(Array)
|
235
|
+
array.each do |value|
|
236
|
+
_delete(value, context)
|
237
|
+
end
|
199
238
|
else
|
200
239
|
super
|
201
240
|
end
|
@@ -241,16 +280,18 @@ class Shrine
|
|
241
280
|
|
242
281
|
def assign_cached(value)
|
243
282
|
cached_file = uploaded_file(value)
|
244
|
-
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)
|
283
|
+
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)
|
245
284
|
super(cached_file)
|
246
285
|
end
|
247
286
|
|
248
287
|
# Converts the Hash of UploadedFile objects into a Hash of data.
|
249
|
-
def convert_to_data(
|
250
|
-
if
|
251
|
-
|
252
|
-
hash.merge!(name =>
|
288
|
+
def convert_to_data(object)
|
289
|
+
if object.is_a?(Hash)
|
290
|
+
object.inject({}) do |hash, (name, value)|
|
291
|
+
hash.merge!(name => convert_to_data(value))
|
253
292
|
end
|
293
|
+
elsif object.is_a?(Array)
|
294
|
+
object.map { |value| convert_to_data(value) }
|
254
295
|
else
|
255
296
|
super
|
256
297
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "fileutils"
|
2
4
|
require "pathname"
|
3
5
|
|
@@ -201,10 +203,15 @@ class Shrine
|
|
201
203
|
# Catches the deprecated `#download` method.
|
202
204
|
def method_missing(name, *args)
|
203
205
|
if name == :download
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
206
|
+
begin
|
207
|
+
Shrine.deprecation("Shrine::Storage::FileSystem#download is deprecated and will be removed in Shrine 3.")
|
208
|
+
tempfile = Tempfile.new(["shrine-filesystem", File.extname(args[0])], binmode: true)
|
209
|
+
open(*args) { |file| IO.copy_stream(file, tempfile) }
|
210
|
+
tempfile.tap(&:open)
|
211
|
+
rescue
|
212
|
+
tempfile.close! if tempfile
|
213
|
+
raise
|
214
|
+
end
|
208
215
|
else
|
209
216
|
super
|
210
217
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "shrine"
|
2
4
|
|
3
5
|
require "forwardable"
|
@@ -34,7 +36,7 @@ class Shrine
|
|
34
36
|
end
|
35
37
|
|
36
38
|
def call(io_factory = default_io_factory)
|
37
|
-
storage.upload(io_factory.call, id = "foo", {})
|
39
|
+
storage.upload(io_factory.call, id = "foo".dup, {})
|
38
40
|
|
39
41
|
lint_download(id) if storage.respond_to?(:download)
|
40
42
|
lint_open(id)
|
@@ -43,17 +45,12 @@ class Shrine
|
|
43
45
|
lint_delete(id)
|
44
46
|
|
45
47
|
if storage.respond_to?(:move)
|
46
|
-
uploaded_file = uploader.upload(io_factory.call, location: "bar")
|
48
|
+
uploaded_file = uploader.upload(io_factory.call, location: "bar".dup)
|
47
49
|
lint_move(uploaded_file, "quux")
|
48
50
|
end
|
49
51
|
|
50
|
-
if storage.respond_to?(:multi_delete)
|
51
|
-
storage.upload(io_factory.call, id = "baz")
|
52
|
-
lint_multi_delete(id)
|
53
|
-
end
|
54
|
-
|
55
52
|
if storage.respond_to?(:clear!)
|
56
|
-
storage.upload(io_factory.call, id = "quux")
|
53
|
+
storage.upload(io_factory.call, id = "quux".dup)
|
57
54
|
lint_clear(id)
|
58
55
|
end
|
59
56
|
end
|
@@ -99,11 +96,6 @@ class Shrine
|
|
99
96
|
end
|
100
97
|
end
|
101
98
|
|
102
|
-
def lint_multi_delete(id)
|
103
|
-
storage.multi_delete([id])
|
104
|
-
error :exists?, "returns true for a file that was multi-deleted" if storage.exists?(id)
|
105
|
-
end
|
106
|
-
|
107
99
|
def lint_clear(id)
|
108
100
|
storage.clear!
|
109
101
|
error :clear!, "file still #exists? after clearing" if storage.exists?(id)
|