├── 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 |
--------------------------------------------------------------------------------