thumbs 0.0.9 → 1.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,9 +1,11 @@
1
1
  # A sample Gemfile
2
2
  source "http://rubygems.org"
3
3
 
4
- gem "rack"
5
- gem "rack-contrib"
6
- gem "mini_magick"
4
+ gem "goliath"
5
+ gem 'eventmachine', '>= 1.0.0.beta.1'
6
+ gem 'em-synchrony', '>= 0.3.0.beta.1'
7
+ gem 'em-http-request', '>= 1.0.0.beta.1'
8
+ gem "em-files"
7
9
  gem "lumberjack"
8
10
 
9
11
  group :development do
data/README.textile CHANGED
@@ -1,53 +1,21 @@
1
1
  h1. Thumbs
2
2
 
3
- Image server proxy that can resize images on the fly. Built in ruby as a Rack application.
3
+ Image proxy server that can resize and cache images on the fly. Built in ruby with Goliath API.
4
4
 
5
5
  h2. Requirements
6
6
 
7
- * rack
8
- * rack-contrib
9
- * mini_magick
7
+ * ruby 1.9.2
8
+ * goliath
10
9
 
11
10
  h2. Usage
12
11
 
13
- 1. as a standalone rack app
12
+ $ gem install thumbs
13
+ $ thumbs -c path/to/config.rb -p 9000
14
14
 
15
- config.ru
16
-
17
- bc.. require "rubygems"
18
- require 'thumbs'
19
-
20
- run Thumbs.new(
21
- :url_map => "/:size/:original_url", # url pattern, :original_url is required
22
- :thumbs_folder => "public/system/thumbs", # default: false - do not write on disk
23
- :cache => true, # cache resized images - has no effect unless thumbs_folder exist
24
- :cache_original => true, # cache original images - has no effect unless thumbs_folder exist
25
- :runtime => true, # add X-Runtime header
26
- :cache_control => {
27
- :ttl => 86400, # add Expires and Cache-Control header for 24h
28
- :last_modified => true # add Last-Modfied header
29
- },
30
- :etag => true, # add ETag
31
- :logfile => false # will write log file if filepath is provided
32
- :server_name => "Thumbs/0.0.8", # Server header
33
- :image_not_found => "image_not_found.jpg" # path to image_not_found
34
- )
35
-
36
- 2. as a moundable rails app
37
-
38
- YourApplication::Application.routes.draw do
39
-
40
- mount Thumbs.new(:url_map => "/:size/:original_url") => "images/resized"
41
-
42
- ....
43
- end
44
-
45
- 3. you can even use it on heroku for free
46
-
47
- create your own rack app, or use this one: http://thumbs-gem.heroku.com/i/100x100/www.google.com/images/logos/ps_logo2.png
48
-
49
15
  h2. TODO
50
16
 
17
+ * documentation
18
+ * more middleware
51
19
  * Tests!
52
20
  * Access control
53
21
  * SSL support
data/Rakefile CHANGED
@@ -15,10 +15,11 @@ Jeweler::Tasks.new do |gem|
15
15
  gem.name = "thumbs"
16
16
  gem.homepage = "http://github.com/martinciu/thumbs"
17
17
  gem.license = "MIT"
18
- gem.summary = "Image server proxy that can resize images on the fly. Built in ruby as a Rack application."
18
+ gem.summary = "Image proxy server that can resize and cache images on the fly. Built in ruby with Goliath API."
19
19
  gem.description = ""
20
20
  gem.email = "marcin.ciunelis@gmail.com"
21
21
  gem.authors = ["Marcin Ciunelis"]
22
+ gem.executables = ["thumbs"]
22
23
  end
23
24
  Jeweler::RubygemsDotOrgTasks.new
24
25
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.9
1
+ 1.0.0.beta.1
data/bin/thumbs ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ config_index = (ARGV.index('-c') || ARGV.index('--config')) + 1
5
+ ARGV[config_index] = File.absolute_path(ARGV[config_index])
6
+ rescue
7
+ puts "Please specify a valid mneme configuration file (ex: -c config.rb)"
8
+ exit if !(ARGV.index('-h') || ARGV.index('--config'))
9
+ end
10
+
11
+ system("/usr/bin/env ruby " + File.dirname(__FILE__) + '/../lib/thumbs.rb' + ' ' + ARGV.join(" "))
@@ -0,0 +1,54 @@
1
+ env = "production"
2
+ app_root = File.expand_path("../..", __FILE__)
3
+
4
+ %w{9001 9002}.each do |port|
5
+ God.watch do |w|
6
+ w.name = "thumbs-#{env}-#{port}"
7
+ w.group = "thumbs-#{env}"
8
+ w.interval = 30.seconds
9
+ w.dir = app_root
10
+ w.start = "thumbs -p #{port} -e #{env}"
11
+ w.uid = 'rails'
12
+ w.gid = 'rails'
13
+
14
+ w.stop_signal = 'QUIT'
15
+ w.stop_timeout = 20.seconds
16
+
17
+ # restart if memory gets too high
18
+ w.transition(:up, :restart) do |on|
19
+ on.condition(:memory_usage) do |c|
20
+ c.above = 150.megabytes
21
+ c.times = 2
22
+ end
23
+ end
24
+
25
+ # determine the state on startup
26
+ w.transition(:init, { true => :up, false => :start }) do |on|
27
+ on.condition(:process_running) do |c|
28
+ c.running = true
29
+ end
30
+ end
31
+
32
+ # determine when process has finished starting
33
+ w.transition([:start, :restart], :up) do |on|
34
+ on.condition(:process_running) do |c|
35
+ c.running = true
36
+ c.interval = 5.seconds
37
+ end
38
+
39
+ # failsafe
40
+ on.condition(:tries) do |c|
41
+ c.times = 5
42
+ c.transition = :start
43
+ c.interval = 5.seconds
44
+ end
45
+ end
46
+
47
+ # start if process is not running
48
+ w.transition(:up, :start) do |on|
49
+ on.condition(:process_running) do |c|
50
+ c.running = false
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,4 @@
1
+ config['url_map'] = "/images/resized/:size/:original_url"
2
+ config['thumbs_folder'] = "cache"
3
+ config['cache'] = true
4
+ config['cache_original'] = true
@@ -0,0 +1,184 @@
1
+ require "open-uri"
2
+ require "stringio"
3
+ require "fileutils"
4
+ require "open3"
5
+
6
+ require File.join(File.dirname(__FILE__), '/image_temp_file')
7
+
8
+ module EventedMagick
9
+ class MiniMagickError < RuntimeError; end
10
+
11
+ class Image
12
+ attr :path
13
+ attr :tempfile
14
+ attr :output
15
+
16
+ # Class Methods
17
+ # -------------
18
+ class << self
19
+ def from_blob(blob, ext = nil)
20
+ begin
21
+ tempfile = ImageTempFile.new(ext)
22
+ tempfile.binmode
23
+ tempfile.write(blob)
24
+ ensure
25
+ tempfile.close if tempfile
26
+ end
27
+
28
+ return self.new(tempfile.path, tempfile)
29
+ end
30
+
31
+ # Use this if you don't want to overwrite the image file
32
+ def open(image_path)
33
+ File.open(image_path, "rb") do |f|
34
+ self.from_blob(f.read, File.extname(image_path))
35
+ end
36
+ end
37
+ alias_method :from_file, :open
38
+ end
39
+
40
+ # Instance Methods
41
+ # ----------------
42
+ def initialize(input_path, tempfile=nil)
43
+ @path = input_path
44
+ @tempfile = tempfile # ensures that the tempfile will stick around until this image is garbage collected.
45
+ @method = defined?(::EM) && EM.reactor_running? ? :evented_execute : :blocking_execute
46
+
47
+ # Ensure that the file is an image
48
+ output = run_command("identify", "-format", format_option("%m %w %h"), @path)
49
+ (format, width, height) = output.split
50
+ @values = { 'format' => format, 'width' => width.to_i, 'height' => height.to_i, 'dimensions' => [width.to_i, height.to_i] }
51
+ end
52
+
53
+ # For reference see http://www.imagemagick.org/script/command-line-options.php#format
54
+ def [](value)
55
+ key = value.to_s
56
+ return @values[key] if %w(format width height dimensions).include? key
57
+ if key == "size"
58
+ File.size(@path)
59
+ else
60
+ run_command('identify', '-format', "\"#{key}\"", @path).split("\n")[0]
61
+ end
62
+ end
63
+
64
+ # Sends raw commands to imagemagick's mogrify command. The image path is automatically appended to the command
65
+ def <<(*args)
66
+ run_command("mogrify", *args << @path)
67
+ end
68
+
69
+ # This is a 'special' command because it needs to change @path to reflect the new extension
70
+ # Formatting an animation into a non-animated type will result in ImageMagick creating multiple
71
+ # pages (starting with 0). You can choose which page you want to manipulate. We default to the
72
+ # first page.
73
+ def format(format, page=0)
74
+ run_command("mogrify", "-format", format, @path)
75
+
76
+ old_path = @path.dup
77
+ @path.sub!(/(\.\w+)?$/, ".#{format}")
78
+ File.delete(old_path) unless old_path == @path
79
+
80
+ unless File.exists?(@path)
81
+ begin
82
+ FileUtils.copy_file(@path.sub(".#{format}", "-#{page}.#{format}"), @path)
83
+ rescue e
84
+ raise MiniMagickError, "Unable to format to #{format}; #{e}" unless File.exist?(@path)
85
+ end
86
+ end
87
+ ensure
88
+ Dir[@path.sub(/(\.\w+)?$/, "-[0-9]*.#{format}")].each do |fname|
89
+ File.unlink(fname)
90
+ end
91
+ end
92
+
93
+ # Writes the temporary image that we are using for processing to the output path
94
+ def write(output_path)
95
+ FileUtils.copy_file @path, output_path
96
+ run_command "identify", output_path # Verify that we have a good image
97
+ end
98
+
99
+ # Give you raw data back
100
+ def to_blob
101
+ f = File.new @path
102
+ f.binmode
103
+ f.read
104
+ ensure
105
+ f.close if f
106
+ end
107
+
108
+ # If an unknown method is called then it is sent through the morgrify program
109
+ # Look here to find all the commands (http://www.imagemagick.org/script/mogrify.php)
110
+ def method_missing(symbol, *args)
111
+ args.push(@path) # push the path onto the end
112
+ run_command("mogrify", "-#{symbol}", *args)
113
+ self
114
+ end
115
+
116
+ # You can use multiple commands together using this method
117
+ def combine_options(&block)
118
+ c = CommandBuilder.new
119
+ block.call c
120
+ run_command("mogrify", *c.args << @path)
121
+ end
122
+
123
+ # Check to see if we are running on win32 -- we need to escape things differently
124
+ def windows?
125
+ !(RUBY_PLATFORM =~ /win32/).nil?
126
+ end
127
+
128
+ # Outputs a carriage-return delimited format string for Unix and Windows
129
+ def format_option(format)
130
+ windows? ? "#{format}\\n" : "#{format}\\\\n"
131
+ end
132
+
133
+ def run_command(command, *args)
134
+ full_args = args.collect do |arg|
135
+ # args can contain characters like '>' so we must escape them, but don't quote switches
136
+ if arg !~ /^[\+\-]/
137
+ "\"#{arg}\""
138
+ else
139
+ arg.to_s
140
+ end
141
+ end.join(' ')
142
+
143
+ full_cmd = "#{command} #{full_args}"
144
+ (output, status) = send(@method, full_cmd)
145
+
146
+ if status.exitstatus == 0
147
+ output
148
+ else
149
+ raise MiniMagickError, "ImageMagick command (#{full_cmd.inspect}) failed: #{{:status_code => status, :output => output}.inspect}"
150
+ end
151
+ end
152
+
153
+ def evented_execute(cmd)
154
+ fiber = Fiber.current
155
+ EM::system(cmd) do |output, status|
156
+ fiber.resume([output, status])
157
+ end
158
+
159
+ Fiber.yield
160
+ end
161
+
162
+ def blocking_execute(cmd)
163
+ output = `#{cmd}`
164
+ [output, $?]
165
+ end
166
+ end
167
+
168
+ class CommandBuilder
169
+ attr :args
170
+
171
+ def initialize
172
+ @args = []
173
+ end
174
+
175
+ def method_missing(symbol, *args)
176
+ @args << "-#{symbol}"
177
+ @args += args
178
+ end
179
+
180
+ def +(value)
181
+ @args << "+#{value}"
182
+ end
183
+ end
184
+ end
data/lib/thumbs/image.rb CHANGED
@@ -1,41 +1,36 @@
1
- require 'digest/sha1'
2
- module Thumbs
1
+ class Image
3
2
 
4
- class Image
3
+ attr_accessor :server, :path, :size, :url
5
4
 
6
- attr_accessor :server, :path, :size, :url
5
+ def initialize(params)
6
+ @size = params[:size] if params[:size] =~ /^\d+x\d+$/
7
+ @url = params[:original_url]
8
+ @thumbs_folder = params[:thumbs_folder]
9
+ end
7
10
 
8
- def initialize(params)
9
- @size = params[:size] if params[:size] =~ /^\d+x\d+$/
10
- @url = params[:original_url]
11
- @thumbs_folder = params[:thumbs_folder]
12
- end
11
+ def local_path(size)
12
+ File.join(@thumbs_folder, spread(sha(size+@url))) if @thumbs_folder
13
+ end
13
14
 
14
- def local_path(size)
15
- File.join(@thumbs_folder, spread(sha(size+@url))) if @thumbs_folder
16
- end
17
-
18
- def original_path
19
- local_path("original")
20
- end
21
-
22
- def resized_path
23
- local_path(@size)
24
- end
25
-
26
- def remote_url
27
- "http://#{@url}"
28
- end
29
-
30
- protected
31
- def sha(path)
32
- Digest::SHA1.hexdigest(path)
33
- end
34
-
35
- def spread(sha, n = 2)
36
- sha[2, 0] = "/"
37
- sha
38
- end
15
+ def original_path
16
+ local_path("original")
39
17
  end
40
18
 
19
+ def resized_path
20
+ local_path(@size)
21
+ end
22
+
23
+ def remote_url
24
+ "http://#{@url}"
25
+ end
26
+
27
+ protected
28
+ def sha(path)
29
+ Digest::SHA1.hexdigest(path)
30
+ end
31
+
32
+ def spread(sha, n = 2)
33
+ sha[2, 0] = "/"
34
+ sha
35
+ end
41
36
  end
@@ -0,0 +1,9 @@
1
+ require "tempfile"
2
+
3
+ module EventedMagick
4
+ class ImageTempFile < Tempfile
5
+ def make_tmpname(ext, n)
6
+ "mini_magick-#{Time.now.to_i}-#{$$}-#{rand(0x10000000)}"
7
+ end
8
+ end
9
+ end
@@ -1,16 +1,21 @@
1
- require 'rack/mime'
1
+ class ContentType
2
+ def initialize(app, default_content_type = "image/jpeg")
3
+ @app = app
4
+ @default_content_type = default_content_type
5
+ end
2
6
 
3
- module Thumbs
4
- class ContentType
5
- def initialize(app, default_content_type = "image/jpeg")
6
- @app = app
7
- @default_content_type = default_content_type
8
- end
9
-
10
- def call(env)
11
- status, headers, body = @app.call(env)
12
- headers['Content-Type'] = Rack::Mime.mime_type(File.extname(env["thumbs.remote_url"]), @default_content_type) if headers['Content-Type'].nil?
13
- [status, headers, body]
7
+ def call(env)
8
+ async_cb = env['async.callback']
9
+ env['async.callback'] = Proc.new do |status, headers, body|
10
+ async_cb.call(post_process(status, headers, body, env))
14
11
  end
12
+
13
+ status, headers, body = @app.call(env)
14
+ post_process(status, headers, body, env)
15
+ end
16
+
17
+ def post_process(status, headers, body, env)
18
+ headers['Content-Type'] = Rack::Mime.mime_type(File.extname(env["PATH_INFO"]), @default_content_type) if headers['Content-Type'].nil?
19
+ [status, headers, body]
15
20
  end
16
- end
21
+ end
data/lib/thumbs.rb CHANGED
@@ -1,74 +1,133 @@
1
1
  require "rubygems"
2
- require "rack"
3
- require "rack/contrib"
4
-
5
- module Thumbs
6
- autoload :Server, 'thumbs/server'
7
- autoload :Image, 'thumbs/image'
8
- autoload :NotFound, 'thumbs/middleware/not_found'
9
- autoload :ServerName, 'thumbs/middleware/server_name'
10
- autoload :CacheControl, 'thumbs/middleware/cache_control'
11
- autoload :Download, 'thumbs/middleware/download'
12
- autoload :Resize, 'thumbs/middleware/resize'
13
- autoload :Config, 'thumbs/middleware/config'
14
- autoload :CacheWrite, 'thumbs/middleware/cache_write'
15
- autoload :CacheRead, 'thumbs/middleware/cache_read'
16
- autoload :Logger, 'thumbs/middleware/logger'
17
- autoload :ContentType, 'thumbs/middleware/content_type'
2
+ require "bundler/setup"
3
+
4
+ require 'goliath'
5
+ require 'em-http'
6
+ require 'em-synchrony/em-http'
7
+ require 'digest/sha1'
8
+ require "em-files"
9
+ require 'rack/mime'
10
+ require 'lumberjack'
11
+
12
+ require File.join(File.dirname(__FILE__), '/thumbs/evented_magick')
13
+ require File.join(File.dirname(__FILE__), '/thumbs/image')
14
+ require File.join(File.dirname(__FILE__), '/thumbs/middleware/content_type')
15
+
16
+ class Thumbs < Goliath::API
18
17
 
19
- def self.new(args)
20
- options = {
21
- :thumbs_folder => false,
22
- :etag => true,
23
- :cache => true,
24
- :cache_original => true,
25
- :cache_control => {
26
- :ttl => 86400,
27
- :last_modified => true
28
- },
29
- :server_name => "Thumbs/0.0.6",
30
- :url_map => "/:size/:original_url",
31
- :image_not_found => File.join(File.dirname(__FILE__), "thumbs", "images", "image_not_found.jpg"),
32
- :runtime => false,
33
- :logfile => false
34
- }.merge!(args)
35
-
36
- Rack::Builder.new do
37
-
38
- use Rack::Runtime if options[:runtime]
39
-
40
- use Rack::Config do |env|
41
- env['thumbs.thumbs_folder'] = options[:thumbs_folder]
18
+ use ::Rack::Runtime
19
+
20
+ use Goliath::Rack::Heartbeat # respond to /status with 200, OK (monitoring, etc)
21
+ use ContentType
22
+
23
+ def initialize
24
+ @logger = Lumberjack::Logger.new("logs/thumbs.log", :time_format => "%Y-%m-%d %H:%M:%S", :roll => :daily, :flush_seconds => 5)
25
+ end
26
+
27
+ def response(env)
28
+ start_time = Time.now.to_f
29
+ log_message = []
30
+ url_pattern, keys = compile(env.url_map)
31
+ status, headers, boady = [-1, {}, ""]
32
+
33
+ if match = url_pattern.match(env['PATH_INFO'])
34
+ values = match.captures.to_a
35
+ params = keys.zip(values).inject({}) do |hash,(k,v)|
36
+ hash[k.to_sym] = v
37
+ hash
42
38
  end
43
-
44
- use Rack::ShowExceptions
45
-
46
- use Thumbs::Config, options[:url_map]
47
-
48
- use Thumbs::Logger, options[:logfile] if options[:logfile]
49
-
50
- use Thumbs::ServerName, options[:server_name] if options[:server_name]
51
- use Thumbs::CacheControl, options[:cache_control] if options[:cache_control]
52
- use Rack::ETag if options[:etag]
53
-
54
- use Thumbs::ContentType
55
-
56
- if options[:cache] && options[:thumbs_folder] && File.exist?(File.expand_path(options[:thumbs_folder]))
57
- use Thumbs::CacheRead, "resized"
58
- use Thumbs::CacheWrite, "resized"
39
+ image = Image.new(params.merge(:thumbs_folder => env.thumbs_folder))
40
+ log_message << image.url
41
+ log_message << image.size
42
+ else
43
+ log_message << "invalid_url"
44
+ return not_found
45
+ end
46
+
47
+ #cache_resized_read
48
+ if env.cache
49
+ begin
50
+ status, body = 200, File.read(image.resized_path)
51
+ log_message << "resized_cache"
52
+ write_log log_message, start_time
53
+ return [status, headers, body]
54
+ rescue Errno::ENOENT, IOError => e
59
55
  end
60
-
61
- use Thumbs::Resize
56
+ end
57
+
58
+ #cache_original_read
59
+ if env.cache_original
60
+ begin
61
+ status, body = 200, File.read(image.original_path)
62
+ log_message << "original_cache"
63
+ rescue Errno::ENOENT, IOError => e
64
+ end
65
+ end
62
66
 
63
- use Thumbs::CacheWrite, "original" if options[:cache_original] && options[:thumbs_folder] && File.exist?(File.expand_path(options[:thumbs_folder]))
67
+ if status < 200
68
+ #download
69
+ log_message << "download"
70
+ original_response = EM::HttpRequest.new(image.remote_url).get :connection_timeout => 5, :inactivity_timeout => 10, :redirects => 0
71
+ status = original_response.response_header.status
72
+ if status >= 200 && status < 300
73
+ status, body = 200, original_response.response
74
+ #cache_original_write
75
+ if cache_original
76
+ path = image.original_path
77
+ tries = 0
78
+ begin
79
+ EM::File.open(path, "wb") {|f| f.write(body) }
80
+ rescue Errno::ENOENT, IOError
81
+ Dir.mkdir(File.dirname(path), 0755)
82
+ retry if (tries += 1) == 1
83
+ end
84
+ end
85
+ else
86
+ status, heders, body = not_found
87
+ end
64
88
 
65
- use Thumbs::NotFound, options[:image_not_found] if options[:image_not_found] && File.exist?(File.expand_path(options[:image_not_found]))
89
+ end
66
90
 
67
- use Thumbs::CacheRead, "original" if options[:cache_original]
68
- use Thumbs::Download
91
+ #resize
92
+ if status > 0
93
+ thumb = EventedMagick::Image.from_blob(body)
94
+ thumb.resize image.size
95
+ body = thumb.to_blob
96
+ end
69
97
 
70
- run Thumbs::Server.new
98
+ #cache_resized_write
99
+ if env.cache && status >= 200 && status < 300
100
+ path = image.resized_path
101
+ tries = 0
102
+ begin
103
+ EM::File.open(path, "wb") {|f| f.write(body) }
104
+ rescue Errno::ENOENT, IOError
105
+ Dir.mkdir(File.dirname(path), 0755)
106
+ retry if (tries += 1) == 1
107
+ end
71
108
  end
109
+ write_log log_message, start_time
110
+ [status, headers, body]
72
111
  end
73
-
112
+
113
+ private
114
+
115
+ def compile(url_map)
116
+ keys = []
117
+ pattern = url_map.gsub(/((:\w+))/) do |match|
118
+ keys << $2[1..-1]
119
+ "(.+?)"
120
+ end
121
+ [/^#{pattern}$/, keys]
122
+ end
123
+
124
+ def not_found
125
+ [404, {}, File.read(File.expand_path("thumbs/images/image_not_found.jpg", __FILE__))]
126
+ end
127
+
128
+ def write_log(message, start_time)
129
+ message << "#{((Time.now.to_f - start_time)*1000).round}ms"
130
+ @logger.info message.join("\t")
131
+ end
132
+
74
133
  end
data/thumbs.gemspec CHANGED
@@ -5,13 +5,14 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{thumbs}
8
- s.version = "0.0.9"
8
+ s.version = "1.0.0.beta.1"
9
9
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
+ s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Marcin Ciunelis"]
12
- s.date = %q{2011-03-07}
12
+ s.date = %q{2011-04-12}
13
13
  s.description = %q{}
14
14
  s.email = %q{marcin.ciunelis@gmail.com}
15
+ s.executables = ["thumbs"]
15
16
  s.extra_rdoc_files = [
16
17
  "LICENSE.txt",
17
18
  "README.textile"
@@ -22,20 +23,15 @@ Gem::Specification.new do |s|
22
23
  "README.textile",
23
24
  "Rakefile",
24
25
  "VERSION",
26
+ "bin/thumbs",
27
+ "examples/config.god",
28
+ "examples/config.rb",
25
29
  "lib/thumbs.rb",
30
+ "lib/thumbs/evented_magick.rb",
26
31
  "lib/thumbs/image.rb",
32
+ "lib/thumbs/image_temp_file.rb",
27
33
  "lib/thumbs/images/image_not_found.jpg",
28
- "lib/thumbs/middleware/cache_control.rb",
29
- "lib/thumbs/middleware/cache_read.rb",
30
- "lib/thumbs/middleware/cache_write.rb",
31
- "lib/thumbs/middleware/config.rb",
32
34
  "lib/thumbs/middleware/content_type.rb",
33
- "lib/thumbs/middleware/download.rb",
34
- "lib/thumbs/middleware/logger.rb",
35
- "lib/thumbs/middleware/not_found.rb",
36
- "lib/thumbs/middleware/resize.rb",
37
- "lib/thumbs/middleware/server_name.rb",
38
- "lib/thumbs/server.rb",
39
35
  "spec/spec_helper.rb",
40
36
  "spec/thumbs_spec.rb",
41
37
  "thumbs.gemspec"
@@ -43,9 +39,10 @@ Gem::Specification.new do |s|
43
39
  s.homepage = %q{http://github.com/martinciu/thumbs}
44
40
  s.licenses = ["MIT"]
45
41
  s.require_paths = ["lib"]
46
- s.rubygems_version = %q{1.6.0}
47
- s.summary = %q{Image server proxy that can resize images on the fly. Built in ruby as a Rack application.}
42
+ s.rubygems_version = %q{1.7.2}
43
+ s.summary = %q{Image proxy server that can resize and cache images on the fly. Built in ruby with Goliath API.}
48
44
  s.test_files = [
45
+ "examples/config.rb",
49
46
  "spec/spec_helper.rb",
50
47
  "spec/thumbs_spec.rb"
51
48
  ]
@@ -54,26 +51,32 @@ Gem::Specification.new do |s|
54
51
  s.specification_version = 3
55
52
 
56
53
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
57
- s.add_runtime_dependency(%q<rack>, [">= 0"])
58
- s.add_runtime_dependency(%q<rack-contrib>, [">= 0"])
59
- s.add_runtime_dependency(%q<mini_magick>, [">= 0"])
54
+ s.add_runtime_dependency(%q<goliath>, [">= 0"])
55
+ s.add_runtime_dependency(%q<eventmachine>, [">= 1.0.0.beta.1"])
56
+ s.add_runtime_dependency(%q<em-synchrony>, [">= 0.3.0.beta.1"])
57
+ s.add_runtime_dependency(%q<em-http-request>, [">= 1.0.0.beta.1"])
58
+ s.add_runtime_dependency(%q<em-files>, [">= 0"])
60
59
  s.add_runtime_dependency(%q<lumberjack>, [">= 0"])
61
60
  s.add_development_dependency(%q<rspec>, [">= 0"])
62
61
  s.add_development_dependency(%q<bundler>, [">= 0"])
63
62
  s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
64
63
  else
65
- s.add_dependency(%q<rack>, [">= 0"])
66
- s.add_dependency(%q<rack-contrib>, [">= 0"])
67
- s.add_dependency(%q<mini_magick>, [">= 0"])
64
+ s.add_dependency(%q<goliath>, [">= 0"])
65
+ s.add_dependency(%q<eventmachine>, [">= 1.0.0.beta.1"])
66
+ s.add_dependency(%q<em-synchrony>, [">= 0.3.0.beta.1"])
67
+ s.add_dependency(%q<em-http-request>, [">= 1.0.0.beta.1"])
68
+ s.add_dependency(%q<em-files>, [">= 0"])
68
69
  s.add_dependency(%q<lumberjack>, [">= 0"])
69
70
  s.add_dependency(%q<rspec>, [">= 0"])
70
71
  s.add_dependency(%q<bundler>, [">= 0"])
71
72
  s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
72
73
  end
73
74
  else
74
- s.add_dependency(%q<rack>, [">= 0"])
75
- s.add_dependency(%q<rack-contrib>, [">= 0"])
76
- s.add_dependency(%q<mini_magick>, [">= 0"])
75
+ s.add_dependency(%q<goliath>, [">= 0"])
76
+ s.add_dependency(%q<eventmachine>, [">= 1.0.0.beta.1"])
77
+ s.add_dependency(%q<em-synchrony>, [">= 0.3.0.beta.1"])
78
+ s.add_dependency(%q<em-http-request>, [">= 1.0.0.beta.1"])
79
+ s.add_dependency(%q<em-files>, [">= 0"])
77
80
  s.add_dependency(%q<lumberjack>, [">= 0"])
78
81
  s.add_dependency(%q<rspec>, [">= 0"])
79
82
  s.add_dependency(%q<bundler>, [">= 0"])
metadata CHANGED
@@ -1,13 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thumbs
3
3
  version: !ruby/object:Gem::Version
4
- hash: 13
5
- prerelease:
6
- segments:
7
- - 0
8
- - 0
9
- - 9
10
- version: 0.0.9
4
+ prerelease: 6
5
+ version: 1.0.0.beta.1
11
6
  platform: ruby
12
7
  authors:
13
8
  - Marcin Ciunelis
@@ -15,113 +10,111 @@ autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
12
 
18
- date: 2011-03-07 00:00:00 +01:00
19
- default_executable:
13
+ date: 2011-04-12 00:00:00 Z
20
14
  dependencies:
21
15
  - !ruby/object:Gem::Dependency
16
+ name: goliath
22
17
  requirement: &id001 !ruby/object:Gem::Requirement
23
18
  none: false
24
19
  requirements:
25
20
  - - ">="
26
21
  - !ruby/object:Gem::Version
27
- hash: 3
28
- segments:
29
- - 0
30
22
  version: "0"
31
- version_requirements: *id001
32
- name: rack
33
- prerelease: false
34
23
  type: :runtime
24
+ prerelease: false
25
+ version_requirements: *id001
35
26
  - !ruby/object:Gem::Dependency
27
+ name: eventmachine
36
28
  requirement: &id002 !ruby/object:Gem::Requirement
37
29
  none: false
38
30
  requirements:
39
31
  - - ">="
40
32
  - !ruby/object:Gem::Version
41
- hash: 3
42
- segments:
43
- - 0
44
- version: "0"
45
- version_requirements: *id002
46
- name: rack-contrib
47
- prerelease: false
33
+ version: 1.0.0.beta.1
48
34
  type: :runtime
35
+ prerelease: false
36
+ version_requirements: *id002
49
37
  - !ruby/object:Gem::Dependency
38
+ name: em-synchrony
50
39
  requirement: &id003 !ruby/object:Gem::Requirement
51
40
  none: false
52
41
  requirements:
53
42
  - - ">="
54
43
  - !ruby/object:Gem::Version
55
- hash: 3
56
- segments:
57
- - 0
58
- version: "0"
59
- version_requirements: *id003
60
- name: mini_magick
61
- prerelease: false
44
+ version: 0.3.0.beta.1
62
45
  type: :runtime
46
+ prerelease: false
47
+ version_requirements: *id003
63
48
  - !ruby/object:Gem::Dependency
49
+ name: em-http-request
64
50
  requirement: &id004 !ruby/object:Gem::Requirement
65
51
  none: false
66
52
  requirements:
67
53
  - - ">="
68
54
  - !ruby/object:Gem::Version
69
- hash: 3
70
- segments:
71
- - 0
72
- version: "0"
73
- version_requirements: *id004
74
- name: lumberjack
75
- prerelease: false
55
+ version: 1.0.0.beta.1
76
56
  type: :runtime
57
+ prerelease: false
58
+ version_requirements: *id004
77
59
  - !ruby/object:Gem::Dependency
60
+ name: em-files
78
61
  requirement: &id005 !ruby/object:Gem::Requirement
79
62
  none: false
80
63
  requirements:
81
64
  - - ">="
82
65
  - !ruby/object:Gem::Version
83
- hash: 3
84
- segments:
85
- - 0
86
66
  version: "0"
87
- version_requirements: *id005
88
- name: rspec
67
+ type: :runtime
89
68
  prerelease: false
90
- type: :development
69
+ version_requirements: *id005
91
70
  - !ruby/object:Gem::Dependency
71
+ name: lumberjack
92
72
  requirement: &id006 !ruby/object:Gem::Requirement
93
73
  none: false
94
74
  requirements:
95
75
  - - ">="
96
76
  - !ruby/object:Gem::Version
97
- hash: 3
98
- segments:
99
- - 0
100
77
  version: "0"
78
+ type: :runtime
79
+ prerelease: false
101
80
  version_requirements: *id006
102
- name: bundler
81
+ - !ruby/object:Gem::Dependency
82
+ name: rspec
83
+ requirement: &id007 !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: "0"
89
+ type: :development
103
90
  prerelease: false
91
+ version_requirements: *id007
92
+ - !ruby/object:Gem::Dependency
93
+ name: bundler
94
+ requirement: &id008 !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: "0"
104
100
  type: :development
101
+ prerelease: false
102
+ version_requirements: *id008
105
103
  - !ruby/object:Gem::Dependency
106
- requirement: &id007 !ruby/object:Gem::Requirement
104
+ name: jeweler
105
+ requirement: &id009 !ruby/object:Gem::Requirement
107
106
  none: false
108
107
  requirements:
109
108
  - - ~>
110
109
  - !ruby/object:Gem::Version
111
- hash: 7
112
- segments:
113
- - 1
114
- - 5
115
- - 2
116
110
  version: 1.5.2
117
- version_requirements: *id007
118
- name: jeweler
119
- prerelease: false
120
111
  type: :development
112
+ prerelease: false
113
+ version_requirements: *id009
121
114
  description: ""
122
115
  email: marcin.ciunelis@gmail.com
123
- executables: []
124
-
116
+ executables:
117
+ - thumbs
125
118
  extensions: []
126
119
 
127
120
  extra_rdoc_files:
@@ -133,24 +126,18 @@ files:
133
126
  - README.textile
134
127
  - Rakefile
135
128
  - VERSION
129
+ - bin/thumbs
130
+ - examples/config.god
131
+ - examples/config.rb
136
132
  - lib/thumbs.rb
133
+ - lib/thumbs/evented_magick.rb
137
134
  - lib/thumbs/image.rb
135
+ - lib/thumbs/image_temp_file.rb
138
136
  - lib/thumbs/images/image_not_found.jpg
139
- - lib/thumbs/middleware/cache_control.rb
140
- - lib/thumbs/middleware/cache_read.rb
141
- - lib/thumbs/middleware/cache_write.rb
142
- - lib/thumbs/middleware/config.rb
143
137
  - lib/thumbs/middleware/content_type.rb
144
- - lib/thumbs/middleware/download.rb
145
- - lib/thumbs/middleware/logger.rb
146
- - lib/thumbs/middleware/not_found.rb
147
- - lib/thumbs/middleware/resize.rb
148
- - lib/thumbs/middleware/server_name.rb
149
- - lib/thumbs/server.rb
150
138
  - spec/spec_helper.rb
151
139
  - spec/thumbs_spec.rb
152
140
  - thumbs.gemspec
153
- has_rdoc: true
154
141
  homepage: http://github.com/martinciu/thumbs
155
142
  licenses:
156
143
  - MIT
@@ -164,26 +151,24 @@ required_ruby_version: !ruby/object:Gem::Requirement
164
151
  requirements:
165
152
  - - ">="
166
153
  - !ruby/object:Gem::Version
167
- hash: 3
154
+ hash: 758287189825552390
168
155
  segments:
169
156
  - 0
170
157
  version: "0"
171
158
  required_rubygems_version: !ruby/object:Gem::Requirement
172
159
  none: false
173
160
  requirements:
174
- - - ">="
161
+ - - ">"
175
162
  - !ruby/object:Gem::Version
176
- hash: 3
177
- segments:
178
- - 0
179
- version: "0"
163
+ version: 1.3.1
180
164
  requirements: []
181
165
 
182
166
  rubyforge_project:
183
- rubygems_version: 1.6.0
167
+ rubygems_version: 1.7.2
184
168
  signing_key:
185
169
  specification_version: 3
186
- summary: Image server proxy that can resize images on the fly. Built in ruby as a Rack application.
170
+ summary: Image proxy server that can resize and cache images on the fly. Built in ruby with Goliath API.
187
171
  test_files:
172
+ - examples/config.rb
188
173
  - spec/spec_helper.rb
189
174
  - spec/thumbs_spec.rb
@@ -1,17 +0,0 @@
1
- module Thumbs
2
- class CacheControl
3
- def initialize(app, options)
4
- @app = app
5
- @ttl = options[:ttl] || 86400
6
- @last_modified = options[:last_modified].nil? ? true : options[:last_modified]
7
- end
8
-
9
- def call(env)
10
- status, headers, body = @app.call(env)
11
- headers['Cache-Control'] ||= "max-age=#{@ttl}, public"
12
- headers['Expires'] ||= (Time.now + @ttl).httpdate
13
- headers['Last-Modified'] ||= Time.now.httpdate if @last_modified
14
- [status, headers, body]
15
- end
16
- end
17
- end
@@ -1,22 +0,0 @@
1
- module Thumbs
2
- class CacheRead
3
-
4
- def initialize(app, cache_type = "resized")
5
- @app = app
6
- @cache_type = cache_type
7
- end
8
-
9
- def call(env)
10
- if env["thumbs.#{@cache_type}_path"]
11
- begin
12
- status, headers, body = [200, {}, File.read(env["thumbs.#{@cache_type}_path"])]
13
- env['thumbs.logger'] << "#{@cache_type}_cache" if env['thumbs.logger']
14
- return [status, headers, body]
15
- rescue Errno::ENOENT, IOError => e
16
- end
17
- end
18
- return @app.call(env)
19
- end
20
- end
21
-
22
- end
@@ -1,32 +0,0 @@
1
- module Thumbs
2
- class CacheWrite
3
-
4
- def initialize(app, cache_type = "resized")
5
- @app = app
6
- @cache_type = cache_type
7
- end
8
-
9
- def call(env)
10
- status, headers, body = @app.call(env)
11
-
12
- path = env["thumbs.#{@cache_type}_path"]
13
-
14
- if path && status == 200
15
- tries = 0
16
- begin
17
- File.open(path, "wb") {|f| f.write(body) }
18
- rescue Errno::ENOENT, IOError
19
- Dir.mkdir(File.dirname(path), 0755)
20
- retry if (tries += 1) == 1
21
- end
22
- end
23
-
24
- [status, headers, body]
25
- end
26
-
27
- end
28
-
29
- end
30
-
31
-
32
-
@@ -1,41 +0,0 @@
1
- module Thumbs
2
- class Config
3
-
4
- def initialize(app, url_map)
5
- @app = app
6
- @url_pattern, @keys = compile(url_map)
7
- end
8
-
9
- def call(env)
10
- env['thumbs.url_pattern'] = @url_pattern
11
- if match = @url_pattern.match(env['PATH_INFO'])
12
- values = match.captures.to_a
13
- params = @keys.zip(values).inject({}) do |hash,(k,v)|
14
- hash[k.to_sym] = v
15
- hash
16
- end
17
-
18
- image = Image.new(params.merge(:thumbs_folder => env['thumbs.thumbs_folder']))
19
-
20
- env['thumbs.remote_url'] = image.remote_url
21
- env['thumbs.url'] = image.url
22
- env['thumbs.resized_path'] = image.resized_path
23
- env['thumbs.original_path'] = image.original_path
24
- env['thumbs.size'] = image.size
25
- end
26
- @app.call(env)
27
- end
28
-
29
- protected
30
- def compile(url_map)
31
- keys = []
32
- pattern = url_map.gsub(/((:\w+))/) do |match|
33
- keys << $2[1..-1]
34
- "(.+?)"
35
- end
36
- [/^#{pattern}$/, keys]
37
- end
38
-
39
- end
40
-
41
- end
@@ -1,27 +0,0 @@
1
- require 'open-uri'
2
-
3
- module Thumbs
4
- class Download
5
-
6
- def initialize(app)
7
- @app = app
8
- end
9
-
10
- def call(env)
11
- if env['thumbs.remote_url']
12
- begin
13
- status, headers, body = [200, {}, open(env['thumbs.remote_url']).read]
14
- env['thumbs.logger'] << "download" if env['thumbs.logger']
15
- return [status, headers, body]
16
- rescue StandardError, Timeout::Error => e
17
- status, headers, body = [404, {'Content-Type' => 'text/plain'}, ["Not found"]]
18
- env['thumbs.logger'] << "not_found" if env['thumbs.logger']
19
- return [status, headers, body]
20
- end
21
- else
22
- return @app.call(env)
23
- end
24
- end
25
-
26
- end
27
- end
@@ -1,25 +0,0 @@
1
- require 'lumberjack'
2
- require 'benchmark'
3
-
4
- module Thumbs
5
- class Logger
6
- def initialize(app, logfile_path)
7
- @logger = Lumberjack::Logger.new(logfile_path, :time_format => "%Y-%m-%d %H:%M:%S", :roll => :daily, :flush_seconds => 5)
8
- @app = app
9
- end
10
-
11
- def call(env)
12
- env['thumbs.logger'] = []
13
- env['thumbs.logger'] << env['thumbs.url']
14
- env['thumbs.logger'] << env['thumbs.size']
15
- response = []
16
- realtime = Benchmark.realtime do
17
- response = @app.call(env)
18
- end
19
- status, headers, body = response
20
- env['thumbs.logger'] << sprintf('%.4f', realtime)
21
- @logger.info env['thumbs.logger'].join("\t")
22
- return [status, headers, body]
23
- end
24
- end
25
- end
@@ -1,24 +0,0 @@
1
- module Thumbs
2
-
3
- class NotFound
4
- F = ::File
5
-
6
- def initialize(app, image_not_found)
7
- @app = app
8
- file = F.expand_path(image_not_found)
9
- @content = F.read(file)
10
- end
11
-
12
- def call(env)
13
- status, headers, body = @app.call(env)
14
- if status == 200
15
- [status, headers, body]
16
- else
17
- env['thumbs.logger'] << "not_found" if env['thumbs.logger']
18
- [404, {}, @content]
19
- end
20
- end
21
-
22
- end
23
-
24
- end
@@ -1,25 +0,0 @@
1
- require 'mini_magick'
2
-
3
- module Thumbs
4
- class Resize
5
-
6
- def initialize(app)
7
- @app = app
8
- end
9
-
10
- def call(env)
11
- status, headers, body = @app.call(env)
12
-
13
- if env['thumbs.size']
14
- begin
15
- thumb = MiniMagick::Image.read(body)
16
- thumb.resize env['thumbs.size']
17
- body = thumb.to_blob
18
- rescue
19
- end
20
- end
21
-
22
- [status, headers, body]
23
- end
24
- end
25
- end
@@ -1,14 +0,0 @@
1
- module Thumbs
2
- class ServerName
3
- def initialize(app, server_name)
4
- @app = app
5
- @server_name = server_name
6
- end
7
-
8
- def call(env)
9
- status, headers, body = @app.call(env)
10
- headers['Server'] = @server_name
11
- [status, headers, body]
12
- end
13
- end
14
- end
data/lib/thumbs/server.rb DELETED
@@ -1,16 +0,0 @@
1
- module Thumbs
2
-
3
- class Server
4
-
5
- def call(env)
6
- if env['PATH_INFO'] =~ env['thumbs.url_pattern']
7
- [200, {}, ["Found"]]
8
- else
9
- [404, {}, ["Not found"]]
10
- end
11
-
12
- end
13
-
14
- end
15
-
16
- end