image_vise 0.5.0 → 0.6.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 +5 -5
- data/SECURITY.md +2 -2
- data/image_vise.gemspec +3 -2
- data/lib/image_vise.rb +6 -1
- data/lib/image_vise/fetchers/fetcher_file.rb +23 -3
- data/lib/image_vise/fetchers/fetcher_http.rb +19 -7
- data/lib/image_vise/render_engine.rb +36 -24
- data/lib/image_vise/version.rb +1 -1
- data/lib/image_vise/writers/auto_writer.rb +4 -12
- data/lib/image_vise/writers/jpeg_writer.rb +2 -2
- data/spec/image_vise/fetcher_file_spec.rb +18 -1
- data/spec/image_vise/fetcher_http_spec.rb +15 -0
- data/spec/image_vise/render_engine_spec.rb +5 -6
- data/spec/image_vise_spec.rb +33 -0
- metadata +33 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1be0d4d8682d0436356b4ea5e0023869b50709dd
|
4
|
+
data.tar.gz: de463d93ee8e7e55c0cfa59db9a5c0dee9b91a25
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e93df7c9c8723663ab09a7ba09916f5327ec7b2a71e63818b7e4bf72d851dcb119fd21c9133bd6e97158fc672e9ac64043db9f6f897a5dd9b051dd7e706c5315
|
7
|
+
data.tar.gz: 15e293b3e33026a78f486ba2921a8df5a1af1b51fb08c3984864b81316697c14979715148e39eaa00f0b85a7228cc0a79f19a7b8f41002558f6e86ceaec507cd
|
data/SECURITY.md
CHANGED
@@ -31,9 +31,9 @@ CDN cache because the query string params contain extra data.
|
|
31
31
|
|
32
32
|
## Protection for remote URLs from HTTP(s) origins
|
33
33
|
|
34
|
-
Only URLs
|
34
|
+
Only URLs on whitelisted hosts are going to be fetched. If there are no hosts added,
|
35
35
|
any remote URL is going to cause an exception. No special verification for whether the upstream must be HTTP
|
36
|
-
or HTTPS is performed at this time.
|
36
|
+
or HTTPS is performed at this time, but HTTPS upstreams' SSL certificats _will_ be verified.
|
37
37
|
|
38
38
|
## Protection for "file:/" URLs
|
39
39
|
|
data/image_vise.gemspec
CHANGED
@@ -27,12 +27,13 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
28
|
spec.require_paths = ["lib"]
|
29
29
|
|
30
|
-
spec.add_dependency 'patron', '~> 0.
|
30
|
+
spec.add_dependency 'patron', '~> 0.9'
|
31
31
|
spec.add_dependency 'rmagick', '~> 2.15'
|
32
32
|
spec.add_dependency 'ks'
|
33
|
-
spec.add_dependency 'magic_bytes', '~> 1'
|
34
33
|
spec.add_dependency 'rack', '>= 1', '< 3'
|
34
|
+
spec.add_dependency 'format_parser', '>= 0.12.1', '< 1.0'
|
35
35
|
|
36
|
+
spec.add_development_dependency 'magic_bytes', '~> 1'
|
36
37
|
spec.add_development_dependency "bundler", "~> 1.7"
|
37
38
|
spec.add_development_dependency "rake", "~> 12.2"
|
38
39
|
spec.add_development_dependency "rack-test"
|
data/lib/image_vise.rb
CHANGED
@@ -6,6 +6,7 @@ require 'magic_bytes'
|
|
6
6
|
require 'thread'
|
7
7
|
require 'base64'
|
8
8
|
require 'rack'
|
9
|
+
require 'format_parser'
|
9
10
|
|
10
11
|
class ImageVise
|
11
12
|
require_relative 'image_vise/version'
|
@@ -16,6 +17,10 @@ class ImageVise
|
|
16
17
|
# The default cache liftime is 30 days, and will be used if no custom lifetime is set.
|
17
18
|
DEFAULT_CACHE_LIFETIME = 2_592_000
|
18
19
|
|
20
|
+
# The default limit on how large may a file loaded for processing be, in bytes. This
|
21
|
+
# is in addition to the constraints on the file format.
|
22
|
+
DEFAULT_MAXIMUM_SOURCE_FILE_SIZE = 48 * 1024 * 1024
|
23
|
+
|
19
24
|
@allowed_hosts = Set.new
|
20
25
|
@keys = Set.new
|
21
26
|
@operators = {}
|
@@ -172,7 +177,7 @@ class ImageVise
|
|
172
177
|
return unless maybe_tempfile
|
173
178
|
ImageVise::Measurometer.instrument('image_vise.tempfile_unlink') do
|
174
179
|
maybe_tempfile.close unless maybe_tempfile.closed?
|
175
|
-
maybe_tempfile.unlink
|
180
|
+
maybe_tempfile.unlink if maybe_tempfile.respond_to?(:unlink)
|
176
181
|
end
|
177
182
|
end
|
178
183
|
end
|
@@ -2,11 +2,16 @@ class ImageVise::FetcherFile
|
|
2
2
|
class AccessError < StandardError
|
3
3
|
def http_status; 403; end
|
4
4
|
end
|
5
|
+
|
6
|
+
class SizeError < AccessError
|
7
|
+
def http_status; 400; end
|
8
|
+
end
|
9
|
+
|
5
10
|
def self.fetch_uri_to_tempfile(uri)
|
6
11
|
tf = Tempfile.new 'imagevise-localfs-copy'
|
7
|
-
real_path_on_filesystem =
|
8
|
-
verify_filesystem_access!
|
9
|
-
|
12
|
+
real_path_on_filesystem = uri_to_path(uri)
|
13
|
+
verify_filesystem_access!(real_path_on_filesystem)
|
14
|
+
verify_file_size_within_limit!(real_path_on_filesystem)
|
10
15
|
File.open(real_path_on_filesystem, 'rb') do |f|
|
11
16
|
IO.copy_stream(f, tf)
|
12
17
|
end
|
@@ -16,6 +21,10 @@ class ImageVise::FetcherFile
|
|
16
21
|
raise e
|
17
22
|
end
|
18
23
|
|
24
|
+
def self.uri_to_path(uri)
|
25
|
+
File.expand_path(URI.decode(uri.path))
|
26
|
+
end
|
27
|
+
|
19
28
|
def self.verify_filesystem_access!(path_on_filesystem)
|
20
29
|
patterns = ImageVise.allowed_filesystem_sources
|
21
30
|
matches = patterns.any? { |glob_pattern| File.fnmatch?(glob_pattern, path_on_filesystem) }
|
@@ -23,5 +32,16 @@ class ImageVise::FetcherFile
|
|
23
32
|
raise AccessError, "#{path_on_filesystem} is not on the path whitelist" unless matches
|
24
33
|
end
|
25
34
|
|
35
|
+
def self.verify_file_size_within_limit!(path_on_filesystem)
|
36
|
+
file_size = File.size(path_on_filesystem)
|
37
|
+
if file_size > maximum_source_file_size_bytes
|
38
|
+
raise SizeError, "#{path_on_filesystem} is too large to process (#{file_size} bytes)"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.maximum_source_file_size_bytes
|
43
|
+
ImageVise::DEFAULT_MAXIMUM_SOURCE_FILE_SIZE
|
44
|
+
end
|
45
|
+
|
26
46
|
ImageVise.register_fetcher 'file', self
|
27
47
|
end
|
@@ -14,23 +14,35 @@ class ImageVise::FetcherHTTP
|
|
14
14
|
def self.fetch_uri_to_tempfile(uri)
|
15
15
|
tf = Tempfile.new 'imagevise-http-download'
|
16
16
|
verify_uri_access!(uri)
|
17
|
+
|
17
18
|
s = Patron::Session.new
|
18
|
-
s
|
19
|
-
s.timeout = EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS
|
20
|
-
s.connect_timeout = EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS
|
21
|
-
|
19
|
+
configure_patron_session!(s)
|
22
20
|
response = s.get_file(uri.to_s, tf.path)
|
23
|
-
|
21
|
+
|
24
22
|
if response.status != 200
|
25
23
|
raise UpstreamError.new(response.status, "Unfortunate upstream response #{response.status} on #{uri}")
|
26
24
|
end
|
27
|
-
|
25
|
+
|
28
26
|
tf
|
27
|
+
rescue Patron::Aborted # File size exceeds permitted size
|
28
|
+
ImageVise.close_and_unlink(tf)
|
29
|
+
raise UpstreamError.new(400, "Upstream resource at #{uri} is too large to load")
|
29
30
|
rescue Exception => e
|
30
31
|
ImageVise.close_and_unlink(tf)
|
31
32
|
raise e
|
32
33
|
end
|
33
|
-
|
34
|
+
|
35
|
+
def self.maximum_response_size_bytes
|
36
|
+
ImageVise::DEFAULT_MAXIMUM_SOURCE_FILE_SIZE
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.configure_patron_session!(session)
|
40
|
+
session.automatic_content_encoding = true
|
41
|
+
session.timeout = EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS
|
42
|
+
session.connect_timeout = EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS
|
43
|
+
session.download_byte_limit = maximum_response_size_bytes
|
44
|
+
end
|
45
|
+
|
34
46
|
def self.verify_uri_access!(uri)
|
35
47
|
host = uri.host
|
36
48
|
return if ImageVise.allowed_hosts.include?(uri.host)
|
@@ -2,6 +2,16 @@
|
|
2
2
|
class UnsupportedInputFormat < StandardError; end
|
3
3
|
class EmptyRender < StandardError; end
|
4
4
|
|
5
|
+
class Filetype < Struct.new(:format_parser_format)
|
6
|
+
def mime
|
7
|
+
Rack::Mime.mime_type(ext)
|
8
|
+
end
|
9
|
+
|
10
|
+
def ext
|
11
|
+
".#{format_parser_format}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
5
15
|
DEFAULT_HEADERS = {
|
6
16
|
'Allow' => "GET"
|
7
17
|
}.freeze
|
@@ -28,12 +38,8 @@
|
|
28
38
|
# decoding for example).
|
29
39
|
IMAGE_CACHE_CONTROL = "public, no-transform, max-age=%d"
|
30
40
|
|
31
|
-
#
|
32
|
-
|
33
|
-
RENDER_TIMEOUT_SECONDS = 10
|
34
|
-
|
35
|
-
# Which input files we permit (based on extensions stored in MagicBytes)
|
36
|
-
PERMITTED_SOURCE_FILE_EXTENSIONS = %w( gif png jpg psd tif)
|
41
|
+
# Which input files we permit (based on format identifiers in format_parser, which are symbols)
|
42
|
+
PERMITTED_SOURCE_FORMATS = [:bmp, :tif, :jpg, :psd, :gif, :png]
|
37
43
|
|
38
44
|
# How long should we wait when fetching the image from the external host
|
39
45
|
EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS = 4
|
@@ -85,7 +91,7 @@
|
|
85
91
|
handle_request_error(e)
|
86
92
|
http_status_code = e.respond_to?(:http_status) ? e.http_status : 400
|
87
93
|
raise_exception_or_error_response(e, http_status_code)
|
88
|
-
rescue
|
94
|
+
rescue => e
|
89
95
|
if http_status_code = (e.respond_to?(:http_status) && e.http_status)
|
90
96
|
handle_request_error(e)
|
91
97
|
raise_exception_or_error_response(e, http_status_code)
|
@@ -134,7 +140,7 @@
|
|
134
140
|
# representing the processing pipeline
|
135
141
|
#
|
136
142
|
# @param image_request[ImageVise::ImageRequest] the request for the image
|
137
|
-
# @return [Array<File,
|
143
|
+
# @return [Array<File, FileType, String]
|
138
144
|
def process_image_request(image_request)
|
139
145
|
# Recover the source image URL and the pipeline instructions (all the image ops)
|
140
146
|
source_image_uri, pipeline = image_request.src_url, image_request.pipeline
|
@@ -146,24 +152,26 @@
|
|
146
152
|
|
147
153
|
# Download/copy the original into a Tempfile
|
148
154
|
fetcher = ImageVise.fetcher_for(source_image_uri.scheme)
|
149
|
-
source_file =
|
150
|
-
|
151
|
-
# Make sure we do not try to process something...questionable
|
152
|
-
source_file_type = detect_file_type(source_file)
|
153
|
-
unless source_file_type_permitted?(source_file_type)
|
154
|
-
raise UnsupportedInputFormat.new("Unsupported/unknown input file format .%s" % source_file_type.ext)
|
155
|
+
source_file = ImageVise::Measurometer.instrument('image_vise.fetch') do
|
156
|
+
fetcher.fetch_uri_to_tempfile(source_image_uri)
|
155
157
|
end
|
158
|
+
file_format = FormatParser.parse(source_file, natures: [:image]).tap { source_file.rewind }
|
159
|
+
raise UnsupportedInputFormat.new("%s has an unknown input file format" % source_image_uri) unless file_format
|
160
|
+
raise UnsupportedInputFormat.new("%s does not pass file constraints" % source_image_uri) unless permitted_format?(file_format)
|
156
161
|
|
157
162
|
render_destination_file = Tempfile.new('imagevise-render').tap{|f| f.binmode }
|
158
163
|
|
159
164
|
# Do the actual imaging stuff
|
160
|
-
expire_after =
|
165
|
+
expire_after = ImageVise::Measurometer.instrument('image_vise.render_engine.apply_pipeline') do
|
166
|
+
apply_pipeline(source_file.path, pipeline, file_format, render_destination_file.path)
|
167
|
+
end
|
161
168
|
|
162
169
|
# Catch this one early
|
163
170
|
render_destination_file.rewind
|
164
171
|
raise EmptyRender, "The rendered image was empty" if render_destination_file.size.zero?
|
165
172
|
|
166
173
|
render_file_type = detect_file_type(render_destination_file)
|
174
|
+
|
167
175
|
[render_destination_file, render_file_type, etag, expire_after]
|
168
176
|
ensure
|
169
177
|
ImageVise.close_and_unlink(source_file)
|
@@ -210,18 +218,23 @@
|
|
210
218
|
# the MIME type.
|
211
219
|
#
|
212
220
|
# @param tempfile[File] the file to perform detection on
|
213
|
-
# @return [
|
221
|
+
# @return [Symbol] the detected file format symbol that can be used as an extension
|
214
222
|
def detect_file_type(tempfile)
|
215
223
|
tempfile.rewind
|
216
|
-
|
224
|
+
parser_result = FormatParser.parse(tempfile, natures: :image).tap { tempfile.rewind }
|
225
|
+
raise "Rendered file type detection failed" unless parser_result
|
226
|
+
Filetype.new(parser_result.format)
|
217
227
|
end
|
218
228
|
|
219
|
-
# Tells whether the
|
229
|
+
# Tells whether the file described by the given FormatParser result object
|
230
|
+
# can be accepted for processing
|
220
231
|
#
|
221
|
-
# @param
|
232
|
+
# @param format_parser_result[FormatParser::Image] file information descriptor
|
222
233
|
# @return [Boolean]
|
223
|
-
def
|
224
|
-
|
234
|
+
def permitted_format?(format_parser_result)
|
235
|
+
return false unless PERMITTED_SOURCE_FORMATS.include?(format_parser_result.format)
|
236
|
+
return false if format_parser_result.has_multiple_frames
|
237
|
+
true
|
225
238
|
end
|
226
239
|
|
227
240
|
# Lists exceptions that should lead to the request being flagged
|
@@ -279,8 +292,7 @@
|
|
279
292
|
# @param pipeline[#apply!(Magick::Image)] the processing pipeline
|
280
293
|
# @param render_to_path[String] the path to write the rendered image to
|
281
294
|
# @return [void]
|
282
|
-
def apply_pipeline(source_file_path, pipeline,
|
283
|
-
render_file_type = source_file_type
|
295
|
+
def apply_pipeline(source_file_path, pipeline, source_format_parser_result, render_to_path)
|
284
296
|
|
285
297
|
# Load the first frame of the animated GIF _or_ the blended compatibility layer from Photoshop
|
286
298
|
image_list = ImageVise::Measurometer.instrument('image_vise.load_pixbuf') do
|
@@ -290,7 +302,7 @@
|
|
290
302
|
magick_image = image_list.first # Picks up the "precomp" PSD layer in compatibility mode, or the first frame of a GIF
|
291
303
|
|
292
304
|
# If any operators want to stash some data for downstream use we use this Hash
|
293
|
-
metadata = {}
|
305
|
+
metadata = {format_parser_result: source_format_parser_result}
|
294
306
|
|
295
307
|
# Apply the pipeline (all the image operators)
|
296
308
|
pipeline.apply!(magick_image, metadata)
|
data/lib/image_vise/version.rb
CHANGED
@@ -3,21 +3,13 @@
|
|
3
3
|
# be chosen. Since ImageVise URLs do not contain a file extension we are free to pick
|
4
4
|
# the suitable format at render time
|
5
5
|
class ImageVise::AutoWriter
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
# Renders the file as a jpg if the custom output filetype operator is used
|
10
|
-
JPG_FILE_TYPE = MagicBytes::FileType.new('jpg','image/jpeg').freeze
|
11
|
-
|
6
|
+
PNG_EXT = 'png'
|
7
|
+
JPG_EXT = 'jpg'
|
12
8
|
def write_image!(magick_image, _, render_to_path)
|
13
9
|
# If processing the image has created an alpha channel, use PNG always.
|
14
10
|
# Otherwise, keep the original format for as far as the supported formats list goes.
|
15
|
-
|
16
|
-
|
17
|
-
else
|
18
|
-
JPG_FILE_TYPE
|
19
|
-
end
|
20
|
-
magick_image.format = render_file_type.ext
|
11
|
+
extension = magick_image.alpha? ? PNG_EXT : JPG_EXT
|
12
|
+
magick_image.format = extension
|
21
13
|
magick_image.write(render_to_path)
|
22
14
|
end
|
23
15
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
class ImageVise::JPGWriter < Ks.strict(:quality)
|
2
|
-
|
2
|
+
JPG_EXT = 'jpg'
|
3
3
|
|
4
4
|
def write_image!(magick_image, _, render_to_path)
|
5
5
|
q = self.quality # to avoid the changing "self" context
|
6
|
-
magick_image.format =
|
6
|
+
magick_image.format = JPG_EXT
|
7
7
|
magick_image.write(render_to_path) { self.quality = q }
|
8
8
|
end
|
9
9
|
end
|
@@ -32,7 +32,7 @@ describe ImageVise::FetcherFile do
|
|
32
32
|
ImageVise::FetcherFile.fetch_uri_to_tempfile(uri)
|
33
33
|
}.to raise_error(ImageVise::FetcherFile::AccessError)
|
34
34
|
end
|
35
|
-
|
35
|
+
|
36
36
|
it 'raises a meaningful exception if this file is not permitted as source' do
|
37
37
|
path = File.expand_path(__FILE__)
|
38
38
|
|
@@ -45,4 +45,21 @@ describe ImageVise::FetcherFile do
|
|
45
45
|
ImageVise::FetcherFile.fetch_uri_to_tempfile(uri)
|
46
46
|
}.to raise_error(ImageVise::FetcherFile::AccessError)
|
47
47
|
end
|
48
|
+
|
49
|
+
it 'raises a meaningful exception if the image exceeds the maximum permitted size' do
|
50
|
+
path = File.expand_path(__FILE__)
|
51
|
+
ruby_files_in_this_directory = __dir__ + '/*.rb'
|
52
|
+
ImageVise.allow_filesystem_source! ruby_files_in_this_directory
|
53
|
+
|
54
|
+
uri = URI('file://' + URI.encode(path))
|
55
|
+
expect(ImageVise::FetcherFile).to receive(:maximum_source_file_size_bytes).and_return(1)
|
56
|
+
|
57
|
+
expect {
|
58
|
+
ImageVise::FetcherFile.fetch_uri_to_tempfile(uri)
|
59
|
+
}.to raise_error {|e|
|
60
|
+
expect(e).to be_kind_of(ImageVise::FetcherFile::AccessError)
|
61
|
+
expect(e.message).to match(/is too large to process/)
|
62
|
+
expect(e.http_status).to eq(400)
|
63
|
+
}
|
64
|
+
end
|
48
65
|
end
|
@@ -31,6 +31,21 @@ describe ImageVise::FetcherHTTP do
|
|
31
31
|
}
|
32
32
|
end
|
33
33
|
|
34
|
+
it 'raises an error if the image exceeds the maximum permitted size' do
|
35
|
+
uri = URI(public_url_psd)
|
36
|
+
ImageVise.add_allowed_host! 'localhost'
|
37
|
+
expect(ImageVise::FetcherHTTP).to receive(:maximum_response_size_bytes).and_return(10)
|
38
|
+
|
39
|
+
expect {
|
40
|
+
ImageVise::FetcherHTTP.fetch_uri_to_tempfile(uri)
|
41
|
+
}.to raise_error {|e|
|
42
|
+
expect(e).to be_kind_of(ImageVise::FetcherHTTP::UpstreamError)
|
43
|
+
expect(e.message).to include(uri.to_s)
|
44
|
+
expect(e.message).to match(/is too large to load/)
|
45
|
+
expect(e.http_status).to eq(400)
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
34
49
|
it 'fetches the image into a Tempfile' do
|
35
50
|
uri = URI(public_url_psd)
|
36
51
|
ImageVise.add_allowed_host! 'localhost'
|
@@ -51,16 +51,15 @@ describe ImageVise::RenderEngine do
|
|
51
51
|
p = ImageVise::Pipeline.new.crop(width: 10, height: 10, gravity: 'c')
|
52
52
|
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
53
53
|
|
54
|
-
|
55
|
-
|
56
|
-
double(status: 200)
|
57
|
-
}
|
54
|
+
bad_data = StringIO.new('totally not an image')
|
55
|
+
expect(ImageVise::FetcherHTTP).to receive(:fetch_uri_to_tempfile).and_return(bad_data)
|
58
56
|
expect(app).to receive(:handle_request_error).and_call_original
|
59
57
|
|
60
58
|
get image_request.to_path_params('l33tness')
|
59
|
+
|
61
60
|
expect(last_response.status).to eq(400)
|
62
61
|
expect(last_response['Cache-Control']).to match(/public/)
|
63
|
-
expect(last_response.body).to include('
|
62
|
+
expect(last_response.body).to include('unknown')
|
64
63
|
end
|
65
64
|
|
66
65
|
it 'halts with 400 when a file:// URL is given and filesystem access is not enabled' do
|
@@ -224,7 +223,7 @@ describe ImageVise::RenderEngine do
|
|
224
223
|
expect(app).to receive(:process_image_request).and_call_original
|
225
224
|
expect(app).to receive(:extract_params_from_request).and_call_original
|
226
225
|
expect(app).to receive(:image_rack_response).and_call_original
|
227
|
-
expect(app).to receive(:
|
226
|
+
expect(app).to receive(:permitted_format?).and_call_original
|
228
227
|
|
229
228
|
get image_request.to_path_params('l33tness')
|
230
229
|
expect(last_response.status).to eq(200)
|
data/spec/image_vise_spec.rb
CHANGED
@@ -102,6 +102,39 @@ describe ImageVise do
|
|
102
102
|
end
|
103
103
|
end
|
104
104
|
|
105
|
+
describe '.close_and_unlink' do
|
106
|
+
it 'closes and unlinks a Tempfile' do
|
107
|
+
tf = Tempfile.new('x')
|
108
|
+
tf << "foo"
|
109
|
+
expect(tf).to receive(:close).and_call_original
|
110
|
+
expect(tf).to receive(:unlink).and_call_original
|
111
|
+
|
112
|
+
ImageVise.close_and_unlink(tf)
|
113
|
+
|
114
|
+
expect(tf).to be_closed
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'unlinks a closed Tempfile' do
|
118
|
+
tf = Tempfile.new('x')
|
119
|
+
tf << "foo"
|
120
|
+
tf.close
|
121
|
+
expect(tf).to receive(:unlink).and_call_original
|
122
|
+
|
123
|
+
ImageVise.close_and_unlink(tf)
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'works on a nil since it gets used in ensure blocks, where the variable might be empty' do
|
127
|
+
ImageVise.close_and_unlink(nil) # Should not raise anything
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'works for a StringIO which does not have unlink' do
|
131
|
+
sio = StringIO.new('some gunk')
|
132
|
+
expect(sio).not_to be_closed
|
133
|
+
ImageVise.close_and_unlink(sio)
|
134
|
+
expect(sio).to be_closed
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
105
138
|
describe 'methods dealing with the operator list' do
|
106
139
|
it 'have the basic operators already set up' do
|
107
140
|
oplist = ImageVise.defined_operator_names
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: image_vise
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-04
|
11
|
+
date: 2018-05-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: patron
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0.
|
19
|
+
version: '0.9'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '0.
|
26
|
+
version: '0.9'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rmagick
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -53,39 +53,59 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: rack
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '1'
|
62
|
+
- - "<"
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '3'
|
62
65
|
type: :runtime
|
63
66
|
prerelease: false
|
64
67
|
version_requirements: !ruby/object:Gem::Requirement
|
65
68
|
requirements:
|
66
|
-
- - "
|
69
|
+
- - ">="
|
67
70
|
- !ruby/object:Gem::Version
|
68
71
|
version: '1'
|
72
|
+
- - "<"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '3'
|
69
75
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
76
|
+
name: format_parser
|
71
77
|
requirement: !ruby/object:Gem::Requirement
|
72
78
|
requirements:
|
73
79
|
- - ">="
|
74
80
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
81
|
+
version: 0.12.1
|
76
82
|
- - "<"
|
77
83
|
- !ruby/object:Gem::Version
|
78
|
-
version: '
|
84
|
+
version: '1.0'
|
79
85
|
type: :runtime
|
80
86
|
prerelease: false
|
81
87
|
version_requirements: !ruby/object:Gem::Requirement
|
82
88
|
requirements:
|
83
89
|
- - ">="
|
84
90
|
- !ruby/object:Gem::Version
|
85
|
-
version:
|
91
|
+
version: 0.12.1
|
86
92
|
- - "<"
|
87
93
|
- !ruby/object:Gem::Version
|
88
|
-
version: '
|
94
|
+
version: '1.0'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: magic_bytes
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - "~>"
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '1'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - "~>"
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '1'
|
89
109
|
- !ruby/object:Gem::Dependency
|
90
110
|
name: bundler
|
91
111
|
requirement: !ruby/object:Gem::Requirement
|
@@ -292,7 +312,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
292
312
|
version: '0'
|
293
313
|
requirements: []
|
294
314
|
rubyforge_project:
|
295
|
-
rubygems_version: 2.
|
315
|
+
rubygems_version: 2.5.2
|
296
316
|
signing_key:
|
297
317
|
specification_version: 4
|
298
318
|
summary: Runtime thumbnailing proxy
|