shrine 1.3.0 → 1.4.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 +14 -10
- data/doc/carrierwave.md +2 -1
- data/doc/changing_location.md +15 -22
- data/doc/creating_storages.md +6 -2
- data/doc/direct_s3.md +19 -5
- data/doc/refile.md +1 -1
- data/lib/shrine/plugins/activerecord.rb +6 -12
- data/lib/shrine/plugins/backgrounding.rb +65 -31
- data/lib/shrine/plugins/backup.rb +28 -19
- data/lib/shrine/plugins/data_uri.rb +18 -8
- data/lib/shrine/plugins/delete_promoted.rb +21 -0
- data/lib/shrine/plugins/delete_raw.rb +38 -0
- data/lib/shrine/plugins/delete_uploaded.rb +3 -40
- data/lib/shrine/plugins/determine_mime_type.rb +13 -18
- data/lib/shrine/plugins/direct_upload.rb +133 -29
- data/lib/shrine/plugins/download_endpoint.rb +31 -12
- data/lib/shrine/plugins/hooks.rb +20 -46
- data/lib/shrine/plugins/keep_files.rb +7 -4
- data/lib/shrine/plugins/logging.rb +21 -15
- data/lib/shrine/plugins/migration_helpers.rb +37 -18
- data/lib/shrine/plugins/moving.rb +3 -2
- data/lib/shrine/plugins/parallelize.rb +36 -17
- data/lib/shrine/plugins/restore_cached.rb +3 -35
- data/lib/shrine/plugins/restore_cached_data.rb +34 -0
- data/lib/shrine/plugins/sequel.rb +5 -11
- data/lib/shrine/plugins/store_dimensions.rb +6 -4
- data/lib/shrine/plugins/validation_helpers.rb +4 -4
- data/lib/shrine/plugins/versions.rb +27 -16
- data/lib/shrine/storage/linter.rb +109 -58
- data/lib/shrine/storage/s3.rb +8 -7
- data/lib/shrine/version.rb +1 -1
- data/lib/shrine.rb +4 -4
- data/shrine.gemspec +1 -2
- metadata +10 -20
@@ -1,46 +1,65 @@
|
|
1
1
|
class Shrine
|
2
2
|
module Plugins
|
3
|
-
# The migration_helpers plugin gives the
|
4
|
-
# which are convenient when doing
|
3
|
+
# The migration_helpers plugin gives the attacher additional helper methods
|
4
|
+
# which are convenient when doing file migrations.
|
5
5
|
#
|
6
|
-
#
|
6
|
+
# By default additional methods are also added to the model which delegate
|
7
|
+
# to the underlying attacher. If you want to disable that, you can load the
|
8
|
+
# plugin with `delegate: false`:
|
7
9
|
#
|
8
|
-
#
|
10
|
+
# plugin :migration_helpers, delegate: false
|
9
11
|
#
|
10
|
-
#
|
11
|
-
#
|
12
|
+
# ## `attachment_cache` and `attachment_store`
|
13
|
+
#
|
14
|
+
# These methods return cache and store uploaders used by the underlying
|
15
|
+
# attacher:
|
12
16
|
#
|
13
|
-
# user = User.new
|
14
17
|
# user.avatar_cache #=> #<Shrine @storage_key=:cache @storage=#<Shrine::Storage::FileSystem @directory=public/uploads>>
|
15
18
|
# user.avatar_store #=> #<Shrine @storage_key=:store @storage=#<Shrine::Storage::S3:0x007fb8343397c8 @bucket=#<Aws::S3::Bucket name="foo">>>
|
16
19
|
#
|
17
|
-
#
|
20
|
+
# # attacher equivalents
|
21
|
+
# user.avatar_attacher.cache
|
22
|
+
# user.avatar_attacher.store
|
23
|
+
#
|
24
|
+
# ## `attachment_cached?` and `attachment_stored?`
|
18
25
|
#
|
19
|
-
#
|
20
|
-
# cached/stored:
|
26
|
+
# These methods return true if attachment exists and is cached/stored:
|
21
27
|
#
|
22
28
|
# user.avatar_cached? # user.avatar && user.avatar_cache.uploaded?(user.avatar)
|
23
29
|
# user.avatar_stored? # user.avatar && user.avatar_store.uploaded?(user.avatar)
|
24
30
|
#
|
25
|
-
#
|
31
|
+
# # attacher equivalents
|
32
|
+
# user.avatar_attacher.cached?
|
33
|
+
# user.avatar_attacher.stored?
|
26
34
|
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
# the result of the
|
35
|
+
# ## `update_attachment`
|
36
|
+
#
|
37
|
+
# This method updates the record's attachment with the result of the given
|
38
|
+
# block.
|
30
39
|
#
|
31
40
|
# user.update_avatar do |avatar|
|
32
41
|
# user.avatar_store.upload(avatar) # saved to the record
|
33
42
|
# end
|
34
43
|
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
44
|
+
# # attacher equivalent
|
45
|
+
# user.avatar_attacher.update_stored { |avatar| }
|
46
|
+
#
|
47
|
+
# The block will get triggered _only_ if the attachment is present and not
|
48
|
+
# cached, *and* will save the record only if the record's attachment
|
49
|
+
# hasn't changed in the time it took to execute the block. This method is
|
50
|
+
# most useful for adding/removing versions and changing locations of files.
|
39
51
|
module MigrationHelpers
|
52
|
+
def self.configure(uploader, options = {})
|
53
|
+
warn "The :delegate option in migration_helpers Shrine plugin will default to false in Shrine 2. To remove this warning, set :delegate explicitly." if !options.key?(:delegate)
|
54
|
+
uploader.opts[:migration_helpers_delegate] = options.fetch(:delegate, true)
|
55
|
+
end
|
56
|
+
|
40
57
|
module AttachmentMethods
|
41
58
|
def initialize(name)
|
42
59
|
super
|
43
60
|
|
61
|
+
return if shrine_class.opts[:migration_helpers_delegate] == false
|
62
|
+
|
44
63
|
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
45
64
|
def update_#{name}(&block)
|
46
65
|
#{name}_attacher.update_stored(&block)
|
@@ -25,7 +25,7 @@ class Shrine
|
|
25
25
|
# deleted after upload.
|
26
26
|
module Moving
|
27
27
|
def self.configure(uploader, storages:)
|
28
|
-
uploader.opts[:
|
28
|
+
uploader.opts[:moving_storages] = storages
|
29
29
|
end
|
30
30
|
|
31
31
|
module InstanceMethods
|
@@ -38,6 +38,7 @@ class Shrine
|
|
38
38
|
if movable?(io, context)
|
39
39
|
move(io, context)
|
40
40
|
else
|
41
|
+
warn "The #{storage_key.inspect} Shrine storage doesn't support moving a #{io.inspect}. It is currently still deleted, but it won't be in Shrine 2."
|
41
42
|
super
|
42
43
|
io.delete if io.respond_to?(:delete)
|
43
44
|
end
|
@@ -55,7 +56,7 @@ class Shrine
|
|
55
56
|
end
|
56
57
|
|
57
58
|
def move?(io, context)
|
58
|
-
opts[:
|
59
|
+
opts[:moving_storages].include?(storage_key)
|
59
60
|
end
|
60
61
|
end
|
61
62
|
end
|
@@ -1,24 +1,15 @@
|
|
1
|
-
require "thread
|
2
|
-
|
3
|
-
Thread::Pool.abort_on_exception = true
|
1
|
+
require "thread"
|
4
2
|
|
5
3
|
class Shrine
|
6
4
|
module Plugins
|
7
|
-
# The parallelize plugin parallelizes
|
8
|
-
#
|
5
|
+
# The parallelize plugin parallelizes uploads and deletes when handling
|
6
|
+
# versions, using threads.
|
9
7
|
#
|
10
8
|
# plugin :parallelize
|
11
9
|
#
|
12
|
-
# This plugin is generally only useful as an addition to the versions
|
13
|
-
# plugin, where multiple files are being uploaded and deleted at once. Note
|
14
|
-
# that it's not possible for this plugin to parallelize processing, but it
|
15
|
-
# should be easy to do that manually.
|
16
|
-
#
|
17
10
|
# By default a pool of 3 threads will be used, but you can change that:
|
18
11
|
#
|
19
12
|
# plugin :parallelize, threads: 5
|
20
|
-
#
|
21
|
-
# [thread]: https://github.com/meh/ruby-thread
|
22
13
|
module Parallelize
|
23
14
|
def self.configure(uploader, threads: 3)
|
24
15
|
uploader.opts[:parallelize_threads] = threads
|
@@ -36,20 +27,48 @@ class Shrine
|
|
36
27
|
private
|
37
28
|
|
38
29
|
def put(io, context)
|
39
|
-
context[:thread_pool].
|
30
|
+
context[:thread_pool].enqueue { super }
|
40
31
|
end
|
41
32
|
|
42
33
|
def remove(uploaded_file, context)
|
43
|
-
context[:thread_pool].
|
34
|
+
context[:thread_pool].enqueue { super }
|
44
35
|
end
|
45
36
|
|
46
37
|
# We initialize a thread pool with configured number of threads.
|
47
|
-
def with_pool
|
48
|
-
pool =
|
38
|
+
def with_pool(&block)
|
39
|
+
pool = ThreadPool.new(opts[:parallelize_threads])
|
49
40
|
result = yield pool
|
50
|
-
pool.
|
41
|
+
pool.perform
|
51
42
|
result
|
52
43
|
end
|
44
|
+
|
45
|
+
class ThreadPool
|
46
|
+
def initialize(thread_count)
|
47
|
+
@thread_count = thread_count
|
48
|
+
@tasks = Queue.new
|
49
|
+
end
|
50
|
+
|
51
|
+
def enqueue(&task)
|
52
|
+
@tasks.enq(task)
|
53
|
+
end
|
54
|
+
|
55
|
+
def perform
|
56
|
+
threads = @thread_count.times.map { spawn_thread }
|
57
|
+
threads.each(&:join)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def spawn_thread
|
63
|
+
Thread.new do
|
64
|
+
Thread.current.abort_on_exception = true
|
65
|
+
loop do
|
66
|
+
task = @tasks.deq(true) rescue break
|
67
|
+
task.call
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
53
72
|
end
|
54
73
|
end
|
55
74
|
|
@@ -1,35 +1,3 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
module Plugins
|
5
|
-
# The restore_cached plugin ensures the cached file data hasn't been
|
6
|
-
# tampered with, by restoring its metadata after assignment. The user can
|
7
|
-
# tamper with the cached file data by modifying the hidden field before
|
8
|
-
# submitting the form.
|
9
|
-
#
|
10
|
-
# Firstly the assignment is terminated if the cached file doesn't exist,
|
11
|
-
# which can happen if the user changes the "id" or "storage" data. If the
|
12
|
-
# cached file exists, the metadata is reextracted from the original file
|
13
|
-
# and replaced with the potentially tampered with ones.
|
14
|
-
#
|
15
|
-
# plugin :restore_cached
|
16
|
-
module RestoreCached
|
17
|
-
module AttacherMethods
|
18
|
-
private
|
19
|
-
|
20
|
-
def assign_cached(value)
|
21
|
-
uploaded_file = uploaded_file(value) do |file|
|
22
|
-
next unless cache.uploaded?(file)
|
23
|
-
return unless file.exists?
|
24
|
-
real_metadata = cache.extract_metadata(file.to_io, context)
|
25
|
-
file.metadata.update(real_metadata)
|
26
|
-
end
|
27
|
-
|
28
|
-
super(uploaded_file)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
register_plugin(:restore_cached, RestoreCached)
|
34
|
-
end
|
35
|
-
end
|
1
|
+
warn "The restore_cached Shrine plugin has been renamed to \"restore_cached_data\". Loading the plugin through \"restore_cached\" will not work in Shrine 2."
|
2
|
+
require "shrine/plugins/restore_cached_data"
|
3
|
+
Shrine::Plugins.register_plugin(:restore_cached, Shrine::Plugins::RestoreCachedData)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The restore_cached_data plugin ensures the cached file data hasn't been
|
4
|
+
# tampered with, by restoring the metadata after assignment. The user can
|
5
|
+
# tamper with the cached file data by modifying the hidden field before
|
6
|
+
# submitting the form.
|
7
|
+
#
|
8
|
+
# Firstly the assignment is terminated if the cached file doesn't exist,
|
9
|
+
# which can happen if the user changes the "id" or "storage" data. If the
|
10
|
+
# cached file exists, the metadata is reextracted from the original file
|
11
|
+
# and replaced with the potentially tampered with ones.
|
12
|
+
#
|
13
|
+
# plugin :restore_cached_data
|
14
|
+
module RestoreCachedData
|
15
|
+
module AttacherMethods
|
16
|
+
private
|
17
|
+
|
18
|
+
def assign_cached(value)
|
19
|
+
cached_file = uploaded_file(value) do |cached_file|
|
20
|
+
next unless cache.uploaded?(cached_file)
|
21
|
+
return unless cached_file.exists?
|
22
|
+
real_metadata = cache.extract_metadata(cached_file.to_io, context)
|
23
|
+
cached_file.metadata.update(real_metadata)
|
24
|
+
cached_file.close
|
25
|
+
end
|
26
|
+
|
27
|
+
super(cached_file)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
register_plugin(:restore_cached_data, RestoreCachedData)
|
33
|
+
end
|
34
|
+
end
|
@@ -73,18 +73,12 @@ class Shrine
|
|
73
73
|
|
74
74
|
# Updates the current attachment with the new one, unless the current
|
75
75
|
# attachment has changed.
|
76
|
-
def swap(uploaded_file)
|
77
|
-
record.db.transaction do
|
78
|
-
break if record.send("#{name}_data") != record.reload.send("#{name}_data")
|
79
|
-
super
|
80
|
-
end
|
81
|
-
rescue ::Sequel::Error
|
82
|
-
end
|
83
|
-
|
84
|
-
# We save the record after updating, raising any validation errors.
|
85
76
|
def update(uploaded_file)
|
86
|
-
|
87
|
-
|
77
|
+
record.this
|
78
|
+
.where(:"#{name}_data" => record.send(:"#{name}_data"))
|
79
|
+
.update(:"#{name}_data" => uploaded_file.to_json)
|
80
|
+
record.reload
|
81
|
+
rescue ::Sequel::Error
|
88
82
|
end
|
89
83
|
|
90
84
|
# Support for Postgres JSON columns.
|
@@ -50,21 +50,23 @@ class Shrine
|
|
50
50
|
def extract_dimensions(io)
|
51
51
|
analyzer = opts[:dimensions_analyzer]
|
52
52
|
|
53
|
-
if io.respond_to?(:width) && io.respond_to?(:height)
|
53
|
+
dimensions = if io.respond_to?(:width) && io.respond_to?(:height)
|
54
54
|
[io.width, io.height]
|
55
55
|
elsif analyzer.is_a?(Symbol)
|
56
56
|
send(:"_extract_dimensions_with_#{analyzer}", io)
|
57
57
|
else
|
58
58
|
analyzer.call(io)
|
59
59
|
end
|
60
|
+
|
61
|
+
io.rewind
|
62
|
+
|
63
|
+
dimensions
|
60
64
|
end
|
61
65
|
|
62
66
|
private
|
63
67
|
|
64
68
|
def _extract_dimensions_with_fastimage(io)
|
65
|
-
|
66
|
-
io.rewind # https://github.com/sdsykes/fastimage/pull/66
|
67
|
-
result
|
69
|
+
FastImage.size(io)
|
68
70
|
end
|
69
71
|
end
|
70
72
|
|
@@ -75,7 +75,7 @@ class Shrine
|
|
75
75
|
# store_dimensions plugin.
|
76
76
|
def validate_max_width(max, message: nil)
|
77
77
|
raise Error, ":store_dimensions plugin is required" if !get.respond_to?(:width)
|
78
|
-
if get.width > max
|
78
|
+
if get.width && get.width > max
|
79
79
|
errors << error_message(:max_width, message, max)
|
80
80
|
end
|
81
81
|
end
|
@@ -84,7 +84,7 @@ class Shrine
|
|
84
84
|
# store_dimensions plugin.
|
85
85
|
def validate_min_width(min, message: nil)
|
86
86
|
raise Error, ":store_dimensions plugin is required" if !get.respond_to?(:width)
|
87
|
-
if get.width < min
|
87
|
+
if get.width && get.width < min
|
88
88
|
errors << error_message(:min_width, message, min)
|
89
89
|
end
|
90
90
|
end
|
@@ -93,7 +93,7 @@ class Shrine
|
|
93
93
|
# store_dimensions plugin.
|
94
94
|
def validate_max_height(max, message: nil)
|
95
95
|
raise Error, ":store_dimensions plugin is required" if !get.respond_to?(:height)
|
96
|
-
if get.height > max
|
96
|
+
if get.height && get.height > max
|
97
97
|
errors << error_message(:max_height, message, max)
|
98
98
|
end
|
99
99
|
end
|
@@ -102,7 +102,7 @@ class Shrine
|
|
102
102
|
# store_dimensions plugin.
|
103
103
|
def validate_min_height(min, message: nil)
|
104
104
|
raise Error, ":store_dimensions plugin is required" if !get.respond_to?(:height)
|
105
|
-
if get.height < min
|
105
|
+
if get.height && get.height < min
|
106
106
|
errors << error_message(:min_height, message, min)
|
107
107
|
end
|
108
108
|
end
|
@@ -62,6 +62,18 @@ class Shrine
|
|
62
62
|
#
|
63
63
|
# user.avatar_url(:medium) #=> "http://example.com/medium.jpg"
|
64
64
|
#
|
65
|
+
# If you already have some versions processed in the foreground when a
|
66
|
+
# background job is kicked off, you can setup explicit URL fallbacks:
|
67
|
+
#
|
68
|
+
# plugin :versions,
|
69
|
+
# names: [:thumb, :thumb_2x, :large, :large_2x],
|
70
|
+
# fallbacks: {:thumb_2x => :thumb, :large_2x => :large}
|
71
|
+
#
|
72
|
+
# # ... (background job is kicked off)
|
73
|
+
#
|
74
|
+
# user.avatar_url(:thumb_2x) # returns :thumb URL until :thumb_2x becomes available
|
75
|
+
# user.avatar_url(:large_2x) # returns :large URL until :large_2x becomes available
|
76
|
+
#
|
65
77
|
# Any additional options will be properly forwarded to the underlying
|
66
78
|
# storage:
|
67
79
|
#
|
@@ -75,7 +87,7 @@ class Shrine
|
|
75
87
|
# end
|
76
88
|
#
|
77
89
|
# When deleting versions, any multi delete capabilities will be leveraged,
|
78
|
-
# so when
|
90
|
+
# so when using Storage::S3, deleting versions will issue only a single HTTP
|
79
91
|
# request. If you want to delete versions manually, you can use
|
80
92
|
# `Shrine#delete`:
|
81
93
|
#
|
@@ -86,8 +98,9 @@ class Shrine
|
|
86
98
|
uploader.plugin :multi_delete
|
87
99
|
end
|
88
100
|
|
89
|
-
def self.configure(uploader, names:)
|
101
|
+
def self.configure(uploader, names:, fallbacks: {})
|
90
102
|
uploader.opts[:version_names] = names
|
103
|
+
uploader.opts[:version_fallbacks] = fallbacks
|
91
104
|
end
|
92
105
|
|
93
106
|
module ClassMethods
|
@@ -95,25 +108,20 @@ class Shrine
|
|
95
108
|
opts[:version_names]
|
96
109
|
end
|
97
110
|
|
111
|
+
def version_fallbacks
|
112
|
+
opts[:version_fallbacks]
|
113
|
+
end
|
114
|
+
|
98
115
|
# Checks that the identifier is a registered version.
|
99
116
|
def version?(name)
|
100
117
|
version_names.map(&:to_s).include?(name.to_s)
|
101
118
|
end
|
102
119
|
|
103
|
-
# Asserts that the hash doesn't contain any unknown versions.
|
104
|
-
def versions!(hash)
|
105
|
-
hash.select { |name, _| version?(name) or raise Error, "unknown version: #{name.inspect}" }
|
106
|
-
end
|
107
|
-
|
108
|
-
# Filters the hash to contain only the registered versions.
|
109
|
-
def versions(hash)
|
110
|
-
hash.select { |name, _| version?(name) }
|
111
|
-
end
|
112
|
-
|
113
120
|
# Converts a hash of data into a hash of versions.
|
114
121
|
def uploaded_file(object, &block)
|
115
|
-
if object.is_a?(Hash) && !
|
116
|
-
|
122
|
+
if (hash = object).is_a?(Hash) && !hash.key?("storage")
|
123
|
+
hash.inject({}) do |result, (name, data)|
|
124
|
+
next result if !version?(name)
|
117
125
|
result.update(name.to_sym => uploaded_file(data, &block))
|
118
126
|
end
|
119
127
|
else
|
@@ -141,7 +149,8 @@ class Shrine
|
|
141
149
|
def _store(io, context)
|
142
150
|
if (hash = io).is_a?(Hash)
|
143
151
|
raise Error, ":location is not applicable to versions" if context.key?(:location)
|
144
|
-
|
152
|
+
hash.inject({}) do |result, (name, version)|
|
153
|
+
raise Error, "unknown version: #{name.inspect}" if !self.class.version?(name)
|
145
154
|
result.update(name => _store(version, version: name, **context))
|
146
155
|
end
|
147
156
|
else
|
@@ -166,9 +175,11 @@ class Shrine
|
|
166
175
|
def url(version = nil, **options)
|
167
176
|
if get.is_a?(Hash)
|
168
177
|
if version
|
169
|
-
raise Error, "unknown version: #{version.inspect}" if !shrine_class.
|
178
|
+
raise Error, "unknown version: #{version.inspect}" if !shrine_class.version?(version)
|
170
179
|
if file = get[version]
|
171
180
|
file.url(**options)
|
181
|
+
elsif fallback = shrine_class.version_fallbacks[version]
|
182
|
+
url(fallback, **options)
|
172
183
|
else
|
173
184
|
default_url(**options, version: version)
|
174
185
|
end
|
@@ -7,18 +7,21 @@ require "tempfile"
|
|
7
7
|
class Shrine
|
8
8
|
# Error which is thrown when Storage::Linter fails.
|
9
9
|
class LintError < Error
|
10
|
-
attr_reader :errors
|
11
|
-
|
12
|
-
def initialize(errors)
|
13
|
-
@errors = errors
|
14
|
-
super(errors.to_s)
|
15
|
-
end
|
16
10
|
end
|
17
11
|
|
18
12
|
module Storage
|
19
|
-
# Checks if the storage conforms to Shrine's specification.
|
20
|
-
#
|
21
|
-
#
|
13
|
+
# Checks if the storage conforms to Shrine's specification.
|
14
|
+
#
|
15
|
+
# Shrine::Storage::Linter.new(storage).call
|
16
|
+
#
|
17
|
+
# If the check fails, by default it raises a `Shrine::LintError`, but you
|
18
|
+
# can also specify `action: :warn`:
|
19
|
+
#
|
20
|
+
# Shrine::Storage::Linter.new(storage, action: :warn).call
|
21
|
+
#
|
22
|
+
# You can also specify an IO factory which the storage will use:
|
23
|
+
#
|
24
|
+
# Shrine::Storage::Linter.new(storage).call(->{File.open("test/fixtures/image.jpg")})
|
22
25
|
class Linter
|
23
26
|
def self.call(*args)
|
24
27
|
new(*args).call
|
@@ -27,68 +30,107 @@ class Shrine
|
|
27
30
|
def initialize(storage, action: :error)
|
28
31
|
@storage = storage
|
29
32
|
@action = action
|
30
|
-
@errors = []
|
31
33
|
end
|
32
34
|
|
33
|
-
def call(io_factory =
|
34
|
-
storage.upload(io_factory.call, id = "foo
|
35
|
+
def call(io_factory = default_io_factory)
|
36
|
+
storage.upload(io_factory.call, id = "foo", {})
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
|
38
|
+
lint_download(id)
|
39
|
+
lint_open(id)
|
40
|
+
lint_read(id)
|
41
|
+
lint_exists(id)
|
42
|
+
lint_url(id)
|
43
|
+
lint_stream(id) if storage.respond_to?(:stream)
|
44
|
+
lint_delete(id)
|
39
45
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
46
|
+
if storage.respond_to?(:move)
|
47
|
+
uploaded_file = uploader.upload(io_factory.call, location: "bar")
|
48
|
+
lint_move(uploaded_file, "quux")
|
49
|
+
end
|
44
50
|
|
45
|
-
if storage.respond_to?(:
|
46
|
-
|
47
|
-
|
48
|
-
error! "#stream does not yield any chunks" if content.empty?
|
51
|
+
if storage.respond_to?(:multi_delete)
|
52
|
+
storage.upload(io_factory.call, id = "baz")
|
53
|
+
lint_multi_delete(id)
|
49
54
|
end
|
50
55
|
|
51
|
-
storage.
|
52
|
-
|
56
|
+
storage.upload(io_factory.call, id = "quux")
|
57
|
+
lint_clear(id)
|
58
|
+
end
|
53
59
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
60
|
+
def lint_download(id)
|
61
|
+
downloaded = storage.download(id)
|
62
|
+
error :download, "doesn't return a Tempfile" if !downloaded.is_a?(Tempfile)
|
63
|
+
error :download, "returns an empty IO object" if downloaded.read.empty?
|
64
|
+
end
|
65
|
+
|
66
|
+
def lint_open(id)
|
67
|
+
opened = storage.open(id)
|
68
|
+
error :open, "doesn't return a valid IO object" if !io?(opened)
|
69
|
+
error :open, "returns an empty IO object" if opened.read.empty?
|
70
|
+
end
|
71
|
+
|
72
|
+
def lint_read(id)
|
73
|
+
read = storage.read(id)
|
74
|
+
error :read, "doesn't return a string" if !read.is_a?(String)
|
75
|
+
error :read, "returns an empty string" if read.empty?
|
76
|
+
end
|
77
|
+
|
78
|
+
def lint_exists(id)
|
79
|
+
error :exists?, "returns false for a file that was uploaded" if !storage.exists?(id)
|
80
|
+
end
|
81
|
+
|
82
|
+
def lint_url(id)
|
83
|
+
# just assert #url exists, it isn't required to return anything
|
84
|
+
url = storage.url(id)
|
85
|
+
error :url, "should return either nil or a string" if !(url.nil? || url.is_a?(String))
|
86
|
+
end
|
87
|
+
|
88
|
+
def lint_stream(id)
|
89
|
+
streamed = storage.enum_for(:stream, id).to_a
|
90
|
+
chunks = streamed.map { |(chunk, _)| chunk }
|
91
|
+
content_length = Array(streamed[0])[1]
|
92
|
+
|
93
|
+
error :stream, "doesn't yield any chunks" if chunks.empty?
|
94
|
+
error :stream, "yielded chunks sum up to empty content" if chunks.inject("", :+).empty?
|
95
|
+
|
96
|
+
if Array(streamed.first).size == 2
|
97
|
+
error :stream, "yielded content length isn't a number" if !content_length.is_a?(Integer)
|
69
98
|
end
|
99
|
+
end
|
70
100
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
101
|
+
def lint_delete(id)
|
102
|
+
storage.delete(id)
|
103
|
+
error :delete, "file still #exists? after deleting" if storage.exists?(id)
|
104
|
+
end
|
105
|
+
|
106
|
+
def lint_move(uploaded_file, id)
|
107
|
+
if storage.movable?(uploaded_file, id)
|
108
|
+
storage.move(uploaded_file, id, {})
|
109
|
+
error :exists?, "returns false for destination after #move" if !storage.exists?(id)
|
110
|
+
error :exists?, "returns true for source after #move" if storage.exists?(uploaded_file.id)
|
75
111
|
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def lint_multi_delete(id)
|
115
|
+
storage.multi_delete([id])
|
116
|
+
error :exists?, "returns true for a file that was multi-deleted" if storage.exists?(id)
|
117
|
+
end
|
118
|
+
|
119
|
+
def lint_clear(id)
|
120
|
+
storage.clear!(:confirm)
|
121
|
+
error :clear!, "file still #exists? after clearing" if storage.exists?(id)
|
76
122
|
|
77
123
|
begin
|
78
124
|
storage.clear!
|
79
|
-
error
|
125
|
+
error :clear!, "should raise Shrine::Confirm if :confirm is not passed in"
|
80
126
|
rescue Shrine::Confirm
|
81
127
|
end
|
82
|
-
|
83
|
-
storage.upload(io_factory.call, id = "foo.jpg")
|
84
|
-
storage.clear!(:confirm)
|
85
|
-
error! "file still #exists? after #clear! was called" if storage.exists?(id)
|
86
|
-
|
87
|
-
raise LintError.new(@errors) if @errors.any? && @action == :error
|
88
128
|
end
|
89
129
|
|
90
130
|
private
|
91
131
|
|
132
|
+
attr_reader :storage
|
133
|
+
|
92
134
|
def uploader
|
93
135
|
shrine = Class.new(Shrine)
|
94
136
|
shrine.storages[:storage] = storage
|
@@ -96,18 +138,27 @@ class Shrine
|
|
96
138
|
end
|
97
139
|
|
98
140
|
def io?(object)
|
99
|
-
|
100
|
-
|
141
|
+
uploader.send(:_enforce_io, object)
|
142
|
+
true
|
143
|
+
rescue Shrine::InvalidFile
|
144
|
+
false
|
145
|
+
end
|
146
|
+
|
147
|
+
def error(method_name, message)
|
148
|
+
if @action == :error
|
149
|
+
raise LintError, full_message(method_name, message)
|
150
|
+
else
|
151
|
+
warn full_message(method_name, message)
|
101
152
|
end
|
102
|
-
missing_methods.empty?
|
103
153
|
end
|
104
154
|
|
105
|
-
def
|
106
|
-
@
|
107
|
-
warn(message) if @action.to_s.start_with?("warn")
|
155
|
+
def full_message(method_name, message)
|
156
|
+
"#{@storage.class}##{method_name} - #{message}"
|
108
157
|
end
|
109
158
|
|
110
|
-
|
159
|
+
def default_io_factory
|
160
|
+
-> { FakeIO.new("file") }
|
161
|
+
end
|
111
162
|
|
112
163
|
class FakeIO
|
113
164
|
def initialize(content)
|