#!/usr/bin/ruby require 'cli' options = CLI.new do description 'HTTP based image storage and thumbnailer' option :bind, :short => :b, :default => 'localhost', :description => "HTTP server bind address" 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." argument :config, :cast => Pathname, :description => "Configuration file path" end.parse! require 'sinatra/base' require 'pathname' require 's3' require 'httpthumbnailer-client' require 'digest/sha2' $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'httpimagestore/configuration' require 'httpimagestore/pathname' sinatra = Sinatra.new sinatra.set :port, options.port sinatra.set :bind, options.bind sinatra.set :environment, 'production' sinatra.set :server, options.server sinatra.set :lock, true sinatra.set :logging, true Configuration.from_file(options.config).put(sinatra) class S3Put def initialize(key_id, key_secret, bucket, options = {}) @options = options @logger = (options[:logger] or Logger.new('/dev/null')) @s3 = S3::Service.new(:access_key_id => key_id, :secret_access_key => key_secret) @logger.info "Getting bucket: #{bucket}" @bucket = @s3.buckets.find(bucket) or fail "no buckte '#{bucket}' found" end def put_image(image_path, content_type, data) @logger.info "Putting image in bucket '#{@bucket.name}': #{image_path}" file = @bucket.objects.build(image_path) file.content_type = content_type file.content = data file.save "http://#{@bucket.name}.s3.amazonaws.com/#{image_path}" end end sinatra.before do # singletons $s3 ||= S3Put.new(settings.s3_key_id, settings.s3_key_secret, settings.s3_bucket, :logger => logger) $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 class BadRequestError < RuntimeError end 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 end sinatra.get '/' do "hello" end sinatra.put %r{/thumbnail/([^/]*)/(.*)} do |classes, image_path| raise BadRequestError, "Path is empty" if image_path.empty? image_path = Pathname.new(image_path) classes = classes.split(',').map{|tc| settings.thumbnail_classes[tc]} image = request.body.read image_hash = digest(image) thumbs = $thumbnailer.thumbnail(image) do classes.each do |tc| thumbnail tc.method, tc.width, tc.height, tc.format, tc.options end end # check for errors thumbs.zip(classes).each do |thumb, thumbnail_class| raise ThumbnailingError.new(thumbnail_class, thumb) if thumb.kind_of? HTTPThumbnailerClient::ThumbnailingError end urls = [] # store all images urls << $s3.put_image(image_path.original_image(image_hash).to_s, response.headers['Content-Type'], image) thumbs.zip(classes).each do |thumb, thumbnail_class| urls << $s3.put_image(image_path.thumbnail_image(image_hash, thumbnail_class.name).to_s, thumb.mime_type, thumb.data) end status 200 headers "Content-Type" => "text/uri-list" body urls.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 BadRequestError do halt_exception(400, env['sinatra.error']) 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!