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
- DEFAULT_HEADERS =
{ 'Allow' => "GET" }.freeze
- JSON_ERROR_HEADERS_REQUEST =
Headers for error responses that denote an invalid or an unsatisfiable request
DEFAULT_HEADERS.merge({ 'Content-Type' => 'application/json', 'Cache-Control' => 'public, max-age=600' }).freeze
- JSON_ERROR_HEADERS_INTERMITTENT =
Headers for error responses that denote an intermittent error (that permit retries)
DEFAULT_HEADERS.merge({ 'Content-Type' => 'application/json', 'Cache-Control' => 'public, max-age=5' }).freeze
- IMAGE_CACHE_CONTROL =
Cache details: “public” of course. Add max-age so that there is some revalidation after a time (otherwise some proxies treat it as “must-revalidate” always), and “no-transform” so that various deflate schemes are not applied to it (does happen with Rack::Cache and leads Chrome to throw up on content decoding for example).
"public, no-transform, max-age=%d"- 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 psd tif)
- EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS =
How long should we wait when fetching the image from the external host
4
Instance Method Summary collapse
-
#apply_pipeline(source_file_path, pipeline, source_file_type, render_to_path) ⇒ void
Applies the given Pipeline to the image, and writes the render to the given path.
- #bail(status, *errors_array) ⇒ Object
-
#call(env) ⇒ Array
The main entry point for the Rack app.
-
#detect_file_type(tempfile) ⇒ MagicBytes::FileType
Detects the file type of the given File and returns a MagicBytes::FileType object that contains the extension and the MIME type.
-
#extract_params_from_request(rack_request) ⇒ Hash
Extracts the image params from the Rack::Request.
-
#handle_generic_error(exception) ⇒ void
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) ⇒ Array
Hadles the Rack request.
-
#handle_request_error(exception) ⇒ void
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_rack_response(render_destination_file, render_file_type, etag) ⇒ Object
Returns a Rack response triplet.
-
#parse_env_into_request(rack_env) ⇒ #get?, #params
Parses the Rack environment into a Rack::Reqest.
-
#permanent_failures ⇒ Object
Lists exceptions that should lead to the request being flagged as invalid (4xx as opposed to 5xx for a generic server error).
-
#process_image_request(image_request) ⇒ Array<File, MagicBytes::FileType, String]
Processes the ImageRequest object created from the request parameters, and returns a triplet of the File object containing the rendered image, the MagicBytes::FileType object of the render, and the cache ETag value representing the processing pipeline.
-
#raise_exception_or_error_response(exception, status_code) ⇒ Object
Depending on ‘raise_exceptions?` will either raise the passed Exception, or force the application to return the error in the Rack response.
-
#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) ⇒ void
Is meant to be overridden by subclasses, will be called at the start of each request to set up the error handling library (Appsignal, Honeybadger, Sentry…).
-
#source_file_type_permitted?(magic_bytes_file_info) ⇒ Boolean
Tells whether the given file type may be loaded into the image processor.
Instance Method Details
#apply_pipeline(source_file_path, pipeline, source_file_type, render_to_path) ⇒ void
This method returns an undefined value.
Applies the given Pipeline to the image, and writes the render to the given path.
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 |
# File 'lib/image_vise/render_engine.rb', line 278 def apply_pipeline(source_file_path, pipeline, source_file_type, render_to_path) render_file_type = source_file_type # Load the first frame of the animated GIF _or_ the blended compatibility layer from Photoshop image_list = Magick::Image.read(source_file_path) magick_image = image_list.first # Picks up the "precomp" PSD layer in compatibility mode, or the first frame of a GIF # If any operators want to stash some data for downstream use we use this Hash = {} # Apply the pipeline (all the image operators) pipeline.apply!(magick_image, ) # Write out the file honoring the possible injected metadata. One of the metadata # elements (that an operator might want to alter) is the :writer, we forcibly #fetch # it so that we get a KeyError if some operator has deleted it without providing a replacement. # If no operators touched the writer we are going to use the automatic format selection writer = .fetch(:writer, ImageVise::AutoWriter.new) writer.write_image!(magick_image, , render_to_path) ensure # destroy all the loaded images explicitly (image_list || []).map {|img| ImageVise.destroy(img) } end |
#bail(status, *errors_array) ⇒ Object
41 42 43 44 45 46 47 48 49 |
# File 'lib/image_vise/render_engine.rb', line 41 def bail(status, *errors_array) headers = if (300...500).cover?(status) JSON_ERROR_HEADERS_REQUEST.dup else JSON_ERROR_HEADERS_INTERMITTENT.dup end response = [status.to_i, headers, [JSON.pretty_generate({errors: errors_array})]] throw :__bail, response end |
#call(env) ⇒ Array
The main entry point for the Rack app. Wraps a call to #handle_request in a ‘catch{}` block so that any method can abort the request by calling #bail
56 57 58 |
# File 'lib/image_vise/render_engine.rb', line 56 def call(env) catch(:__bail) { handle_request(env) } end |
#detect_file_type(tempfile) ⇒ MagicBytes::FileType
Detects the file type of the given File and returns a MagicBytes::FileType object that contains the extension and the MIME type.
210 211 212 213 |
# File 'lib/image_vise/render_engine.rb', line 210 def detect_file_type(tempfile) tempfile.rewind MagicBytes.read_and_detect(tempfile).tap { tempfile.rewind } end |
#extract_params_from_request(rack_request) ⇒ Hash
Extracts the image params from the Rack::Request
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/image_vise/render_engine.rb', line 108 def extract_params_from_request(rack_request) # Prevent cache bypass DOS attacks by only permitting :sig and :q bail(400, 'Query strings are not supported') if rack_request.params.any? # Take the last two path components of the request URI. # The second-to-last is the Base64-encoded image request, the last is the signature. # Slashes within the image request are masked out already, no need to worry about them. # Parameters are passed in the path so that ImageVise integrates easier with CDNs and so that # it becomes harder to blow the cache by appending spurious query string parameters and/or # reordering query string parameters at will. *, q_from_path, sig_from_path = rack_request.path_info.split('/') # Raise if any of them are empty or blank nothing_recovered = [q_from_path, sig_from_path].all?{|v| v.nil? || v.empty? } bail(400, 'Need 2 usable path components') if nothing_recovered {q: q_from_path, sig: sig_from_path} end |
#handle_generic_error(exception) ⇒ void
This method returns an undefined value.
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). The method should not re-raise the exception.
260 261 |
# File 'lib/image_vise/render_engine.rb', line 260 def handle_generic_error(exception) end |
#handle_request(env) ⇒ Array
Hadles the Rack request. If one of the steps calls #bail the ‘:__bail` symbol will be thrown and the execution will abort. Any errors will cause either an error response in JSON format or an Exception will be raised (depending on the return value of `raise_exceptions?`)
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 |
# File 'lib/image_vise/render_engine.rb', line 66 def handle_request(env) setup_error_handling(env) # 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 = parse_env_into_request(env) bail(405, 'Only GET supported') unless req.get? params = extract_params_from_request(req) image_request = ImageVise::ImageRequest.from_params(qs_params: params, secrets: ImageVise.secret_keys) render_destination_file, render_file_type, etag = process_image_request(image_request) image_rack_response(render_destination_file, render_file_type, etag) rescue *permanent_failures => e handle_request_error(e) http_status_code = e.respond_to?(:http_status) ? e.http_status : 400 raise_exception_or_error_response(e, http_status_code) rescue Exception => e if http_status_code = (e.respond_to?(:http_status) && e.http_status) handle_request_error(e) raise_exception_or_error_response(e, http_status_code) else handle_generic_error(e) raise_exception_or_error_response(e, 500) end end |
#handle_request_error(exception) ⇒ void
This method returns an undefined value.
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. The method should not re-raise the exception.
250 251 |
# File 'lib/image_vise/render_engine.rb', line 250 def handle_request_error(exception) end |
#image_rack_response(render_destination_file, render_file_type, etag) ⇒ Object
Returns a Rack response triplet. Accepts the return value of ‘process_image_request` unsplatted, and returns a triplet that can be returned as a Rack response. The Rack response will contain an iterable body object that is designed to automatically delete the Tempfile it wraps on close. Sets the cache lifetime to either the default value of 2592000 or the value the user selected using add_custom_cache_max_length.
178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/image_vise/render_engine.rb', line 178 def image_rack_response(render_destination_file, render_file_type, etag) response_headers = DEFAULT_HEADERS.merge({ 'Content-Type' => render_file_type.mime, 'Content-Length' => '%d' % render_destination_file.size, 'Cache-Control' => IMAGE_CACHE_CONTROL % ImageVise.cache_lifetime_seconds, '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)] end |
#parse_env_into_request(rack_env) ⇒ #get?, #params
Parses the Rack environment into a Rack::Reqest. The following methods are going to be called on it: ‘#get?` and `#params`. You can use this method to override path-to-parameter translation for example.
100 101 102 |
# File 'lib/image_vise/render_engine.rb', line 100 def parse_env_into_request(rack_env) Rack::Request.new(rack_env) end |
#permanent_failures ⇒ Object
Lists exceptions that should lead to the request being flagged as invalid (4xx as opposed to 5xx for a generic server error). Decent clients should not retry those requests.
226 227 228 229 230 231 232 |
# File 'lib/image_vise/render_engine.rb', line 226 def permanent_failures [ Magick::ImageMagickError, UnsupportedInputFormat, ImageVise::ImageRequest::InvalidRequest ] end |
#process_image_request(image_request) ⇒ Array<File, MagicBytes::FileType, String]
Processes the ImageRequest object created from the request parameters, and returns a triplet of the File object containing the rendered image, the MagicBytes::FileType object of the render, and the cache ETag value representing the processing pipeline
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/image_vise/render_engine.rb', line 134 def process_image_request(image_request) # 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/copy the original into a Tempfile fetcher = ImageVise.fetcher_for(source_image_uri.scheme) source_file = fetcher.fetch_uri_to_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 render_destination_file = Tempfile.new('imagevise-render').tap{|f| f.binmode } # Do the actual imaging stuff apply_pipeline(source_file.path, pipeline, source_file_type, render_destination_file.path) # Catch this one early render_destination_file.rewind raise EmptyRender, "The rendered image was empty" if render_destination_file.size.zero? render_file_type = detect_file_type(render_destination_file) [render_destination_file, render_file_type, etag] ensure ImageVise.close_and_unlink(source_file) end |
#raise_exception_or_error_response(exception, status_code) ⇒ Object
Depending on ‘raise_exceptions?` will either raise the passed Exception, or force the application to return the error in the Rack response.
196 197 198 199 200 201 202 |
# File 'lib/image_vise/render_engine.rb', line 196 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.
267 268 269 |
# File 'lib/image_vise/render_engine.rb', line 267 def raise_exceptions? false end |
#setup_error_handling(rack_env) ⇒ void
This method returns an undefined value.
Is meant to be overridden by subclasses, will be called at the start of each request to set up the error handling library (Appsignal, Honeybadger, Sentry…)
240 241 |
# File 'lib/image_vise/render_engine.rb', line 240 def setup_error_handling(rack_env) end |
#source_file_type_permitted?(magic_bytes_file_info) ⇒ Boolean
Tells whether the given file type may be loaded into the image processor.
219 220 221 |
# File 'lib/image_vise/render_engine.rb', line 219 def source_file_type_permitted?(magic_bytes_file_info) PERMITTED_SOURCE_FILE_EXTENSIONS.include?(magic_bytes_file_info.ext) end |