Skip to content
This repository has been archived by the owner on Aug 21, 2019. It is now read-only.

better error handling #39

Merged
merged 14 commits into from
Dec 22, 2014
27 changes: 14 additions & 13 deletions lib/mustachio.rb
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
require 'magickly'
require 'fastimage'
require File.join(File.dirname(__FILE__), 'mustachio', 'factories')
require File.join(File.dirname(__FILE__), 'mustachio', 'rekognition')
require File.join(File.dirname(__FILE__), 'mustachio', 'shortcuts')


module Mustachio
# FACE_POS_ATTRS = ['center', 'eye_left', 'eye_right', 'mouth_left', 'mouth_center', 'mouth_right', 'nose']
REQUIRED_FACE_ATTRS = %w(mouth_left mouth_right nose)
FACE_SPAN_SCALE = 2.0

class << self

def mustaches
@@mustaches
end

def setup
staches = YAML.load(File.read(File.join(File.dirname(__FILE__), '..', 'config', 'staches.yml')))
staches.map! do |stache|
stache['vert_offset'] ||= 0
stache['mouth_overlap'] ||= 0

stache['file_path'] = File.expand_path(File.join(File.dirname(__FILE__), 'mustachio', 'public', 'images', 'staches', stache['filename']))
unless stache['width'] && stache['height']
stache['width'], stache['height'] = FastImage.size(File.new(stache['file_path']))
Expand All @@ -45,7 +46,7 @@ def setup_rekognition
Mustachio::Rekognition.face_detection file
end
end

def face_data(file_or_job)
file = case file_or_job
when Dragonfly::Job
Expand All @@ -64,7 +65,7 @@ def face_data(file_or_job)

@@face_detection_proc.call file
end

def face_data_as_px(file_or_job, width, height)
faces = self.face_data file_or_job

Expand All @@ -77,30 +78,30 @@ def face_data_as_px(file_or_job, width, height)
end
new_faces
end

# TODO : Fix to work with pluggable face detection
# def face_span(file_or_job)
# face_data = self.face_data_as_px(file_or_job)
# faces = face_data['tags']

# left_face, right_face = faces.minmax_by{|face| face['center']['x'] }
# top_face, bottom_face = faces.minmax_by{|face| face['center']['y'] }

# top = top_face['eye_left']['y']
# bottom = bottom_face['mouth_center']['y']
# right = right_face['eye_right']['x']
# left = left_face['eye_left']['x']
# width = right - left
# height = bottom - top

# # compute adjusted values for padding around face span
# adj_width = width * FACE_SPAN_SCALE
# adj_height = height * FACE_SPAN_SCALE
# adj_top = top - ((adj_height - height) / 2.0)
# adj_bottom = bottom + ((adj_height - height) / 2.0)
# adj_right = right + ((adj_width - width) / 2.0)
# adj_left = left - ((adj_width - width) / 2.0)

# {
# :top => adj_top,
# :bottom => adj_bottom,
Expand All @@ -113,9 +114,9 @@ def face_data_as_px(file_or_job, width, height)
# }
# end


end


self.setup
end
61 changes: 48 additions & 13 deletions lib/mustachio/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,81 @@
module Mustachio
class App < Sinatra::Base
DEMO_IMAGE = 'http://www.librarising.com/astrology/celebs/images2/QR/queenelizabethii.jpg'

set :static, true

configure :production do
require 'newrelic_rpm' if ENV['NEW_RELIC_ID']
end

before do


def redirect_to_canonical_host
app_host = ENV['MUSTACHIO_APP_DOMAIN']
if app_host && request.host != app_host
request_host_with_port = request.env['HTTP_HOST']
redirect request.url.sub(request_host_with_port, app_host), 301
end
end



def valid_url?(url)
url =~ /\A#{URI::regexp(['http', 'https'])}\z/
end

def serve_stache(src, stache_arg)
if valid_url?(src)
begin
image = Magickly.process_src(src, mustachify: stache_arg)
image.to_response(env)
rescue ArgumentError => e
if e.message == 'uncaught throw :unable_to_handle'
status 415
"Unsupported image format."
else
raise
end
rescue Dragonfly::DataStorage::DataNotFound, SocketError
status 502
"Image not found."
rescue Mustachio::Rekognition::Error => e
status 502
e.message
rescue Timeout::Error
status 504
"Image download timed out."
end
else
status 415
"Invalid src parameter."
end
end


before do
redirect_to_canonical_host
end

get %r{^/(\d+|rand)?$} do |stache_num|
src = params[:src]
if src
# use the specified stache, otherwise fall back to random
image = Magickly.process_src params[:src], :mustachify => (stache_num || true)
image.to_response(env)
stache_arg = stache_num || true
serve_stache(src, stache_arg)
else
@stache_num = stache_num
@site = Addressable::URI.parse(request.url).site
haml :index
end
end

get '/gallery' do
haml :gallery
end

get '/test' do
haml :test
end

get '/face_api_dev_challenge' do
haml :face_api_dev_challenge
end

end
end
25 changes: 16 additions & 9 deletions lib/mustachio/rekognition.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
module Mustachio
class Rekognition
class << self
class Error < StandardError; end

class << self
REKOGNITION_KEY = ENV['MUSTACHIO_REKOGNITION_KEY'] || raise('please set MUSTACHIO_REKOGNITION_KEY')
REKOGNITION_SECRET = ENV['MUSTACHIO_REKOGNITION_SECRET'] || raise('please set MUSTACHIO_REKOGNITION_SECRET')

# return tuple [rekognition_json, rekognition_width, rekognition_height]
def data file
json = self.json file
width, height = self.dims file
[json, width, height]
end

def json file, jobs = 'face'
def get_response(file, jobs)
conn = Faraday.new :url => 'https://rekognition.com' do |faraday|
faraday.request :multipart
faraday.request :url_encoded
Expand All @@ -28,8 +23,11 @@ def json file, jobs = 'face'
:user_id => ''
}

response = conn.post '/func/api/', payload
conn.post('/func/api/', payload)
end

def json file, jobs = 'face'
response = self.get_response(file, jobs)
JSON.parse response.body
end

Expand All @@ -41,8 +39,17 @@ def content_type file
`file -b --mime #{file.path}`.strip.split(/[:;]\s+/)[0]
end

def validate_response(json)
unless json['face_detection']
usage = json['usage'] || {}
msg = usage['status'] || 'failure.'
raise Error.new("Rekognition API: #{msg}")
end
end

def face_detection file
json = self.json file, 'face_part'
self.validate_response(json)
width, height = self.dims file

json['face_detection'].map do |entry|
Expand Down
15 changes: 11 additions & 4 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
ENV['RACK_ENV'] ||= 'test'
ENV['MUSTACHIO_REKOGNITION_KEY'] = '123'
ENV['MUSTACHIO_REKOGNITION_SECRET'] = '456'

require 'rubygems'
require 'bundler'
Expand Down Expand Up @@ -35,16 +37,21 @@
config.expect_with :rspec do |c|
c.syntax = :expect
end
end


ENV['MUSTACHIO_REKOGNITION_KEY'] = '123'
ENV['MUSTACHIO_REKOGNITION_SECRET'] = '456'
def image_path(filename)
File.join(File.dirname(__FILE__), 'support', filename)
end

def image_file(filename)
path = image_path(filename)
File.new(path)
end

def get_photo(filename='dubya.jpeg')
image_url = "http://www.foo.com/#{filename}"
image_path = File.join(File.dirname(__FILE__), 'support', filename)
stub_request(:get, image_url).to_return(:body => File.new(image_path))
stub_request(:get, image_url).to_return(body: image_file(filename))

Magickly.dragonfly.fetch(image_url)
end
Expand Down
Binary file added spec/support/handbag.webp
Binary file not shown.
56 changes: 56 additions & 0 deletions spec/unit/app_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
require 'spec_helper'
require 'rack/test'

describe Mustachio::App do
include Rack::Test::Methods

def app
subject
end

describe "GET /" do
it "shows the homepage" do
get '/'
expect(last_response.status).to eq(200)
expect(last_response.body).to include('Created by')
end
end

describe "GET /?src=..." do
it "handles a Rekognition API failure" do
stub_request(:get, 'http://example.com/dubya.jpeg').to_return(body: image_file('dubya.jpeg'))
expect(Mustachio::Rekognition).to receive(:face_detection).and_raise(Mustachio::Rekognition::Error)
get '/?src=http://example.com/dubya.jpeg'
expect(last_response.status).to eq(502)
end

it "handles a missing image file" do
stub_request(:get, 'http://existentsite.com/missing.png').to_return(status: 404)
get '/?src=http://existentsite.com/missing.png'
expect(last_response.status).to eq(502)
end

it "handles a missing image host" do
stub_request(:get, 'http://nonexistentsite.com/foo.png').to_raise(SocketError)
get '/?src=http://nonexistentsite.com/foo.png'
expect(last_response.status).to eq(502)
end

it "handles when the image format can't be processed" do
stub_request(:get, 'http://example.com/handbag.webp').to_return(body: image_file('handbag.webp'))
get '/?src=http://example.com/handbag.webp'
expect(last_response.status).to eq(415)
end

it "handles when the image download times out" do
stub_request(:get, 'http://slowsite.com/foo.png').to_timeout
get '/?src=http://slowsite.com/foo.png'
expect(last_response.status).to eq(504)
end

it "handles invalid URLs" do
get '/?src=foo'
expect(last_response.status).to eq(415)
end
end
end
21 changes: 21 additions & 0 deletions spec/unit/rekognition_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'spec_helper'

describe Mustachio::Rekognition do
describe '.face_detection' do
it "handles API errors" do
# they still return status 200 :(
stub_request(:post, 'https://rekognition.com/func/api/').to_return(body: {
url: 'http://example.com/dubya.jpeg',
usage: {
quota: '0',
status: "ERROR! Image is corrupted!",
api_id: '123'
}
}.to_json)

expect {
Mustachio::Rekognition.face_detection(image_file('dubya.jpeg'))
}.to raise_error(Mustachio::Rekognition::Error)
end
end
end