#!/usr/bin/ruby require 'ip' require 'pathname' require 'cli' options = CLI.new do description 'HTTP based image storage and thumbnailer' switch :no_bind, :description => "Do not bind to TCP socket - useful with -s fastcgi option" switch :no_logging, :description => "Disable logging" switch :debug, :description => "Enable debugging" switch :no_uri_encode, :description => "Disable output URI encoding" option :bind, :short => :b, :default => IP.new('127.0.0.1'), :cast => IP, :description => "HTTP server bind address - use 0.0.0.0 to bind to all interfaces" option :port, :short => :p, :default => 3000, :cast => Integer, :description => "HTTP server TCP port" option :server, :short => :s, :default => 'mongrel', :description => "Rack server handler like thin, mongrel, webrick, fastcgi etc." option :upload_retry_times, :default => 3, :cast => Integer, :description => "Number of times file upload will be retried in case of an error" option :upload_retry_delay, :default => 0.0, :cast => Float, :description => "Time to sleep between retries" argument :config, :cast => Pathname, :description => "Configuration file path" end.parse! require 'sinatra/base' require 'httpthumbnailer-client' require 'digest/sha2' $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'httpimagestore/configuration' require 'httpimagestore/image_path' require 'httpimagestore/s3_service' sinatra = Sinatra.new unless options.no_bind sinatra.set :port, options.port sinatra.set :bind, options.bind.to_s else sinatra.set :port, nil sinatra.set :bind, nil end sinatra.set :environment, 'production' sinatra.set :server, options.server sinatra.set :lock, true sinatra.set :logging, (not options.no_logging) sinatra.set :debug, options.debug sinatra.set :uri_encode, (not options.no_uri_encode) sinatra.set :upload_retry_times, options.upload_retry_times sinatra.set :upload_retry_delay, options.upload_retry_delay Configuration.from_file(options.config).put(sinatra) class ThumbnailingError < RuntimeError def initialize(thumbnail_class, remote_error) @remote_error = remote_error super "Thumbnailing for class '#{thumbnail_class.name}' failed: #{remote_error.message}" end attr_reader :remote_error end sinatra.before do logger.level = Logger::DEBUG if settings.logging and settings.debug # singletons $s3 ||= S3Service.new(settings.s3_key_id, settings.s3_key_secret, settings.s3_bucket, :logger => logger, :upload_retry_times => settings.upload_retry_times, :upload_retry_delay => settings.upload_retry_delay) $thumbnailer ||= HTTPThumbnailerClient.new(settings.thumbnailer_url) end sinatra.helpers do def hatl_response(status, msg) headers "Content-Type" => "text/plain" body msg.gsub("\n", "\r\n") + "\r\n" halt status end def halt_exception(status, exception) hatl_response(status, "Error: #{exception.class.name}: #{exception}") end def digest(data) Digest::SHA2.new.update(data).to_s[0,16] end end sinatra.get '/' do "hello" end sinatra.put %r{/thumbnail/([^/]*)/?(.*)} do |thumbnail_classes, image_path| thumbnail_classes = thumbnail_classes.split(',').map{|thumbnail_class| settings.thumbnail_classes[thumbnail_class]} image = request.body.read image_hash = digest(image) unless image_path.empty? image_path = ImagePath::Custom.new(image_hash, image_path) else image_path = ImagePath::Auto.new(image_hash) end thumbnails = $thumbnailer.thumbnail(image) do thumbnail_classes.each do |thumbnail_class| thumbnail thumbnail_class.method, thumbnail_class.width, thumbnail_class.height, thumbnail_class.format, thumbnail_class.options end end # check for errors thumbnails.zip(thumbnail_classes).each do |thumbnail, thumbnail_class| raise ThumbnailingError.new(thumbnail_class, thumbnail) if thumbnail.kind_of? HTTPThumbnailerClient::ThumbnailingError end urls = [] # store all images urls << $s3.put_image(image_path.original_image(thumbnails.input_mime_type), thumbnails.input_mime_type, image) thumbnails.zip(thumbnail_classes).each do |thumbnail, thumbnail_class| urls << $s3.put_image(image_path.thumbnail_image(thumbnail.mime_type, thumbnail_class.name), thumbnail.mime_type, thumbnail.data) end status 200 headers "Content-Type" => "text/uri-list" body urls.map{|u| settings.uri_encode ? URI.encode(u) : u}.map{|u| u + "\r\n"}.join end sinatra.error HTTPThumbnailerClient::UnsupportedMediaTypeError do halt_exception(415, env['sinatra.error']) end sinatra.error HTTPThumbnailerClient::ImageTooLargeError do halt_exception(413, env['sinatra.error']) end # problem with singe thumbnail sinatra.error ThumbnailingError do e = env['sinatra.error'] case e.remote_error.type when "Thumbnailer::ImageTooLargeError" halt_exception(413, e) else halt_exception(500, e) end end sinatra.error Configuration::ThumbnailClassDoesNotExistError do halt_exception(404, env['sinatra.error']) end sinatra.error Sinatra::NotFound do hatl_response(404, "Resource '#{request.path_info}' not found") end sinatra.error do halt_exception(500, env['sinatra.error']) end sinatra.run!