Class: ImageVise::RenderEngine
- Inherits:
-
Object
- Object
- ImageVise::RenderEngine
- Defined in:
- lib/image_vise/render_engine.rb
Defined Under Namespace
Classes: EmptyRender, UnsupportedInputFormat
Constant Summary collapse
- PASSTHROUGH_STATUS_CODES =
Codes that have to be sent through to the requester
[404, 403, 503, 504, 500]
- DEFAULT_HEADERS =
{ 'Allow' => "GET" }.freeze
- JSON_ERROR_HEADERS =
To prevent some string allocations
DEFAULT_HEADERS.merge({ 'Content-Type' => 'application/json', 'Cache-Control' => 'private, max-age=0, no-cache' }).freeze
- RENDER_TIMEOUT_SECONDS =
How long is a render (the ImageMagick/write part) is allowed to take before we kill it
10
- PERMITTED_SOURCE_FILE_EXTENSIONS =
Which input files we permit (based on extensions stored in MagicBytes)
%w( gif png jpg )
- PERMITTED_OUTPUT_FILE_EXTENSIONS =
Which output files are permitted (regardless of the input format the processed images will be converted to one of these types)
%W( gif png jpg)
- EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS =
How long should we wait when fetching the image from the external host
4
- PNG_FILE_TYPE =
The default file type for images with alpha
Class.new do def self.mime; 'image/png'; end def self.ext; 'png'; end end
Instance Method Summary collapse
- #apply_pipeline(source_file_path, pipeline, source_file_type, render_to_path) ⇒ Object
- #bail(status, *errors_array) ⇒ Object
- #binary_tempfile ⇒ Object
-
#call(env) ⇒ Object
The main entry point URL, at the index so that the Sinatra app can be used in-place of a Rails controller (as opposed to having to mount it at the root of the Rails app or having all the URLs refer to a subpath).
- #close_and_unlink(f) ⇒ Object
- #copy_path_into_tempfile(path_on_filesystem) ⇒ Object
- #detect_file_type(tempfile) ⇒ Object
- #enable_forking? ⇒ Boolean
- #fetch_url(source_image_uri) ⇒ Object
-
#fetch_url_into_tempfile(source_image_uri) ⇒ Object
Fetch the given URL into a Tempfile and return the File object.
-
#handle_generic_error(err) ⇒ Object
Is meant to be overridden by subclasses, will be called when a request fails due to an error on the server (like an unexpected error in an image operator).
- #handle_request(env) ⇒ Object
-
#handle_request_error(err) ⇒ Object
Is meant to be overridden by subclasses, will be called when a request fails due to a malformed query string, unrecognized signature or other client-induced problems.
- #image_request_options ⇒ Object
- #output_file_type_permitted?(magick_bytes_file_info) ⇒ Boolean
-
#permanent_failures ⇒ Object
Lists exceptions that should lead to the request being flagged as invalid (and not 5xx).
- #raise_exception_or_error_response(exception, status_code) ⇒ Object
-
#raise_exceptions? ⇒ Boolean
Tells whether the engine must raise the exceptions further up the Rack stack, or they should be suppressed and a JSON response must be returned.
-
#setup_error_handling(rack_env) ⇒ Object
Is meant to be overridden by subclasses, will be called at the start of each reauest.
- #source_file_type_permitted?(magick_bytes_file_info) ⇒ Boolean
Instance Method Details
#apply_pipeline(source_file_path, pipeline, source_file_type, render_to_path) ⇒ Object
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/image_vise/render_engine.rb', line 194 def apply_pipeline(source_file_path, pipeline, source_file_type, render_to_path) render_file_type = source_file_type magick_image = Magick::Image.read(source_file_path)[0] pipeline.apply!(magick_image) # If processing the image has created an alpha channel, use PNG always. # Otherwise, keep the original format for as far as the supported formats list goes. render_file_type = PNG_FILE_TYPE if magick_image.alpha? render_file_type = PNG_FILE_TYPE unless output_file_type_permitted?(render_file_type) magick_image.format = render_file_type.ext magick_image.write(render_to_path) ensure ImageVise.destroy(magick_image) end |
#bail(status, *errors_array) ⇒ Object
48 49 50 51 52 |
# File 'lib/image_vise/render_engine.rb', line 48 def bail(status, *errors_array) h = JSON_ERROR_HEADERS.dup # Needed because some upstream middleware migh be modifying headers response = [status.to_i, h, [JSON.pretty_generate({errors: errors_array})]] throw :__bail, response end |
#binary_tempfile ⇒ Object
140 141 142 |
# File 'lib/image_vise/render_engine.rb', line 140 def binary_tempfile Tempfile.new('imagevise-tmp').tap{|f| f.binmode } end |
#call(env) ⇒ Object
The main entry point URL, at the index so that the Sinatra app can be used in-place of a Rails controller (as opposed to having to mount it at the root of the Rails app or having all the URLs refer to a subpath)
57 58 59 |
# File 'lib/image_vise/render_engine.rb', line 57 def call(env) catch(:__bail) { handle_request(env) } end |
#close_and_unlink(f) ⇒ Object
134 135 136 137 138 |
# File 'lib/image_vise/render_engine.rb', line 134 def close_and_unlink(f) return unless f f.close unless f.closed? f.unlink end |
#copy_path_into_tempfile(path_on_filesystem) ⇒ Object
235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/image_vise/render_engine.rb', line 235 def copy_path_into_tempfile(path_on_filesystem) tf = binary_tempfile File.open(path_on_filesystem, 'rb') do |f| IO.copy_stream(f, tf) end tf.rewind; tf rescue Errno::ENOENT tf.close; tf.unlink; bail 404, "Image file not found" rescue Exception => e tf.close; tf.unlink; raise e end |
#detect_file_type(tempfile) ⇒ Object
144 145 146 147 |
# File 'lib/image_vise/render_engine.rb', line 144 def detect_file_type(tempfile) tempfile.rewind MagicBytes.read_and_detect(tempfile) end |
#enable_forking? ⇒ Boolean
190 191 192 |
# File 'lib/image_vise/render_engine.rb', line 190 def enable_forking? ENV['IMAGE_VISE_ENABLE_FORK'] == 'yes' end |
#fetch_url(source_image_uri) ⇒ Object
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/image_vise/render_engine.rb', line 218 def fetch_url(source_image_uri) tf = binary_tempfile s = Patron::Session.new s.automatic_content_encoding = true s.timeout = EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS s.connect_timeout = EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS response = s.get_file(source_image_uri, tf.path) if PASSTHROUGH_STATUS_CODES.include?(response.status) tf.close; tf.unlink; bail response.status, "Unfortunate upstream response: #{response.status}" end tf rescue Exception => e tf.close; tf.unlink; raise e end |
#fetch_url_into_tempfile(source_image_uri) ⇒ Object
Fetch the given URL into a Tempfile and return the File object
39 40 41 42 43 44 45 46 |
# File 'lib/image_vise/render_engine.rb', line 39 def fetch_url_into_tempfile(source_image_uri) parsed = URI.parse(source_image_uri) if parsed.scheme == 'file' copy_path_into_tempfile(URI.decode(parsed.path)) else fetch_url(source_image_uri) end end |
#handle_generic_error(err) ⇒ Object
Is meant to be overridden by subclasses, will be called when a request fails due to an error on the server (like an unexpected error in an image operator)
181 182 |
# File 'lib/image_vise/render_engine.rb', line 181 def handle_generic_error(err) end |
#handle_request(env) ⇒ Object
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/image_vise/render_engine.rb', line 61 def handle_request(env) setup_error_handling(env) render_destination_file = binary_tempfile # Assume that if _any_ ETag is given the image is being requested anew as a refetch, # and the client already has it. Just respond with a 304. return [304, DEFAULT_HEADERS.dup, []] if env['HTTP_IF_NONE_MATCH'] req = Rack::Request.new(env) bail(405, 'Only GET supported') unless req.get? # Validate the inputs image_request = ImageVise::ImageRequest.to_request(qs_params: req.params, **) # Recover the source image URL and the pipeline instructions (all the image ops) source_image_uri, pipeline = image_request.src_url, image_request.pipeline raise 'Image pipeline has no operators' if pipeline.empty? # Compute an ETag which describes this image transform + image source location. # Assume the image URL contents does _never_ change. etag = image_request.cache_etag # Download the original into a Tempfile source_file = fetch_url_into_tempfile(source_image_uri) # Make sure we do not try to process something...questionable source_file_type = detect_file_type(source_file) unless source_file_type_permitted?(source_file_type) raise UnsupportedInputFormat.new("Unsupported/unknown input file format .%s" % source_file_type.ext) end # Perform the processing if enable_forking? require 'exceptional_fork' ExceptionalFork.fork_and_wait { apply_pipeline(source_file.path, pipeline, source_file_type, render_destination_file.path) } else apply_pipeline(source_file.path, pipeline, source_file_type, render_destination_file.path) end # Catch this one early raise EmptyRender, "The rendered image was empty" if render_destination_file.size.zero? render_destination_file.rewind render_file_type = detect_file_type(render_destination_file) response_headers = DEFAULT_HEADERS.merge({ 'Content-Type' => render_file_type.mime, 'Content-Length' => '%d' % render_destination_file.size, 'Cache-Control' => 'public', 'ETag' => etag }) # Wrap the body Tempfile with a self-closing response. # Once the response is read in full, the tempfile is going to be closed and unlinked. [200, response_headers, ImageVise::FileResponse.new(render_destination_file)] rescue *permanent_failures => e handle_request_error(e) raise_exception_or_error_response(e, 422) rescue Exception => e handle_generic_error(e) raise_exception_or_error_response(e, 500) ensure close_and_unlink(source_file) end |
#handle_request_error(err) ⇒ Object
Is meant to be overridden by subclasses, will be called when a request fails due to a malformed query string, unrecognized signature or other client-induced problems
175 176 |
# File 'lib/image_vise/render_engine.rb', line 175 def handle_request_error(err) end |
#image_request_options ⇒ Object
210 211 212 213 214 215 216 |
# File 'lib/image_vise/render_engine.rb', line 210 def { secrets: ImageVise.secret_keys, permitted_source_hosts: ImageVise.allowed_hosts, allowed_filesystem_patterns: ImageVise.allowed_filesystem_sources, } end |
#output_file_type_permitted?(magick_bytes_file_info) ⇒ Boolean
153 154 155 |
# File 'lib/image_vise/render_engine.rb', line 153 def output_file_type_permitted?(magick_bytes_file_info) PERMITTED_OUTPUT_FILE_EXTENSIONS.include?(magick_bytes_file_info.ext) end |
#permanent_failures ⇒ Object
Lists exceptions that should lead to the request being flagged as invalid (and not 5xx). Decent clients should not retry those requests.
159 160 161 162 163 164 165 |
# File 'lib/image_vise/render_engine.rb', line 159 def permanent_failures [ Magick::ImageMagickError, UnsupportedInputFormat, ImageVise::ImageRequest::InvalidRequest ] end |
#raise_exception_or_error_response(exception, status_code) ⇒ Object
126 127 128 129 130 131 132 |
# File 'lib/image_vise/render_engine.rb', line 126 def raise_exception_or_error_response(exception, status_code) if raise_exceptions? raise exception else bail status_code, exception. end end |
#raise_exceptions? ⇒ Boolean
Tells whether the engine must raise the exceptions further up the Rack stack, or they should be suppressed and a JSON response must be returned.
186 187 188 |
# File 'lib/image_vise/render_engine.rb', line 186 def raise_exceptions? false end |
#setup_error_handling(rack_env) ⇒ Object
Is meant to be overridden by subclasses, will be called at the start of each reauest
169 170 |
# File 'lib/image_vise/render_engine.rb', line 169 def setup_error_handling(rack_env) end |
#source_file_type_permitted?(magick_bytes_file_info) ⇒ Boolean
149 150 151 |
# File 'lib/image_vise/render_engine.rb', line 149 def source_file_type_permitted?(magick_bytes_file_info) PERMITTED_SOURCE_FILE_EXTENSIONS.include?(magick_bytes_file_info.ext) end |