├── log └── .keep ├── public └── .keep ├── tmp └── .keep ├── .gitignore ├── .gems ├── test ├── assets │ ├── fish.png │ └── fish.svg ├── helper.rb ├── test_svg_generation.rb ├── test_image_variant_generator.rb └── test_remote_proxy.rb ├── Capfile ├── lib ├── imagery │ ├── vendor │ │ └── SyslogLogger-1.4.0 │ │ │ ├── Manifest.txt │ │ │ ├── History.txt │ │ │ ├── README.txt │ │ │ ├── Rakefile │ │ │ ├── lib │ │ │ └── syslog_logger.rb │ │ │ └── test │ │ │ └── test_syslog_logger.rb │ ├── middleware │ │ ├── server_name.rb │ │ ├── favicon_filter.rb │ │ ├── cache_purge.rb │ │ ├── remote_proxy.rb │ │ ├── logged_request.rb │ │ └── accel_redirect.rb │ ├── transformations.rb │ ├── server.rb │ ├── send_file.rb │ ├── transformations │ │ ├── borders.rb │ │ ├── transform.rb │ │ └── sizes.rb │ ├── svg_generator.rb │ ├── image_variant_generator.rb │ ├── image.rb │ └── logger_ext.rb └── imagery.rb ├── Rakefile ├── config ├── env.rb └── deploy.rb └── config.ru /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | *.log -------------------------------------------------------------------------------- /.gems: -------------------------------------------------------------------------------- 1 | patron 2 | rack-cache 3 | memcached 4 | RMagick 5 | -------------------------------------------------------------------------------- /test/assets/fish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobi/imagery/HEAD/test/assets/fish.png -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | 2 | load 'deploy' if respond_to?(:namespace) # cap2 differentiator 3 | load 'config/deploy' 4 | 5 | -------------------------------------------------------------------------------- /lib/imagery/vendor/SyslogLogger-1.4.0/Manifest.txt: -------------------------------------------------------------------------------- 1 | History.txt 2 | Manifest.txt 3 | README.txt 4 | Rakefile 5 | lib/analyzer_tools/syslog_logger.rb 6 | lib/syslog_logger.rb 7 | test/test_syslog_logger.rb 8 | -------------------------------------------------------------------------------- /lib/imagery/middleware/server_name.rb: -------------------------------------------------------------------------------- 1 | module Imagery 2 | class ServerName 3 | def initialize(app) 4 | @app = app 5 | end 6 | 7 | def call(env) 8 | hash = @app.call(env) 9 | hash[1]['Server'] = 'Shopify Imagery' 10 | hash 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/imagery/vendor/SyslogLogger-1.4.0/History.txt: -------------------------------------------------------------------------------- 1 | == 1.4.0 / 2007-05-08 2 | 3 | * Split from rails_analyzer_tools. 4 | * Added eh methods for compatibility with Logger. 5 | * Added syslog-ng instructions. Patch by Tom Lianza. 6 | * Fixed require in documentation. Reported by Gianni Jacklone. 7 | 8 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..') 2 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '../lib') 3 | 4 | require 'rubygems' 5 | require 'test/unit' 6 | 7 | require 'fakeweb' 8 | require 'mocha' 9 | 10 | require 'rack' 11 | require 'imagery' 12 | require 'config/env' 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | namespace :gems do 2 | 3 | desc "Install all needed gems" 4 | task :install do 5 | File.read('.gems').each_line do |line| 6 | begin 7 | p line 8 | system("sudo gem install #{line.chomp}") 9 | rescue 10 | STDERR.puts "Could not install gem #{line.split.first}" 11 | end 12 | end 13 | end 14 | 15 | end -------------------------------------------------------------------------------- /lib/imagery/vendor/SyslogLogger-1.4.0/README.txt: -------------------------------------------------------------------------------- 1 | = SyslogLogger 2 | 3 | SyslogLogger is a Logger replacement that logs to syslog. It is almost 4 | drop-in with a few caveats. 5 | 6 | http://seattlerb.rubyforge.org/SyslogLogger 7 | 8 | http://rubyforge.org/projects/seattlerb 9 | 10 | == About 11 | 12 | See SyslogLogger 13 | 14 | == Install 15 | 16 | sudo gem install SyslogLogger 17 | 18 | -------------------------------------------------------------------------------- /lib/imagery/middleware/favicon_filter.rb: -------------------------------------------------------------------------------- 1 | module Imagery 2 | class FaviconFilter 3 | Empty = [200, {'Content-Type' => 'text/plain'}, ['']].freeze 4 | 5 | def initialize(app) 6 | @app = app 7 | end 8 | 9 | def call(env) 10 | if env['REQUEST_URI'] == '/favicon.ico' 11 | return Empty 12 | else 13 | @app.call(env) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/imagery.rb: -------------------------------------------------------------------------------- 1 | require 'imagery/send_file' 2 | require 'imagery/middleware/cache_purge' 3 | require 'imagery/middleware/logged_request' 4 | require 'imagery/middleware/remote_proxy' 5 | require 'imagery/middleware/server_name' 6 | require 'imagery/middleware/favicon_filter' 7 | require 'imagery/logger_ext' 8 | require 'imagery/transformations' 9 | require 'imagery/svg_generator' 10 | require 'imagery/image_variant_generator' 11 | require 'imagery/image' 12 | require 'imagery/server' 13 | -------------------------------------------------------------------------------- /lib/imagery/transformations.rb: -------------------------------------------------------------------------------- 1 | require 'RMagick' 2 | 3 | module Imagery 4 | module Transformations 5 | @transformations = Hash.new 6 | 7 | def self.list 8 | @transformations.keys 9 | end 10 | 11 | def self.[](name) 12 | @transformations[name.to_s] 13 | end 14 | 15 | def self.register(name, &block) 16 | @transformations[name.to_s] = Proc.new(&block) 17 | end 18 | end 19 | end 20 | 21 | 22 | 23 | Dir[ File.dirname(__FILE__) + '/transformations/*.rb'].each { |f| require f } 24 | -------------------------------------------------------------------------------- /lib/imagery/vendor/SyslogLogger-1.4.0/Rakefile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | require 'hoe' 4 | require './lib/syslog_logger.rb' 5 | 6 | Hoe.new('SyslogLogger', SyslogLogger::VERSION) do |p| 7 | p.rubyforge_name = 'seattlerb' 8 | p.author = 'Eric Hodel' 9 | p.email = 'drbrain@segment7.net' 10 | p.summary = p.paragraphs_of('README.txt', 1).first 11 | p.description = p.summary 12 | p.url = p.paragraphs_of('README.txt', 2).first 13 | p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n") 14 | end 15 | 16 | # vim: syntax=Ruby 17 | -------------------------------------------------------------------------------- /lib/imagery/middleware/cache_purge.rb: -------------------------------------------------------------------------------- 1 | module Imagery 2 | class CachePurge 3 | Success = [200, {'Content-Type' => 'text/plain'}, ['OK']] 4 | 5 | def initialize(app) 6 | @app = app 7 | end 8 | 9 | # PURGE /s/files/1/0001/4168/files/thumbs/pic_thumb.jpg?12428536032 HTTP/1.0 10 | 11 | def call(env) 12 | # Rack cache automatically invalidates resource if the verbs are not GET/POST so 13 | # we don't actually have to do anything. Simply don't delegate those to the backend 14 | if env['REQUEST_METHOD'] == 'PURGE' 15 | Success 16 | else 17 | @app.call(env) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/imagery/middleware/remote_proxy.rb: -------------------------------------------------------------------------------- 1 | module Imagery 2 | class RemoteProxy 3 | include SendFile 4 | 5 | def initialize(app) 6 | @app = app 7 | end 8 | 9 | def call(env) 10 | request = Rack::Request.new(env) 11 | 12 | requested_file = Image.new(env['imagery.origin_host'], env['PATH_INFO'] + (env['QUERY_STRING'].empty? ? '' : "?#{env['QUERY_STRING']}")) 13 | 14 | # If file exists we simply sent it to the client. 15 | if requested_file.found? 16 | Logger.current.info "Requested file exists upstream." 17 | 18 | send_file(requested_file) 19 | else 20 | @app.call(env) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/imagery/server.rb: -------------------------------------------------------------------------------- 1 | module Imagery 2 | class Server 3 | include SendFile 4 | 5 | NotFound = [404, {'Content-Type' => 'text/html'}, ['

File not Found

']].freeze 6 | 7 | def call(env) 8 | Logger.current.info 'Attempting to generate missing file...' 9 | 10 | [SvgGenerator, ImageVariantGenerator].each do |generator| 11 | if image = generator.from_url(env['imagery.origin_host'], env['PATH_INFO'] + (env['QUERY_STRING'].empty? ? '' : "?#{env['QUERY_STRING']}")) 12 | 13 | return send_file(image) 14 | end 15 | end 16 | 17 | Logger.current.info 'No generator available' 18 | 19 | NotFound 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/imagery/middleware/logged_request.rb: -------------------------------------------------------------------------------- 1 | module Imagery 2 | class LoggedRequest 3 | 4 | def initialize(app) 5 | @app = app 6 | end 7 | 8 | def call(env) 9 | 10 | request = Rack::Cache::Request.new(env) 11 | 12 | resp = nil 13 | 14 | Logger.current.buffer do 15 | 16 | Logger.current.info "#{request.request_method} #{request.path} [#{request.ip}]" 17 | 18 | secs = Benchmark.realtime do 19 | Logger.current.intend do 20 | resp = @app.call(env) 21 | end 22 | end 23 | 24 | Logger.current.info((resp[0] < 399 ? 'Success' : "Error [#{resp[0]}]") + " after %.3fs Cache: %s" % [secs, resp[1]['X-Rack-Cache']]) 25 | Logger.current.info '' 26 | 27 | end 28 | 29 | resp 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /config/env.rb: -------------------------------------------------------------------------------- 1 | # Image server configuration file 2 | RACK_ENV = ENV['RACK_ENV'] || 'development' 3 | 4 | $settings = if File.exist?('/etc/imagery/config.yml') 5 | YAML.load_file('/etc/imagery/config.yml') 6 | else 7 | {} 8 | end 9 | 10 | # Upstream Server where the assets live 11 | 12 | ORIGIN_SERVER = $settings['origin_server'] || 'shopify.s3.amazonaws.com' 13 | 14 | 15 | # Middleware configuration 16 | # recommended to be memcached for meta and disk for entities. 17 | 18 | require 'memcached' 19 | 20 | ENV['CACHE_LOCATION'] = '/mnt/data/cache/rack/body' 21 | ENV['META_STORE'] = 'memcache://127.0.0.1:11211/meta' 22 | ENV['ENTITY_STORE'] = "file:#{ENV['CACHE_LOCATION']}" 23 | 24 | 25 | # Logging 26 | if RACK_ENV == 'production' 27 | Logger.current = SyslogLogger.new('rack.imagery') 28 | else 29 | Logger.current = Logger.new(File.dirname(__FILE__) + "/../log/#{RACK_ENV}.log") 30 | end 31 | 32 | -------------------------------------------------------------------------------- /lib/imagery/middleware/accel_redirect.rb: -------------------------------------------------------------------------------- 1 | require 'rack/file' 2 | 3 | class File #:nodoc: 4 | alias :to_path :path 5 | end 6 | 7 | # To make this work you have to add: 8 | # 9 | # location /cache/ { 10 | # internal; 11 | # alias /mnt/data/cache/rack/body; 12 | # } 13 | # 14 | # to nginx config 15 | 16 | 17 | module Imagery 18 | class AccelRedirect 19 | F = ::File 20 | 21 | def initialize(app, variation=nil) 22 | @app = app 23 | end 24 | 25 | def call(env) 26 | status, headers, body = @app.call(env) 27 | if body.respond_to?(:to_path) 28 | 29 | path = body.to_path 30 | url = path.sub(/^#{ENV['CACHE_LOCATION']}/i, '/cache') 31 | 32 | Logger.current.info " => sending #{url} through nginx" 33 | 34 | headers['Content-Length'] = '0' 35 | headers['X-Accel-Redirect'] = url 36 | body = [] 37 | end 38 | [status, headers, body] 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/test_svg_generation.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'helper') 2 | 3 | class TestRemoteProxy < Test::Unit::TestCase 4 | StandardResponse = [200, {}, ['OK']] 5 | ExpectedResponse = [200, {"Cache-Control"=>"public, max-age=0", "Content-Type"=>"text/plain"}, ["Hello World!"]] 6 | 7 | def setup 8 | end 9 | 10 | def test_successfull_call 11 | Patron::Session.any_instance.expects(:get).with('/image.svg').returns( stub(:headers => {}, :body => File.read( File.dirname(__FILE__) + '/assets/fish.svg'), :status => 200)) 12 | 13 | assert Imagery::SvgGenerator.from_url('static.shopify.com', '/image.svg.png') 14 | end 15 | 16 | def test_wrong_filename 17 | 18 | assert_equal nil, Imagery::SvgGenerator.from_url('static.shopify.com', '/image.svg') 19 | assert_equal nil, Imagery::SvgGenerator.from_url('static.shopify.com', '/image.png') 20 | assert_equal nil, Imagery::SvgGenerator.from_url('static.shopify.com', '/image.svg.bmp') 21 | end 22 | 23 | end -------------------------------------------------------------------------------- /lib/imagery/send_file.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | module Imagery 4 | module SendFile 5 | CopyHeaders = ['Content-Type', 'Cache-Control', 'Last-Modified', 'ETag'] 6 | 7 | ContentTypes = { 8 | '.gif' => 'image/gif', 9 | '.jpg' => 'image/jpeg', 10 | '.jpeg' => 'image/jpeg', 11 | '.png' => 'image/png', 12 | '.bmp' => 'image/x-bitmap', 13 | '.svg' => 'image/svg+xml' 14 | } 15 | 16 | def send_file(file) 17 | headers = {'Content-Length' => file.content.length.to_s} 18 | 19 | if file.respond_to?(:headers) 20 | CopyHeaders.each do |key| 21 | headers[key] = file.headers[key] if file.headers.has_key?(key) 22 | end 23 | end 24 | 25 | headers['ETag'] ||= Digest::MD5.hexdigest(file.content) 26 | headers['Cache-Control'] ||= 'public, max-age=31557600' 27 | headers['Last-Modified'] ||= Time.new.httpdate 28 | 29 | [200, headers, [file.content]] 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/imagery/transformations/borders.rb: -------------------------------------------------------------------------------- 1 | # Creates a 2 | 3 | Imagery::Transformations.register :border do |image| 4 | # Add Polaroid border 5 | image.border!(5, 5, "white") 6 | end 7 | 8 | Imagery::Transformations.register :shadow do |image| 9 | shadow = image.flip 10 | shadow = shadow.colorize(1, 1, 1, "#ccc") 11 | shadow.background_color = "white" 12 | shadow.border!(10, 10, "white") 13 | shadow = shadow.blur_image(0, 7) 14 | 15 | x = (shadow.columns - image.columns) / 2 16 | y = (shadow.rows - image.rows) / 2 17 | 18 | ## Composite original image on top of shadow and save 19 | shadow.composite(image, x, y-3, Magick::OverCompositeOp) 20 | end 21 | 22 | Imagery::Transformations.register :polaroid do |image| 23 | image.border!(10, 10, "white") 24 | 25 | shadow = image.colorize(1, 1, 1, "#ccc") 26 | shadow.background_color = "white" 27 | shadow.border!(10, 10, "white") 28 | shadow = shadow.blur_image(0, 7) 29 | 30 | x = (shadow.columns - image.columns) / 2 31 | y = (shadow.rows - image.rows) / 2 32 | 33 | ## Composite original image on top of shadow and save 34 | shadow.composite(image, x, y-3, Magick::OverCompositeOp) 35 | end 36 | -------------------------------------------------------------------------------- /lib/imagery/transformations/transform.rb: -------------------------------------------------------------------------------- 1 | Imagery::Transformations.register :square do |image| 2 | min = [image.columns, image.rows].min 3 | image.crop_resized(min, min, Magick::CenterGravity) 4 | end 5 | 6 | Imagery::Transformations.register 'max-square' do |image| 7 | max = [image.columns, image.rows].max 8 | image.crop_resized(max, max, Magick::CenterGravity) 9 | end 10 | 11 | Imagery::Transformations.register 'pico-square' do |image| 12 | image.crop_resized(16,16, Magick::CenterGravity) 13 | end 14 | 15 | Imagery::Transformations.register 'icon-square' do |image| 16 | image.crop_resized(32,32, Magick::CenterGravity) 17 | end 18 | 19 | Imagery::Transformations.register 'thumb-square' do |image| 20 | image.crop_resized(50,50, Magick::CenterGravity) 21 | end 22 | 23 | Imagery::Transformations.register 'medium-square' do |image| 24 | image.crop_resized(240,240, Magick::CenterGravity) 25 | end 26 | 27 | Imagery::Transformations.register 'small-square' do |image| 28 | image.crop_resized(100,100, Magick::CenterGravity) 29 | end 30 | 31 | Imagery::Transformations.register 'large-square' do |image| 32 | image.crop_resized(480,480, Magick::CenterGravity) 33 | end 34 | -------------------------------------------------------------------------------- /lib/imagery/transformations/sizes.rb: -------------------------------------------------------------------------------- 1 | Imagery::Transformations.register :pico do |image| 2 | image.change_geometry("16x16>") { |x, y, image| image.resize!(x,y) } 3 | end 4 | 5 | Imagery::Transformations.register :icon do |image| 6 | image.change_geometry("32x32>") { |x, y, image| image.resize!(x,y) } 7 | end 8 | 9 | Imagery::Transformations.register :thumb do |image| 10 | image.change_geometry("50x50>") { |x, y, image| image.resize!(x,y) } 11 | end 12 | 13 | Imagery::Transformations.register :small do |image| 14 | image.change_geometry("100x100>") { |x, y, image| image.resize!(x,y) } 15 | end 16 | 17 | Imagery::Transformations.register :compact do |image| 18 | image.change_geometry("160x160>") { |x, y, image| image.resize!(x,y) } 19 | end 20 | 21 | Imagery::Transformations.register :medium do |image| 22 | image.change_geometry("240x240>") { |x, y, image| image.resize!(x,y) } 23 | end 24 | 25 | Imagery::Transformations.register :large do |image| 26 | image.change_geometry("480x480>") { |x, y, image| image.resize!(x,y) } 27 | end 28 | 29 | Imagery::Transformations.register :grande do |image| 30 | image.change_geometry("600x600>") { |x, y, image| image.resize!(x,y) } 31 | end 32 | -------------------------------------------------------------------------------- /lib/imagery/svg_generator.rb: -------------------------------------------------------------------------------- 1 | require 'RMagick' 2 | require 'fileutils' 3 | require 'net/http' 4 | 5 | module Imagery 6 | # http://localhost:9292/s/files/1/0001/8392/assets/fish.svg 7 | # 8 | class SvgGenerator 9 | SvgFileTest = /\.svg\.png/i 10 | 11 | def self.from_url(server, path) 12 | return nil unless path =~ SvgFileTest 13 | 14 | file = Image.new(server, original_path_for(path) ) 15 | if file.found? 16 | file.headers['Content-Type'] = 'image/png' 17 | file.content = Converter.new(file.content).svg_to_png 18 | file 19 | else 20 | nil 21 | end 22 | end 23 | 24 | def self.original_path_for(path) 25 | path.gsub(/\.png/, '') 26 | end 27 | 28 | class Converter 29 | def initialize(blob) 30 | @blob = blob 31 | end 32 | 33 | def svg_to_png 34 | logger.info "** rasterize svg to png" 35 | result = popen("rsvg-convert") 36 | raise TransformationError, "Data was not a valid SVG image." unless $? == 0 37 | result 38 | end 39 | 40 | private 41 | 42 | def logger 43 | Logger.current 44 | end 45 | 46 | def popen(cmd) 47 | IO.popen(cmd, 'r+') do |io| 48 | io.write @blob 49 | io.close_write 50 | io.read 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/imagery/image_variant_generator.rb: -------------------------------------------------------------------------------- 1 | require 'RMagick' 2 | require 'fileutils' 3 | 4 | module Imagery 5 | class ImageVariantGenerator 6 | VARIANT_DELIMITER = '_' 7 | SupportedImageTypes = ['.gif', '.jpg', '.jpeg', '.png', '.bmp'] 8 | 9 | attr_accessor :content 10 | attr_accessor :content_type 11 | 12 | def self.variant_parser 13 | @variant_parser ||= /(.*)\_(#{Transformations.list.join('|')})(#{SupportedImageTypes.join('|')})/i 14 | end 15 | 16 | def self.from_url(server, path) 17 | return nil unless path =~ variant_parser 18 | 19 | remote_path = "#{$1}#{$3}" 20 | 21 | file = Image.new(server, remote_path) 22 | if file.found? 23 | transform_content(file, $2) 24 | file 25 | else 26 | nil 27 | end 28 | end 29 | 30 | def initialize(image) 31 | @image = image 32 | end 33 | 34 | def self.transform_content(image, variant) 35 | img = Magick::Image.from_blob(image.content).first 36 | transformation = Transformations[variant] 37 | 38 | Logger.current.info_with_time "Transforming image to #{variant}" do 39 | raise ArgumentError, "#{variant} is not a known transformation. (#{Transformations.list.join(', ')})" if transformation.nil? 40 | img = transformation.call(img) 41 | raise ArgumentError, "Creating variant #{variant} for #{path} produced an error. Please return a Magick::Image" if img.nil? 42 | image.content = img.to_blob 43 | end 44 | true 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/imagery/image.rb: -------------------------------------------------------------------------------- 1 | require 'RMagick' 2 | require 'fileutils' 3 | require 'patron' 4 | 5 | module Imagery 6 | class Image 7 | attr_accessor :content 8 | attr_reader :headers 9 | attr_reader :status 10 | attr_reader :server 11 | 12 | def initialize(server, path) 13 | @server = server 14 | @path = path 15 | download(path) 16 | end 17 | 18 | def found? 19 | @status == 200 20 | end 21 | 22 | def basename 23 | File.basename(@path) 24 | end 25 | 26 | def basename_no_ext 27 | File.basename(@path, ext) 28 | end 29 | 30 | def ext 31 | File.extname(@path) 32 | end 33 | 34 | def dirname 35 | File.dirname(@path) 36 | end 37 | 38 | private 39 | 40 | def session 41 | @@session ||= begin 42 | sess = Patron::Session.new 43 | sess.timeout = 10 44 | sess.headers['User-Agent'] = 'imagery/1.0' 45 | sess 46 | end 47 | end 48 | 49 | def download(path_info) 50 | session.base_url = "http://#{server}" 51 | 52 | response = Logger.current.info_with_time "Loading http://#{server}#{path_info}" do 53 | session.get(path_info) 54 | end 55 | 56 | @path = path_info.split('?')[0] 57 | @headers = response.headers 58 | @status = response.status 59 | 60 | if found? 61 | self.content = response.body 62 | true 63 | else 64 | Logger.current.error "Not found" 65 | false 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/imagery/logger_ext.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | require 'benchmark' 3 | 4 | begin 5 | require File.dirname(__FILE__) + "/vendor/SyslogLogger-1.4.0/lib/syslog_logger" 6 | rescue LoadError 7 | STDERR.puts('** Syslog is not supported') 8 | end 9 | 10 | class Logger 11 | 12 | module Extensions 13 | def self.included(base) 14 | base.send(:define_method, :mutex) {@mutex ||= Mutex.new} 15 | end 16 | 17 | def intend 18 | @intend = true 19 | yield 20 | ensure 21 | @intend = false 22 | end 23 | 24 | def buffer 25 | self.mutex.synchronize do 26 | @buffer = [] 27 | begin 28 | yield 29 | ensure 30 | buffer = @buffer 31 | @buffer = nil 32 | buffer.each do |method, msg| 33 | self.send(method, msg) 34 | end 35 | end 36 | end 37 | end 38 | 39 | def info_with_time(msg) 40 | result = nil 41 | rm = Benchmark.realtime { result = yield } 42 | info msg + " [%.3fs]" % [rm] 43 | result 44 | end 45 | 46 | [:info, :warn, :error, :debug].each do |level| 47 | define_method(level) do |msg| 48 | 49 | msg = @intend ? " " + msg : msg 50 | 51 | if @buffer 52 | @buffer << [level, msg] 53 | else 54 | super(msg) 55 | end 56 | end 57 | end 58 | 59 | end 60 | 61 | def self.current 62 | @logger 63 | end 64 | 65 | def self.current=(logger) 66 | @logger = logger 67 | end 68 | 69 | end 70 | 71 | Logger.send :include, Logger::Extensions 72 | 73 | if defined? SyslogLogger 74 | SyslogLogger.send :include, Logger::Extensions 75 | end 76 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rackup -s thin -E none 2 | 3 | require 'rubygems' 4 | require 'rack/cache' 5 | require 'rack/contrib' 6 | 7 | $: << File.join(File.dirname(__FILE__), 'lib') 8 | require 'imagery' 9 | require 'config/env' 10 | 11 | 12 | use Rack::Config do |env| 13 | env['imagery.origin_host'] = ORIGIN_SERVER 14 | end 15 | 16 | 17 | # Add rack sendfile extension. 18 | # Allows us to serve cache hits directly from file system 19 | # by nginx (big speed boost). read: 20 | # http://github.com/rack/rack-contrib/blob/5ea5e585a43669842314aa07f1e603be70d6e288/lib/rack/contrib/sendfile.rb 21 | 22 | 23 | if ENV['NGINX_ACCEL_REDIRECTS'] 24 | STDERR.puts 'Using accel redirect (Shopify config).' 25 | require 'imagery/middleware/accel_redirect' 26 | use Imagery::AccelRedirect 27 | else 28 | use Rack::Sendfile 29 | end 30 | 31 | use Rack::ShowExceptions 32 | 33 | # 1. Forget about stupid favicons 34 | use Imagery::FaviconFilter 35 | 36 | # 2. Log all other incoming requests 37 | use Imagery::LoggedRequest 38 | 39 | # 3. Override server name into something non embarrasing 40 | use Imagery::ServerName 41 | 42 | # 4. Content type needs to be present, default to attachment 43 | use Rack::ContentType, "application/octet-stream" 44 | 45 | # 5. Serve converted images directly from cache 46 | use Rack::Cache, 47 | :metastore => ENV['META_STORE'], 48 | :entitystore => ENV['ENTITY_STORE'] 49 | 50 | # 6. handle PURGE requests 51 | use Imagery::CachePurge 52 | 53 | # 7. See if files already exist on remote host, if so handle them directly 54 | use Imagery::RemoteProxy 55 | 56 | # 8. Otherwise run the image server and produce the missing images 57 | run Imagery::Server.new 58 | -------------------------------------------------------------------------------- /test/test_image_variant_generator.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'helper') 2 | 3 | FakeWeb.allow_net_connect = false 4 | FakeWeb.register_uri(:get, "http://static.shopify.com/image.png", :body => File.read( File.dirname(__FILE__) + '/assets/fish.png'), :content_type => "image/png", :cache_control => 'public, max-age=0') 5 | FakeWeb.register_uri(:get, "http://static.shopify.com/failed_image.png", :status => 404) 6 | 7 | class TestRemoteProxy < Test::Unit::TestCase 8 | 9 | def setup 10 | @headers = {'Content-Type' => "image/png", 'Cache-Control' => 'public, max-age=0', 'ETag' => 'abc', 'Last-Modified' => "Mon, 24 Aug 2009 18:07:15 GMT"} 11 | 12 | Patron::Session.any_instance.stubs(:get).with('/image.png').returns( 13 | stub(:headers => @headers, :body => File.read( File.dirname(__FILE__) + '/assets/fish.png'), :status => 200) 14 | ) 15 | 16 | Patron::Session.any_instance.stubs(:get).with('/failed_image.png').returns( 17 | stub(:headers => {}, :status => 404) 18 | ) 19 | end 20 | 21 | def test_successfull_call 22 | assert Imagery::ImageVariantGenerator.from_url('static.shopify.com', '/image_pico.png') 23 | assert Imagery::ImageVariantGenerator.from_url('static.shopify.com', '/image_small.png') 24 | end 25 | 26 | def test_return_nil_on_404 27 | assert_equal nil, Imagery::ImageVariantGenerator.from_url('static.shopify.com', '/failed_image_pico.png') 28 | end 29 | 30 | def test_wrong_filename 31 | assert_equal nil, Imagery::ImageVariantGenerator.from_url('static.shopify.com', '/image.png') 32 | assert_equal nil, Imagery::ImageVariantGenerator.from_url('static.shopify.com', '/image_whatever.png') 33 | assert_equal nil, Imagery::ImageVariantGenerator.from_url('static.shopify.com', '/image.tga') 34 | end 35 | 36 | end -------------------------------------------------------------------------------- /test/test_remote_proxy.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'helper') 2 | 3 | # FakeWeb.allow_net_connect = false 4 | # FakeWeb.register_uri(:get, "http://static.shopify.com/test.txt", :body => "Hello World!", :content_type => "text/plain", :cache_control => 'public, max-age=0') 5 | # FakeWeb.register_uri(:get, "http://static.shopify.com/test2.txt", :status => 404) 6 | # FakeWeb.register_uri(:get, "http://static.shopify.com/test3.txt?abc", :body => "Hello World!", :content_type => "text/plain", :cache_control => 'public, max-age=0') 7 | # 8 | class TestRemoteProxy < Test::Unit::TestCase 9 | StandardResponse = [200, {}, ['OK']] 10 | ExpectedResponse = [200, 11 | {"Cache-Control"=>"public, max-age=0", "Content-Type"=>"text/plain", "ETag"=>"abc", "Content-Length"=>"12", 'Last-Modified' => "Mon, 24 Aug 2009 18:07:15 GMT"}, 12 | ["Hello World!"]] 13 | 14 | def setup 15 | @headers = {'Content-Type' => "text/plain", 'Cache-Control' => 'public, max-age=0', 'ETag' => 'abc', 'Last-Modified' => "Mon, 24 Aug 2009 18:07:15 GMT"} 16 | 17 | Patron::Session.any_instance.stubs(:get).with('/test.txt').returns( 18 | stub(:headers => @headers, :body => 'Hello World!', :status => 200) 19 | ) 20 | 21 | Patron::Session.any_instance.stubs(:get).with('/test2.txt').returns( 22 | stub(:headers => {}, :status => 404) 23 | ) 24 | 25 | Patron::Session.any_instance.stubs(:get).with('/test3.txt?abc').returns( 26 | stub(:headers => @headers, :body => 'Hello World!', :status => 200) 27 | ) 28 | 29 | 30 | @app = Imagery::RemoteProxy.new lambda { StandardResponse } 31 | end 32 | 33 | def test_successfull_call 34 | env = Rack::MockRequest.env_for("/test.txt", {}) 35 | assert_equal ExpectedResponse, @app.call(env) 36 | end 37 | 38 | def test_remote_miss_continues_chain 39 | env = Rack::MockRequest.env_for("/test2.txt", {}) 40 | 41 | assert_equal StandardResponse, @app.call(env) 42 | end 43 | 44 | def test_remote_calls_preserve_query_parameters 45 | env = Rack::MockRequest.env_for("/test3.txt?abc", {}) 46 | assert_equal ExpectedResponse, @app.call(env) 47 | end 48 | end -------------------------------------------------------------------------------- /config/deploy.rb: -------------------------------------------------------------------------------- 1 | set :application, "imagery" 2 | set :repository, "git://github.com/tobi/image_server.git" 3 | set :branch, "origin/master" 4 | set :user, 'deploy' 5 | set :deploy_type, 'deploy' 6 | 7 | role :app, instance = ENV['INSTANCE'] || "vm" 8 | 9 | namespace :deploy do 10 | desc "Deploy it" 11 | task :default do 12 | update_code 13 | restart 14 | cleanup 15 | end 16 | 17 | desc "Setup a GitHub-style deployment." 18 | task :setup, :except => { :no_release => true } do 19 | run "git clone #{repository} #{current_path}" 20 | end 21 | 22 | desc "Update the deployed code." 23 | task :update_code, :except => { :no_release => true } do 24 | run "cd #{current_path}; git fetch origin; git reset --hard #{branch}; git tag '#{Time.now.to_i}-#{deploy_type}'" 25 | end 26 | 27 | desc "List deployment tags for use with deploy:rollback TAG=" 28 | task :list_tags, :except => { :no_release => true } do 29 | run "cd #{current_path}; git tag -l '*-deploy' -n 3" 30 | end 31 | 32 | begin 33 | 34 | namespace :rollback do 35 | desc "Rollback a single commit." 36 | task :default, :except => { :no_release => true } do 37 | branch = ENV['TAG'] || capture("cd #{current_path}; git tag -l '*-deploy' | tail -n2 | head -n1") 38 | set :deploy_type, 'rollback' 39 | set :branch, branch 40 | deploy.default 41 | end 42 | end 43 | 44 | rescue ArgumentError 45 | 46 | desc "Rollback a single commit." 47 | task :rollback, :except => { :no_release => true } do 48 | branch = ENV['TAG'] || capture("cd #{current_path}; git tag -l '*-deploy' | tail -n2 | head -n1") 49 | set :deploy_type, 'rollback' 50 | set :branch, branch 51 | deploy.default 52 | end 53 | 54 | end 55 | 56 | 57 | desc "Signal Passenger to restart the application" 58 | task :restart, :roles => :app do 59 | run "mkdir -p #{current_path}/tmp && touch #{current_path}/tmp/restart.txt" 60 | end 61 | end 62 | 63 | namespace :logs do 64 | 65 | desc "Watch jobs log" 66 | task :default do 67 | sudo "tail -f #{current_path}/log/production.log" 68 | end 69 | 70 | end -------------------------------------------------------------------------------- /lib/imagery/vendor/SyslogLogger-1.4.0/lib/syslog_logger.rb: -------------------------------------------------------------------------------- 1 | require 'syslog' 2 | require 'logger' 3 | 4 | ## 5 | # SyslogLogger is a Logger work-alike that logs via syslog instead of to a 6 | # file. You can add SyslogLogger to your Rails production environment to 7 | # aggregate logs between multiple machines. 8 | # 9 | # By default, SyslogLogger uses the program name 'rails', but this can be 10 | # changed via the first argument to SyslogLogger.new. 11 | # 12 | # NOTE! You can only set the SyslogLogger program name when you initialize 13 | # SyslogLogger for the first time. This is a limitation of the way 14 | # SyslogLogger uses syslog (and in some ways, a limitation of the way 15 | # syslog(3) works). Attempts to change SyslogLogger's program name after the 16 | # first initialization will be ignored. 17 | # 18 | # = Sample usage with Rails 19 | # 20 | # == config/environment/production.rb 21 | # 22 | # Add the following lines: 23 | # 24 | # require 'syslog_logger' 25 | # RAILS_DEFAULT_LOGGER = SyslogLogger.new 26 | # 27 | # == config/environment.rb 28 | # 29 | # In 0.10.0, change this line: 30 | # 31 | # RAILS_DEFAULT_LOGGER = Logger.new("#{RAILS_ROOT}/log/#{RAILS_ENV}.log") 32 | # 33 | # to: 34 | # 35 | # RAILS_DEFAULT_LOGGER ||= Logger.new("#{RAILS_ROOT}/log/#{RAILS_ENV}.log") 36 | # 37 | # Other versions of Rails should have a similar change. 38 | # 39 | # == BSD syslog setup 40 | # 41 | # === /etc/syslog.conf 42 | # 43 | # Add the following lines: 44 | # 45 | # !rails 46 | # *.* /var/log/production.log 47 | # 48 | # Then touch /var/log/production.log and signal syslogd with a HUP 49 | # (killall -HUP syslogd, on FreeBSD). 50 | # 51 | # === /etc/newsyslog.conf 52 | # 53 | # Add the following line: 54 | # 55 | # /var/log/production.log 640 7 * @T00 Z 56 | # 57 | # This creates a log file that is rotated every day at midnight, gzip'd, then 58 | # kept for 7 days. Consult newsyslog.conf(5) for more details. 59 | # 60 | # == syslog-ng setup 61 | # 62 | # === syslog-ng.conf 63 | # 64 | # destination rails_log { file("/var/log/production.log"); }; 65 | # filter f_rails { program("rails.*"); }; 66 | # log { source(src); filter(f_rails); destination(rails_log); }; 67 | # 68 | # == Starting 69 | # 70 | # Now restart your Rails app. Your production logs should now be showing up 71 | # in /var/log/production.log. If you have mulitple machines, you can log them 72 | # all to a central machine with remote syslog logging for analysis. Consult 73 | # your syslogd(8) manpage for further details. 74 | 75 | class SyslogLogger 76 | 77 | ## 78 | # The version of SyslogLogger you are using. 79 | 80 | VERSION = '1.4.0' 81 | 82 | ## 83 | # Maps Logger warning types to syslog(3) warning types. 84 | 85 | LOGGER_MAP = { 86 | :unknown => :alert, 87 | :fatal => :err, 88 | :error => :warning, 89 | :warn => :notice, 90 | :info => :info, 91 | :debug => :debug, 92 | } 93 | 94 | ## 95 | # Maps Logger log levels to their values so we can silence. 96 | 97 | LOGGER_LEVEL_MAP = {} 98 | 99 | LOGGER_MAP.each_key do |key| 100 | LOGGER_LEVEL_MAP[key] = Logger.const_get key.to_s.upcase 101 | end 102 | 103 | ## 104 | # Maps Logger log level values to syslog log levels. 105 | 106 | LEVEL_LOGGER_MAP = {} 107 | 108 | LOGGER_LEVEL_MAP.invert.each do |level, severity| 109 | LEVEL_LOGGER_MAP[level] = LOGGER_MAP[severity] 110 | end 111 | 112 | ## 113 | # Builds a methods for level +meth+. 114 | 115 | def self.make_methods(meth) 116 | eval <<-EOM, nil, __FILE__, __LINE__ + 1 117 | def #{meth}(message = nil) 118 | return true if #{LOGGER_LEVEL_MAP[meth]} < @level 119 | SYSLOG.#{LOGGER_MAP[meth]} clean(message || yield) 120 | return true 121 | end 122 | 123 | def #{meth}? 124 | @level <= Logger::#{meth.to_s.upcase} 125 | end 126 | EOM 127 | end 128 | 129 | LOGGER_MAP.each_key do |level| 130 | make_methods level 131 | end 132 | 133 | ## 134 | # Log level for Logger compatibility. 135 | 136 | attr_accessor :level 137 | 138 | ## 139 | # Fills in variables for Logger compatibility. If this is the first 140 | # instance of SyslogLogger, +program_name+ may be set to change the logged 141 | # program name. 142 | # 143 | # Due to the way syslog works, only one program name may be chosen. 144 | 145 | def initialize(program_name = 'rails') 146 | @level = Logger::DEBUG 147 | 148 | return if defined? SYSLOG 149 | self.class.const_set :SYSLOG, Syslog.open(program_name) 150 | end 151 | 152 | ## 153 | # Almost duplicates Logger#add. +progname+ is ignored. 154 | 155 | def add(severity, message = nil, progname = nil, &block) 156 | severity ||= Logger::UNKNOWN 157 | return true if severity < @level 158 | message = clean(message || block.call) 159 | SYSLOG.send LEVEL_LOGGER_MAP[severity], clean(message) 160 | return true 161 | end 162 | 163 | ## 164 | # Allows messages of a particular log level to be ignored temporarily. 165 | # 166 | # Can you say "Broken Windows"? 167 | 168 | def silence(temporary_level = Logger::ERROR) 169 | old_logger_level = @level 170 | @level = temporary_level 171 | yield 172 | ensure 173 | @level = old_logger_level 174 | end 175 | 176 | private 177 | 178 | ## 179 | # Clean up messages so they're nice and pretty. 180 | 181 | def clean(message) 182 | message = message.to_s.dup 183 | message.strip! 184 | message.gsub!(/%/, '%%') # syslog(3) freaks on % (printf) 185 | message.gsub!(/\e\[[^m]*m/, '') # remove useless ansi color codes 186 | return message 187 | end 188 | 189 | end 190 | 191 | -------------------------------------------------------------------------------- /lib/imagery/vendor/SyslogLogger-1.4.0/test/test_syslog_logger.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'tempfile' 3 | require 'syslog_logger' 4 | 5 | module MockSyslog; end 6 | 7 | class << MockSyslog 8 | 9 | @line = nil 10 | 11 | SyslogLogger::LOGGER_MAP.values.uniq.each do |level| 12 | eval <<-EOM 13 | def #{level}(message) 14 | @line = "#{level.to_s.upcase} - \#{message}" 15 | end 16 | EOM 17 | end 18 | 19 | attr_reader :line 20 | attr_reader :program_name 21 | 22 | def open(program_name) 23 | @program_name = program_name 24 | end 25 | 26 | def reset 27 | @line = '' 28 | end 29 | 30 | end 31 | 32 | SyslogLogger.const_set :SYSLOG, MockSyslog 33 | 34 | class TestLogger < Test::Unit::TestCase 35 | 36 | LEVEL_LABEL_MAP = { 37 | Logger::DEBUG => 'DEBUG', 38 | Logger::INFO => 'INFO', 39 | Logger::WARN => 'WARN', 40 | Logger::ERROR => 'ERROR', 41 | Logger::FATAL => 'FATAL', 42 | Logger::UNKNOWN => 'ANY', 43 | } 44 | 45 | def setup 46 | @logger = Logger.new(nil) 47 | end 48 | 49 | class Log 50 | attr_reader :line, :label, :datetime, :pid, :severity, :progname, :msg 51 | def initialize(line) 52 | @line = line 53 | /\A(\w+), \[([^#]*)#(\d+)\]\s+(\w+) -- (\w*): ([\x0-\xff]*)/ =~ @line 54 | @label, @datetime, @pid, @severity, @progname, @msg = $1, $2, $3, $4, $5, $6 55 | end 56 | end 57 | 58 | def log_add(severity, msg, progname = nil, &block) 59 | log(:add, severity, msg, progname, &block) 60 | end 61 | 62 | def log(msg_id, *arg, &block) 63 | Log.new(log_raw(msg_id, *arg, &block)) 64 | end 65 | 66 | def log_raw(msg_id, *arg, &block) 67 | logdev = Tempfile.new(File.basename(__FILE__) + '.log') 68 | @logger.instance_eval { @logdev = Logger::LogDevice.new(logdev) } 69 | assert_equal true, @logger.__send__(msg_id, *arg, &block) 70 | logdev.open 71 | msg = logdev.read 72 | logdev.close 73 | msg 74 | end 75 | 76 | def test_initialize 77 | assert_equal Logger::DEBUG, @logger.level 78 | end 79 | 80 | def test_add 81 | msg = log_add nil, 'unknown level message' # nil == unknown 82 | assert_equal LEVEL_LABEL_MAP[Logger::UNKNOWN], msg.severity 83 | 84 | msg = log_add Logger::FATAL, 'fatal level message' 85 | assert_equal LEVEL_LABEL_MAP[Logger::FATAL], msg.severity 86 | 87 | msg = log_add Logger::ERROR, 'error level message' 88 | assert_equal LEVEL_LABEL_MAP[Logger::ERROR], msg.severity 89 | 90 | msg = log_add Logger::WARN, 'warn level message' 91 | assert_equal LEVEL_LABEL_MAP[Logger::WARN], msg.severity 92 | 93 | msg = log_add Logger::INFO, 'info level message' 94 | assert_equal LEVEL_LABEL_MAP[Logger::INFO], msg.severity 95 | 96 | msg = log_add Logger::DEBUG, 'debug level message' 97 | assert_equal LEVEL_LABEL_MAP[Logger::DEBUG], msg.severity 98 | end 99 | 100 | def test_add_level_unknown 101 | @logger.level = Logger::UNKNOWN 102 | 103 | msg = log_add nil, 'unknown level message' # nil == unknown 104 | assert_equal LEVEL_LABEL_MAP[Logger::UNKNOWN], msg.severity 105 | 106 | msg = log_add Logger::FATAL, 'fatal level message' 107 | assert_equal '', msg.line 108 | 109 | msg = log_add Logger::ERROR, 'error level message' 110 | assert_equal '', msg.line 111 | 112 | msg = log_add Logger::WARN, 'warn level message' 113 | assert_equal '', msg.line 114 | 115 | msg = log_add Logger::INFO, 'info level message' 116 | assert_equal '', msg.line 117 | 118 | msg = log_add Logger::DEBUG, 'debug level message' 119 | assert_equal '', msg.line 120 | end 121 | 122 | def test_add_level_fatal 123 | @logger.level = Logger::FATAL 124 | 125 | msg = log_add nil, 'unknown level message' # nil == unknown 126 | assert_equal LEVEL_LABEL_MAP[Logger::UNKNOWN], msg.severity 127 | 128 | msg = log_add Logger::FATAL, 'fatal level message' 129 | assert_equal LEVEL_LABEL_MAP[Logger::FATAL], msg.severity 130 | 131 | msg = log_add Logger::ERROR, 'error level message' 132 | assert_equal '', msg.line 133 | 134 | msg = log_add Logger::WARN, 'warn level message' 135 | assert_equal '', msg.line 136 | 137 | msg = log_add Logger::INFO, 'info level message' 138 | assert_equal '', msg.line 139 | 140 | msg = log_add Logger::DEBUG, 'debug level message' 141 | assert_equal '', msg.line 142 | end 143 | 144 | def test_add_level_error 145 | @logger.level = Logger::ERROR 146 | 147 | msg = log_add nil, 'unknown level message' # nil == unknown 148 | assert_equal LEVEL_LABEL_MAP[Logger::UNKNOWN], msg.severity 149 | 150 | msg = log_add Logger::FATAL, 'fatal level message' 151 | assert_equal LEVEL_LABEL_MAP[Logger::FATAL], msg.severity 152 | 153 | msg = log_add Logger::ERROR, 'error level message' 154 | assert_equal LEVEL_LABEL_MAP[Logger::ERROR], msg.severity 155 | 156 | msg = log_add Logger::WARN, 'warn level message' 157 | assert_equal '', msg.line 158 | 159 | msg = log_add Logger::INFO, 'info level message' 160 | assert_equal '', msg.line 161 | 162 | msg = log_add Logger::DEBUG, 'debug level message' 163 | assert_equal '', msg.line 164 | end 165 | 166 | def test_add_level_warn 167 | @logger.level = Logger::WARN 168 | 169 | msg = log_add nil, 'unknown level message' # nil == unknown 170 | assert_equal LEVEL_LABEL_MAP[Logger::UNKNOWN], msg.severity 171 | 172 | msg = log_add Logger::FATAL, 'fatal level message' 173 | assert_equal LEVEL_LABEL_MAP[Logger::FATAL], msg.severity 174 | 175 | msg = log_add Logger::ERROR, 'error level message' 176 | assert_equal LEVEL_LABEL_MAP[Logger::ERROR], msg.severity 177 | 178 | msg = log_add Logger::WARN, 'warn level message' 179 | assert_equal LEVEL_LABEL_MAP[Logger::WARN], msg.severity 180 | 181 | msg = log_add Logger::INFO, 'info level message' 182 | assert_equal '', msg.line 183 | 184 | msg = log_add Logger::DEBUG, 'debug level message' 185 | assert_equal '', msg.line 186 | end 187 | 188 | def test_add_level_info 189 | @logger.level = Logger::INFO 190 | 191 | msg = log_add nil, 'unknown level message' # nil == unknown 192 | assert_equal LEVEL_LABEL_MAP[Logger::UNKNOWN], msg.severity 193 | 194 | msg = log_add Logger::FATAL, 'fatal level message' 195 | assert_equal LEVEL_LABEL_MAP[Logger::FATAL], msg.severity 196 | 197 | msg = log_add Logger::ERROR, 'error level message' 198 | assert_equal LEVEL_LABEL_MAP[Logger::ERROR], msg.severity 199 | 200 | msg = log_add Logger::WARN, 'warn level message' 201 | assert_equal LEVEL_LABEL_MAP[Logger::WARN], msg.severity 202 | 203 | msg = log_add Logger::INFO, 'info level message' 204 | assert_equal LEVEL_LABEL_MAP[Logger::INFO], msg.severity 205 | 206 | msg = log_add Logger::DEBUG, 'debug level message' 207 | assert_equal '', msg.line 208 | end 209 | 210 | def test_add_level_debug 211 | @logger.level = Logger::DEBUG 212 | 213 | msg = log_add nil, 'unknown level message' # nil == unknown 214 | assert_equal LEVEL_LABEL_MAP[Logger::UNKNOWN], msg.severity 215 | 216 | msg = log_add Logger::FATAL, 'fatal level message' 217 | assert_equal LEVEL_LABEL_MAP[Logger::FATAL], msg.severity 218 | 219 | msg = log_add Logger::ERROR, 'error level message' 220 | assert_equal LEVEL_LABEL_MAP[Logger::ERROR], msg.severity 221 | 222 | msg = log_add Logger::WARN, 'warn level message' 223 | assert_equal LEVEL_LABEL_MAP[Logger::WARN], msg.severity 224 | 225 | msg = log_add Logger::INFO, 'info level message' 226 | assert_equal LEVEL_LABEL_MAP[Logger::INFO], msg.severity 227 | 228 | msg = log_add Logger::DEBUG, 'debug level message' 229 | assert_equal LEVEL_LABEL_MAP[Logger::DEBUG], msg.severity 230 | end 231 | 232 | def test_unknown 233 | msg = log :unknown, 'unknown level message' 234 | assert_equal LEVEL_LABEL_MAP[Logger::UNKNOWN], msg.severity 235 | 236 | @logger.level = Logger::UNKNOWN 237 | msg = log :unknown, 'unknown level message' 238 | assert_equal LEVEL_LABEL_MAP[Logger::UNKNOWN], msg.severity 239 | 240 | @logger.level = Logger::FATAL 241 | msg = log :unknown, 'unknown level message' 242 | assert_equal LEVEL_LABEL_MAP[Logger::UNKNOWN], msg.severity 243 | 244 | @logger.level = Logger::ERROR 245 | msg = log :unknown, 'unknown level message' 246 | assert_equal LEVEL_LABEL_MAP[Logger::UNKNOWN], msg.severity 247 | 248 | @logger.level = Logger::WARN 249 | msg = log :unknown, 'unknown level message' 250 | assert_equal LEVEL_LABEL_MAP[Logger::UNKNOWN], msg.severity 251 | 252 | @logger.level = Logger::INFO 253 | msg = log :unknown, 'unknown level message' 254 | assert_equal LEVEL_LABEL_MAP[Logger::UNKNOWN], msg.severity 255 | 256 | @logger.level = Logger::DEBUG 257 | msg = log :unknown, 'unknown level message' 258 | assert_equal LEVEL_LABEL_MAP[Logger::UNKNOWN], msg.severity 259 | end 260 | 261 | def test_unknown_eh 262 | @logger.level = Logger::UNKNOWN 263 | assert_equal true, @logger.unknown? 264 | 265 | @logger.level = Logger::UNKNOWN + 1 266 | assert_equal false, @logger.unknown? 267 | end 268 | 269 | def test_fatal 270 | msg = log :fatal, 'fatal level message' 271 | assert_equal LEVEL_LABEL_MAP[Logger::FATAL], msg.severity 272 | 273 | @logger.level = Logger::UNKNOWN 274 | msg = log :fatal, 'fatal level message' 275 | assert_equal '', msg.line 276 | 277 | @logger.level = Logger::FATAL 278 | msg = log :fatal, 'fatal level message' 279 | assert_equal LEVEL_LABEL_MAP[Logger::FATAL], msg.severity 280 | 281 | @logger.level = Logger::ERROR 282 | msg = log :fatal, 'fatal level message' 283 | assert_equal LEVEL_LABEL_MAP[Logger::FATAL], msg.severity 284 | 285 | @logger.level = Logger::WARN 286 | msg = log :fatal, 'fatal level message' 287 | assert_equal LEVEL_LABEL_MAP[Logger::FATAL], msg.severity 288 | 289 | @logger.level = Logger::INFO 290 | msg = log :fatal, 'fatal level message' 291 | assert_equal LEVEL_LABEL_MAP[Logger::FATAL], msg.severity 292 | 293 | @logger.level = Logger::DEBUG 294 | msg = log :fatal, 'fatal level message' 295 | assert_equal LEVEL_LABEL_MAP[Logger::FATAL], msg.severity 296 | end 297 | 298 | def test_fatal_eh 299 | @logger.level = Logger::FATAL 300 | assert_equal true, @logger.fatal? 301 | 302 | @logger.level = Logger::UNKNOWN 303 | assert_equal false, @logger.fatal? 304 | end 305 | 306 | def test_error 307 | msg = log :error, 'error level message' 308 | assert_equal LEVEL_LABEL_MAP[Logger::ERROR], msg.severity 309 | 310 | @logger.level = Logger::UNKNOWN 311 | msg = log :error, 'error level message' 312 | assert_equal '', msg.line 313 | 314 | @logger.level = Logger::FATAL 315 | msg = log :error, 'error level message' 316 | assert_equal '', msg.line 317 | 318 | @logger.level = Logger::ERROR 319 | msg = log :error, 'error level message' 320 | assert_equal LEVEL_LABEL_MAP[Logger::ERROR], msg.severity 321 | 322 | @logger.level = Logger::WARN 323 | msg = log :error, 'error level message' 324 | assert_equal LEVEL_LABEL_MAP[Logger::ERROR], msg.severity 325 | 326 | @logger.level = Logger::INFO 327 | msg = log :error, 'error level message' 328 | assert_equal LEVEL_LABEL_MAP[Logger::ERROR], msg.severity 329 | 330 | @logger.level = Logger::DEBUG 331 | msg = log :error, 'error level message' 332 | assert_equal LEVEL_LABEL_MAP[Logger::ERROR], msg.severity 333 | end 334 | 335 | def test_error_eh 336 | @logger.level = Logger::ERROR 337 | assert_equal true, @logger.error? 338 | 339 | @logger.level = Logger::FATAL 340 | assert_equal false, @logger.error? 341 | end 342 | 343 | def test_warn 344 | msg = log :warn, 'warn level message' 345 | assert_equal LEVEL_LABEL_MAP[Logger::WARN], msg.severity 346 | 347 | @logger.level = Logger::UNKNOWN 348 | msg = log :warn, 'warn level message' 349 | assert_equal '', msg.line 350 | 351 | @logger.level = Logger::FATAL 352 | msg = log :warn, 'warn level message' 353 | assert_equal '', msg.line 354 | 355 | @logger.level = Logger::ERROR 356 | msg = log :warn, 'warn level message' 357 | assert_equal '', msg.line 358 | 359 | @logger.level = Logger::WARN 360 | msg = log :warn, 'warn level message' 361 | assert_equal LEVEL_LABEL_MAP[Logger::WARN], msg.severity 362 | 363 | @logger.level = Logger::INFO 364 | msg = log :warn, 'warn level message' 365 | assert_equal LEVEL_LABEL_MAP[Logger::WARN], msg.severity 366 | 367 | @logger.level = Logger::DEBUG 368 | msg = log :warn, 'warn level message' 369 | assert_equal LEVEL_LABEL_MAP[Logger::WARN], msg.severity 370 | end 371 | 372 | def test_warn_eh 373 | @logger.level = Logger::WARN 374 | assert_equal true, @logger.warn? 375 | 376 | @logger.level = Logger::ERROR 377 | assert_equal false, @logger.warn? 378 | end 379 | 380 | def test_info 381 | msg = log :info, 'info level message' 382 | assert_equal LEVEL_LABEL_MAP[Logger::INFO], msg.severity 383 | 384 | @logger.level = Logger::UNKNOWN 385 | msg = log :info, 'info level message' 386 | assert_equal '', msg.line 387 | 388 | @logger.level = Logger::FATAL 389 | msg = log :info, 'info level message' 390 | assert_equal '', msg.line 391 | 392 | @logger.level = Logger::ERROR 393 | msg = log :info, 'info level message' 394 | assert_equal '', msg.line 395 | 396 | @logger.level = Logger::WARN 397 | msg = log :info, 'info level message' 398 | assert_equal '', msg.line 399 | 400 | @logger.level = Logger::INFO 401 | msg = log :info, 'info level message' 402 | assert_equal LEVEL_LABEL_MAP[Logger::INFO], msg.severity 403 | 404 | @logger.level = Logger::DEBUG 405 | msg = log :info, 'info level message' 406 | assert_equal LEVEL_LABEL_MAP[Logger::INFO], msg.severity 407 | end 408 | 409 | def test_info_eh 410 | @logger.level = Logger::INFO 411 | assert_equal true, @logger.info? 412 | 413 | @logger.level = Logger::WARN 414 | assert_equal false, @logger.info? 415 | end 416 | 417 | def test_debug 418 | msg = log :debug, 'debug level message' 419 | assert_equal LEVEL_LABEL_MAP[Logger::DEBUG], msg.severity 420 | 421 | @logger.level = Logger::UNKNOWN 422 | msg = log :debug, 'debug level message' 423 | assert_equal '', msg.line 424 | 425 | @logger.level = Logger::FATAL 426 | msg = log :debug, 'debug level message' 427 | assert_equal '', msg.line 428 | 429 | @logger.level = Logger::ERROR 430 | msg = log :debug, 'debug level message' 431 | assert_equal '', msg.line 432 | 433 | @logger.level = Logger::WARN 434 | msg = log :debug, 'debug level message' 435 | assert_equal '', msg.line 436 | 437 | @logger.level = Logger::INFO 438 | msg = log :debug, 'debug level message' 439 | assert_equal '', msg.line 440 | 441 | @logger.level = Logger::DEBUG 442 | msg = log :debug, 'debug level message' 443 | assert_equal LEVEL_LABEL_MAP[Logger::DEBUG], msg.severity 444 | end 445 | 446 | def test_debug_eh 447 | @logger.level = Logger::DEBUG 448 | assert_equal true, @logger.debug? 449 | 450 | @logger.level = Logger::INFO 451 | assert_equal false, @logger.debug? 452 | end 453 | 454 | end 455 | 456 | class TestSyslogLogger < TestLogger 457 | 458 | def setup 459 | super 460 | @logger = SyslogLogger.new 461 | end 462 | 463 | class Log 464 | attr_reader :line, :label, :datetime, :pid, :severity, :progname, :msg 465 | def initialize(line) 466 | @line = line 467 | return unless /\A(\w+) - (.*)\Z/ =~ @line 468 | severity, @msg = $1, $2 469 | severity = SyslogLogger::LOGGER_MAP.invert[severity.downcase.intern] 470 | @severity = severity.to_s.upcase 471 | @severity = 'ANY' if @severity == 'UNKNOWN' 472 | end 473 | end 474 | 475 | def log_add(severity, msg, progname = nil, &block) 476 | log(:add, severity, msg, progname, &block) 477 | end 478 | 479 | def log(msg_id, *arg, &block) 480 | Log.new(log_raw(msg_id, *arg, &block)) 481 | end 482 | 483 | def log_raw(msg_id, *arg, &block) 484 | assert_equal true, @logger.__send__(msg_id, *arg, &block) 485 | msg = MockSyslog.line 486 | MockSyslog.reset 487 | return msg 488 | end 489 | 490 | end 491 | 492 | -------------------------------------------------------------------------------- /test/assets/fish.svg: -------------------------------------------------------------------------------- 1 | 2 | Created with Raven (http://www.aviary.com) 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 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 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 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 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | --------------------------------------------------------------------------------