module Shrimp class Middleware def initialize(app, options = { }, conditions = { }) @app = app @options = options @conditions = conditions @options[:polling_interval] ||= 1 @options[:polling_offset] ||= 1 @options[:cache_ttl] ||= 1 @options[:request_timeout] ||= @options[:polling_interval] * 10 end def call(env) @request = Rack::Request.new(env) if render_as_pdf? #&& headers['Content-Type'] =~ /text\/html|application\/xhtml\+xml/ if already_rendered? && (up_to_date?(@options[:cache_ttl]) || @options[:cache_ttl] == 0) if File.size(render_to) == 0 File.delete(render_to) remove_rendering_flag return error_response end return ready_response if env['HTTP_X_REQUESTED_WITH'] file = File.open(render_to, "rb") body = file.read file.close File.delete(render_to) if @options[:cache_ttl] == 0 remove_rendering_flag response = [body] headers = { } headers["Content-Length"] = (body.respond_to?(:bytesize) ? body.bytesize : body.size).to_s headers["Content-Type"] = "application/pdf" [200, headers, response] else if rendering_in_progress? if rendering_timed_out? remove_rendering_flag error_response else reload_response(@options[:polling_interval]) end else File.delete(render_to) if already_rendered? set_rendering_flag fire_phantom reload_response(@options[:polling_offset]) end end else @app.call(env) end end private # Private: start phantom rendering in a separate process def fire_phantom Process::detach fork { Phantom.new(@request.url.sub(%r{\.pdf$}, ''), @options, @request.cookies).to_pdf(render_to) } end def render_to file_name = Digest::MD5.hexdigest(@request.path) + ".pdf" file_path = @options[:out_path] "#{file_path}/#{file_name}" end def already_rendered? File.exists?(render_to) end def up_to_date?(ttl = 30) (Time.now - File.new(render_to).mtime) <= ttl end def remove_rendering_flag @request.session["phantom-rendering"] ||={ } @request.session["phantom-rendering"].delete(render_to) end def set_rendering_flag @request.session["phantom-rendering"] ||={ } @request.session["phantom-rendering"][render_to] = Time.now end def rendering_timed_out? Time.now - @request.session["phantom-rendering"][render_to] > @options[:request_timeout] end def rendering_in_progress? @request.session["phantom-rendering"]||={ } @request.session["phantom-rendering"][render_to] end def render_as_pdf? request_path_is_pdf = !!@request.path.match(%r{\.pdf$}) if request_path_is_pdf && @conditions[:only] rules = [@conditions[:only]].flatten rules.any? do |pattern| if pattern.is_a?(Regexp) @request.path =~ pattern else @request.path[0, pattern.length] == pattern end end elsif request_path_is_pdf && @conditions[:except] rules = [@conditions[:except]].flatten rules.map do |pattern| if pattern.is_a?(Regexp) return false if @request.path =~ pattern else return false if @request.path[0, pattern.length] == pattern end end return true else request_path_is_pdf end end def concat(accepts, type) (accepts || '').split(',').unshift(type).compact.join(',') end def reload_response(interval=1) body = <<-HTML.gsub(/[ \n]+/, ' ').strip

Preparing pdf...

HTML headers = { } headers["Content-Length"] = body.size.to_s headers["Content-Type"] = "text/html" headers["Retry-After"] = interval.to_s [503, headers, [body]] end def ready_response body = <<-HTML.gsub(/[ \n]+/, ' ').strip PDF ready here HTML headers = { } headers["Content-Length"] = body.size.to_s headers["Content-Type"] = "text/html" [200, headers, [body]] end def error_response body = <<-HTML.gsub(/[ \n]+/, ' ').strip

Sorry request timed out...

HTML headers = { } headers["Content-Length"] = body.size.to_s headers["Content-Type"] = "text/html" [504, headers, [body]] end end end