thumbs 0.0.9 → 1.0.0.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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