anisoptera 0.0.2
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/.gitignore +6 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/README.md +116 -0
- data/Rakefile +8 -0
- data/anisoptera.gemspec +32 -0
- data/examples/http_router.ru +36 -0
- data/examples/pic1.jpg +0 -0
- data/lib/anisoptera.rb +32 -0
- data/lib/anisoptera/app.rb +29 -0
- data/lib/anisoptera/async_endpoint.rb +73 -0
- data/lib/anisoptera/commander.rb +148 -0
- data/lib/anisoptera/endpoint.rb +60 -0
- data/lib/anisoptera/error.png +0 -0
- data/lib/anisoptera/serializer.rb +35 -0
- data/lib/anisoptera/sync_endpoint.rb +21 -0
- data/lib/anisoptera/version.rb +3 -0
- data/spec/anisoptera_spec.rb +11 -0
- data/spec/async_endpoint_spec.rb +258 -0
- data/spec/commander_spec.rb +104 -0
- data/spec/files/Chile.gif +0 -0
- data/spec/files/test.gif +0 -0
- data/spec/serializer_spec.rb +22 -0
- data/spec/test_helper.rb +19 -0
- metadata +182 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
# Configurable Rack endpoint for Async image resizing.
|
2
|
+
|
3
|
+
In progress.
|
4
|
+
|
5
|
+
Borrows heavily from Dragonfly ([https://github.com/markevans/dragonfly](https://github.com/markevans/dragonfly)).
|
6
|
+
|
7
|
+
Async mode relies on Thin's async.callback env variable and EventMachine, and ImageMagick for image processing.
|
8
|
+
|
9
|
+
See [http_router](/ismasan/anisoptera/blob/master/examples/http_router.ru) example for an intro.
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
You need a rack router of some sort.
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
# image_resizer.ru
|
17
|
+
|
18
|
+
require 'http_router'
|
19
|
+
require 'anisoptera'
|
20
|
+
|
21
|
+
Anisoptera[:media].configure do |config|
|
22
|
+
# This is where your original image files are
|
23
|
+
config.base_path = './'
|
24
|
+
# In case of error, resize and serve up this image
|
25
|
+
config.error_image = './Error.gif'
|
26
|
+
# Run this block in case of error
|
27
|
+
config.on_error do |exception, params|
|
28
|
+
Airbrake.notify(
|
29
|
+
:error_class => exception.class.name,
|
30
|
+
:error_message => exception.message,
|
31
|
+
:parameters => params
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Create an app with defined routes
|
37
|
+
|
38
|
+
app = HttpRouter.new do
|
39
|
+
|
40
|
+
add('/media/:geometry/:color_mode/:filename').to Anisoptera[:media].endpoint {|image, params|
|
41
|
+
image.file(params[:filename]).thumb(params[:geometry])
|
42
|
+
image.greyscale if params[:color_mode] == 'grey'
|
43
|
+
image.encode('png')
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Run the app
|
48
|
+
run app
|
49
|
+
```
|
50
|
+
|
51
|
+
Run with Thin
|
52
|
+
|
53
|
+
$ thin start -R image_resizer.ru -e production -p 5000
|
54
|
+
|
55
|
+
Now you get on-the fly image resizes
|
56
|
+
|
57
|
+
http://some.host/media/100x100/grey/logo.png
|
58
|
+
|
59
|
+
Anisoptera returns all the right HTTP headers so if you put this behind a caching proxy such as Varnish it should just work.
|
60
|
+
|
61
|
+
## Custom headers
|
62
|
+
|
63
|
+
You can pass optional headers, or override default ones
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
Anisoptera[:media].configure do |config|
|
67
|
+
config.headers = {
|
68
|
+
'Cache-Control' => '1234567890',
|
69
|
+
'X-Custom' => 'Hello there'
|
70
|
+
}
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
## DoS protection
|
75
|
+
|
76
|
+
Obviously it's a bad idea to allow people to freely resize images on the fly as it might bring your servers down. You can hash the parameters in the URL with a shared key and secret, something like:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
get('/media/:hash').to Anisoptera[:media].endpoint {|image, params|
|
80
|
+
verify_sha! params[:hash], params[:k]
|
81
|
+
|
82
|
+
args = Anisoptera::Serializer.marshal_decode(params[:hash])
|
83
|
+
image_path = args[:file_name]
|
84
|
+
image.file(image_path).thumb(args[:geometry])
|
85
|
+
image.greyscale if args[:grey]
|
86
|
+
image.encode('jpg')
|
87
|
+
}
|
88
|
+
```
|
89
|
+
|
90
|
+
Then you request images with passing a hash of parameters encoded with the shared secret, and a public key to decode it back.
|
91
|
+
|
92
|
+
http://some.host/media/BAh7CToGZiIVMjUzNjctaGVsbWV0LmpwZzoJZ3JleUY6DHNob3BfaWRpAeA6BmciDDIwMHgyMDA?k=6a58f9458425f73
|
93
|
+
|
94
|
+
Anisoptera::Serializer's encode and decode methods can help you Base64-encode a hash of parameters. This is also good because some ImageMagick geometry strings are not valid URL components.
|
95
|
+
|
96
|
+
# LICENSE
|
97
|
+
|
98
|
+
Copyright (C) 2011 Ismael Celis
|
99
|
+
|
100
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
101
|
+
this software and associated documentation files (the "Software"), to deal in
|
102
|
+
the Software without restriction, including without limitation the rights to
|
103
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
104
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
105
|
+
so, subject to the following conditions:
|
106
|
+
|
107
|
+
The above copyright notice and this permission notice shall be included in all
|
108
|
+
copies or substantial portions of the Software.
|
109
|
+
|
110
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
111
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
112
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
113
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
114
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
115
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
116
|
+
SOFTWARE.
|
data/Rakefile
ADDED
data/anisoptera.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "anisoptera/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "anisoptera"
|
7
|
+
s.version = Anisoptera::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Ismael Celis"]
|
10
|
+
s.email = ["ismaelct@gmail.com"]
|
11
|
+
s.homepage = ""
|
12
|
+
s.summary = %q{Async Rack app for image thumbnailing}
|
13
|
+
s.description = %q{You'll need an Eventmachine server such as Thin to run this. See README.'}
|
14
|
+
|
15
|
+
s.rubyforge_project = "anisoptera"
|
16
|
+
|
17
|
+
s.add_dependency 'eventmachine', ">= 0.12.10"
|
18
|
+
s.add_dependency 'thin'
|
19
|
+
s.add_dependency 'thin_async'
|
20
|
+
s.add_dependency 'rack', ">= 1.2.2"
|
21
|
+
|
22
|
+
s.add_development_dependency "bundler", ">= 1.0.0"
|
23
|
+
s.add_development_dependency "rack-test"
|
24
|
+
s.add_development_dependency "rspec"
|
25
|
+
s.add_development_dependency "thin-async-test"
|
26
|
+
s.add_development_dependency "http_router"
|
27
|
+
|
28
|
+
s.files = `git ls-files`.split("\n")
|
29
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
30
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
31
|
+
s.require_paths = ["lib"]
|
32
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# thin start -R http_router -p 3000
|
2
|
+
#
|
3
|
+
# http://localhost:3000/media/170x240+210+210/m/pic1.jpg
|
4
|
+
# http://localhost:3000/media/170x240/m/pic1.jpg
|
5
|
+
# http://localhost:3000/media/100x100-ne/m/pic1.jpg
|
6
|
+
# http://localhost:3000/media/100x100-c/m/pic1.jpg
|
7
|
+
# http://localhost:3000/media/100x100-c/greyscale/pic1.jpg
|
8
|
+
|
9
|
+
require 'rubygems'
|
10
|
+
# require 'bundler'
|
11
|
+
# Bundler.setup
|
12
|
+
|
13
|
+
require 'http_router'
|
14
|
+
|
15
|
+
$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
16
|
+
|
17
|
+
require 'anisoptera'
|
18
|
+
|
19
|
+
Anisoptera[:media].configure do |config|
|
20
|
+
config.base_path = './'
|
21
|
+
end
|
22
|
+
|
23
|
+
# Anisoptera.prefer_async = false # uncomment this to test synchronous endpoint with any Rack server
|
24
|
+
|
25
|
+
|
26
|
+
routes = HttpRouter.new do
|
27
|
+
|
28
|
+
add('/media/:geometry/:color_mode/:filename').to Anisoptera[:media].endpoint {|image, params|
|
29
|
+
image.file(params[:filename]).thumb(params[:geometry])
|
30
|
+
image.greyscale if params[:color_mode] == 'grey'
|
31
|
+
image.encode('png')
|
32
|
+
}
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
run routes
|
data/examples/pic1.jpg
ADDED
Binary file
|
data/lib/anisoptera.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'rack/mime'
|
3
|
+
require 'anisoptera/commander'
|
4
|
+
require 'anisoptera/serializer'
|
5
|
+
require 'anisoptera/version'
|
6
|
+
require 'anisoptera/app'
|
7
|
+
require 'anisoptera/endpoint'
|
8
|
+
|
9
|
+
module Anisoptera
|
10
|
+
|
11
|
+
HEADERS = {
|
12
|
+
'Cache-Control' => 'public, max-age=3153600'
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
@apps = {}
|
16
|
+
@prefer_async = true
|
17
|
+
|
18
|
+
def prefer_async=(bool)
|
19
|
+
@prefer_async = bool
|
20
|
+
end
|
21
|
+
|
22
|
+
def prefer_async
|
23
|
+
!!@prefer_async
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](app_name)
|
27
|
+
@apps[app_name] ||= Anisoptera::App.new
|
28
|
+
end
|
29
|
+
|
30
|
+
extend self
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Anisoptera
|
2
|
+
|
3
|
+
class Config < OpenStruct
|
4
|
+
|
5
|
+
def on_error(&block)
|
6
|
+
@on_error = block if block_given?
|
7
|
+
@on_error
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
class App
|
13
|
+
|
14
|
+
attr_reader :config
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@config = Config.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def configure(&block)
|
21
|
+
block.call @config
|
22
|
+
end
|
23
|
+
|
24
|
+
def endpoint(&block)
|
25
|
+
Anisoptera::Endpoint.factory(@config, block)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'thin/async'
|
3
|
+
|
4
|
+
module Anisoptera
|
5
|
+
|
6
|
+
class AsyncEndpoint
|
7
|
+
|
8
|
+
include Anisoptera::Endpoint
|
9
|
+
|
10
|
+
# This is a template async response.
|
11
|
+
AsyncResponse = [-1, {}, []].freeze
|
12
|
+
|
13
|
+
STATUSES = {
|
14
|
+
0 => 200,
|
15
|
+
1 => 500,
|
16
|
+
255 => 200 # exit status represented differently when running tests in editor
|
17
|
+
}
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
response = Thin::AsyncResponse.new(env)
|
21
|
+
|
22
|
+
params = routing_params(env)
|
23
|
+
|
24
|
+
begin
|
25
|
+
job = Anisoptera::Commander.new( @config.base_path, @config.convert_command )
|
26
|
+
convert = @handler.call(job, params)
|
27
|
+
response.headers.update(update_headers(convert))
|
28
|
+
|
29
|
+
if !job.check_file
|
30
|
+
response.headers['X-Error'] = 'Image not found'
|
31
|
+
handle_error error_status(404), response, convert
|
32
|
+
else
|
33
|
+
handle_success response, convert
|
34
|
+
end
|
35
|
+
rescue => boom
|
36
|
+
response.headers['X-Error'] = boom.message
|
37
|
+
response.headers.update(update_headers)
|
38
|
+
@config.on_error.call(boom, params, env) if @config.on_error
|
39
|
+
handle_error(error_status(500), response)
|
40
|
+
end
|
41
|
+
|
42
|
+
response.finish
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def handle_success(response, convert)
|
48
|
+
EM.system( convert.command ){ |output, status|
|
49
|
+
http_status = STATUSES[status.exitstatus]
|
50
|
+
response.status = http_status
|
51
|
+
r = http_status == 200 ? output : 'SERVER ERROR'
|
52
|
+
response << r
|
53
|
+
response.done
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_error(status, response, convert = nil)
|
58
|
+
response.status = status
|
59
|
+
response.headers['Content-Type'] = Rack::Mime.mime_type(::File.extname(error_image))
|
60
|
+
if convert # pass error image through original IM command
|
61
|
+
EM.system( convert.command(error_image) ){ |output, status|
|
62
|
+
response << output
|
63
|
+
response.done
|
64
|
+
}
|
65
|
+
else # just blocking read because user handler blew up
|
66
|
+
response << ::File.read(error_image)
|
67
|
+
response.done
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module Anisoptera
|
2
|
+
|
3
|
+
# Simple interface to ImageMagick main commands
|
4
|
+
# Most of it borrowed from DragonFly
|
5
|
+
# https://github.com/markevans/dragonfly/blob/master/lib/dragonfly/image_magick/processor.rb
|
6
|
+
#
|
7
|
+
class Commander
|
8
|
+
|
9
|
+
GRAVITIES = {
|
10
|
+
'nw' => 'NorthWest',
|
11
|
+
'n' => 'North',
|
12
|
+
'ne' => 'NorthEast',
|
13
|
+
'w' => 'West',
|
14
|
+
'c' => 'Center',
|
15
|
+
'e' => 'East',
|
16
|
+
'sw' => 'SouthWest',
|
17
|
+
's' => 'South',
|
18
|
+
'se' => 'SouthEast'
|
19
|
+
}
|
20
|
+
|
21
|
+
# Geometry string patterns
|
22
|
+
RESIZE_GEOMETRY = /^\d*x\d*[><%^!]?$|^\d+@$/ # e.g. '300x200!'
|
23
|
+
CROPPED_RESIZE_GEOMETRY = /^(\d+)x(\d+)[#|-](\w{1,2})?$/ # e.g. '20x50#ne'
|
24
|
+
CROP_GEOMETRY = /^(\d+)x(\d+)([+-]\d+)?([+-]\d+)?(\w{1,2})?$/ # e.g. '30x30+10+10'
|
25
|
+
|
26
|
+
def initialize(base_path, convert_command = nil)
|
27
|
+
@convert_command = convert_command || 'convert'
|
28
|
+
@base_path = base_path
|
29
|
+
@original = nil
|
30
|
+
@geometry = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def file(path)
|
34
|
+
@original = path
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def encode(format)
|
39
|
+
@encode = format
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def square(size)
|
44
|
+
size = size.to_i
|
45
|
+
d = size * 2
|
46
|
+
@square = "-thumbnail x#{d} -resize '#{d}x<' -resize 50% -gravity center -crop #{size}x#{size}+0+0"
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def greyscale
|
51
|
+
@greyscale = '-colorspace Gray'
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def thumb(geometry)
|
56
|
+
@geometry = case geometry
|
57
|
+
when RESIZE_GEOMETRY
|
58
|
+
resize(geometry)
|
59
|
+
when CROPPED_RESIZE_GEOMETRY
|
60
|
+
resize_and_crop(:width => $1, :height => $2, :gravity => $3)
|
61
|
+
when CROP_GEOMETRY
|
62
|
+
crop(
|
63
|
+
:width => $1,
|
64
|
+
:height => $2,
|
65
|
+
:x => $3,
|
66
|
+
:y => $4,
|
67
|
+
:gravity => $5
|
68
|
+
)
|
69
|
+
else raise ArgumentError, "Didn't recognise the geometry string #{geometry}."
|
70
|
+
end
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def mime_type
|
75
|
+
ext = @encode ? ".#{@encode}" : File.extname(@original)
|
76
|
+
Rack::Mime.mime_type ext
|
77
|
+
end
|
78
|
+
|
79
|
+
# Utility method to test that all commands are working
|
80
|
+
#
|
81
|
+
def to_file(path)
|
82
|
+
File.open(path, 'w+') do |f|
|
83
|
+
f.write(`#{command}`)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def command(file_path = nil)
|
88
|
+
raise ArgumentError, "no original file provided. Do commander.file('some_file.jpg')" unless @original
|
89
|
+
file_path ||= full_file_path
|
90
|
+
cmd = []
|
91
|
+
cmd << @geometry if @geometry
|
92
|
+
cmd << @greyscale if @greyscale
|
93
|
+
cmd << @square if @square # should clear command
|
94
|
+
if @encode
|
95
|
+
cmd << "#{@encode}:-" # to stdout
|
96
|
+
else
|
97
|
+
cmd << '-'
|
98
|
+
end
|
99
|
+
|
100
|
+
"#{@convert_command} #{file_path} " + cmd.join(' ')
|
101
|
+
end
|
102
|
+
|
103
|
+
def check_file
|
104
|
+
::File.exists?(full_file_path)
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def full_file_path
|
110
|
+
File.join(@base_path, @original)
|
111
|
+
end
|
112
|
+
|
113
|
+
def resize(geometry)
|
114
|
+
"-resize \"#{geometry}\""
|
115
|
+
end
|
116
|
+
|
117
|
+
def crop(opts={})
|
118
|
+
width = opts[:width]
|
119
|
+
height = opts[:height]
|
120
|
+
gravity = GRAVITIES[opts[:gravity]]
|
121
|
+
x = "#{opts[:x] || 0}"
|
122
|
+
x = '+' + x unless x[/^[+-]/]
|
123
|
+
y = "#{opts[:y] || 0}"
|
124
|
+
y = '+' + y unless y[/^[+-]/]
|
125
|
+
repage = opts[:repage] == false ? '' : '+repage'
|
126
|
+
resize = opts[:resize]
|
127
|
+
|
128
|
+
"#{"-resize #{resize} " if resize}#{"-gravity #{gravity} " if gravity}-crop #{width}x#{height}#{x}#{y} #{repage}"
|
129
|
+
end
|
130
|
+
|
131
|
+
def resize_and_crop(opts={})
|
132
|
+
if !opts[:width] && !opts[:height]
|
133
|
+
return self
|
134
|
+
elsif !opts[:width] || !opts[:height]
|
135
|
+
attrs = identify(temp_object)
|
136
|
+
opts[:width] ||= attrs[:width]
|
137
|
+
opts[:height] ||= attrs[:height]
|
138
|
+
end
|
139
|
+
|
140
|
+
opts[:gravity] ||= 'c'
|
141
|
+
|
142
|
+
opts[:resize] = "#{opts[:width]}x#{opts[:height]}^^"
|
143
|
+
crop(opts)
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Anisoptera
|
2
|
+
|
3
|
+
module Endpoint
|
4
|
+
|
5
|
+
def self.factory(config, block)
|
6
|
+
if defined?(EventMachine) && Anisoptera.prefer_async
|
7
|
+
require 'anisoptera/async_endpoint'
|
8
|
+
AsyncEndpoint.new(config, &block)
|
9
|
+
else
|
10
|
+
require 'anisoptera/sync_endpoint'
|
11
|
+
SyncEndpoint.new(config, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(config, &block)
|
17
|
+
@config = config
|
18
|
+
@handler = block
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Borrowed from Dragonfly
|
24
|
+
#
|
25
|
+
def routing_params(env)
|
26
|
+
pars = env['rack.routing_args'] ||
|
27
|
+
env['action_dispatch.request.path_parameters'] ||
|
28
|
+
env['router.params'] ||
|
29
|
+
env['usher.params'] ||
|
30
|
+
raise(ArgumentError, "couldn't find any routing parameters in env #{env.inspect}")
|
31
|
+
|
32
|
+
# http_router doesn't parse querystring! Let's make sure we do
|
33
|
+
query = Rack::Utils.parse_nested_query(env["QUERY_STRING"]).inject({}) do |mem, (k,v)|
|
34
|
+
mem[k.to_sym] = v
|
35
|
+
mem
|
36
|
+
end
|
37
|
+
pars.update(query)
|
38
|
+
end
|
39
|
+
|
40
|
+
def update_headers(commander = nil)
|
41
|
+
heads = Anisoptera::HEADERS.dup.update(
|
42
|
+
'X-Generator' => self.class.name,
|
43
|
+
'Last-Modified' => Time.now.gmtime.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
44
|
+
)
|
45
|
+
heads.update(@config.headers) if @config.headers && @config.headers.is_a?(Hash)
|
46
|
+
heads['Content-Type'] = commander.mime_type if commander
|
47
|
+
heads
|
48
|
+
end
|
49
|
+
|
50
|
+
def error_image
|
51
|
+
@config.error_image || ::File.join(File.dirname(__FILE__), 'error.png')
|
52
|
+
end
|
53
|
+
|
54
|
+
def error_status(status)
|
55
|
+
@config.error_status || status
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
Binary file
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
# Stolen fr... I mean 'borrowed' from DragonFly
|
5
|
+
# https://github.com/markevans/dragonfly/blob/master/lib/dragonfly/serializer.rb
|
6
|
+
#
|
7
|
+
module Anisoptera
|
8
|
+
module Serializer
|
9
|
+
|
10
|
+
# Exceptions
|
11
|
+
class BadString < RuntimeError; end
|
12
|
+
|
13
|
+
extend self # So we can do Serializer.b64_encode, etc.
|
14
|
+
|
15
|
+
def b64_encode(string)
|
16
|
+
Base64.encode64(string).tr("\n=",'')
|
17
|
+
end
|
18
|
+
|
19
|
+
def b64_decode(string)
|
20
|
+
padding_length = string.length % 4
|
21
|
+
Base64.decode64(string + '=' * padding_length)
|
22
|
+
end
|
23
|
+
|
24
|
+
def marshal_encode(object)
|
25
|
+
b64_encode(Marshal.dump(object))
|
26
|
+
end
|
27
|
+
|
28
|
+
def marshal_decode(string)
|
29
|
+
Marshal.load(b64_decode(string))
|
30
|
+
rescue TypeError, ArgumentError => e
|
31
|
+
raise BadString, "couldn't decode #{string} - got #{e}"
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Anisoptera
|
2
|
+
|
3
|
+
class SyncEndpoint
|
4
|
+
|
5
|
+
include Anisoptera::Endpoint
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
params = routing_params(env)
|
9
|
+
job = Anisoptera::Commander.new( @config.base_path )
|
10
|
+
convert = @handler.call(job, params)
|
11
|
+
|
12
|
+
result = `#{convert.command}`
|
13
|
+
|
14
|
+
headers = update_headers(convert)
|
15
|
+
|
16
|
+
[200, headers, [result]]
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,258 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require File.dirname(__FILE__) + '/test_helper'
|
3
|
+
|
4
|
+
require 'thin'
|
5
|
+
require 'thin/async'
|
6
|
+
require 'thin/async/test'
|
7
|
+
require 'rack/test'
|
8
|
+
require 'http_router'
|
9
|
+
require "base64"
|
10
|
+
|
11
|
+
$HERE = File.dirname(__FILE__)
|
12
|
+
|
13
|
+
$CONVERT = "/usr/local/bin/convert"
|
14
|
+
|
15
|
+
describe 'Anisoptera::AsyncEndpoint' do
|
16
|
+
include Rack::Test::Methods
|
17
|
+
|
18
|
+
def app
|
19
|
+
Thin::Async::Test.new(@app)
|
20
|
+
end
|
21
|
+
|
22
|
+
before do
|
23
|
+
# redefine config each time to reset app
|
24
|
+
Anisoptera[:media].configure do |config|
|
25
|
+
config.base_path = File.join($HERE, 'files')
|
26
|
+
config.error_status = nil
|
27
|
+
config.error_image = nil
|
28
|
+
config.convert_command = $CONVERT
|
29
|
+
end
|
30
|
+
|
31
|
+
@app = HttpRouter.new do
|
32
|
+
add('/:g/:file').to Anisoptera[:media].endpoint {|image, params|
|
33
|
+
image_path = params[:file]
|
34
|
+
image.file(image_path).thumb(params[:g])
|
35
|
+
image.greyscale if params[:grey]
|
36
|
+
image.encode('jpg')
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
shared_examples_for "a cached image" do |status, content_type|
|
42
|
+
|
43
|
+
it 'should have status 200' do
|
44
|
+
last_response.status.should == status
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should have long-lived headers' do
|
48
|
+
last_response.headers["Cache-Control"].should == "public, max-age=3153600"
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should return Last-Modified header' do
|
52
|
+
last_response.headers['Last-Modified'].should == @the_time
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should return encoded content-type' do
|
56
|
+
last_response.headers['Content-Type'].should == content_type
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
shared_examples_for 'resized image data' do |test_image, geometry, content_type|
|
61
|
+
it 'should return resized image data' do
|
62
|
+
tempimg = File.join($HERE, 'files', "temp.#{content_type}")
|
63
|
+
Base64.encode64(`#{$CONVERT} #{test_image} -resize \"#{geometry}\" #{tempimg}`)
|
64
|
+
Base64.encode64(last_response.body).should == Base64.encode64(File.read(tempimg))
|
65
|
+
File.unlink(tempimg)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe 'success' do
|
70
|
+
before do
|
71
|
+
@the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
|
72
|
+
get '/20x20/test.gif'
|
73
|
+
end
|
74
|
+
|
75
|
+
it_behaves_like 'a cached image', 200, 'image/jpeg'
|
76
|
+
|
77
|
+
it_behaves_like 'resized image data', File.join($HERE,'files','test.gif'), '20x20', 'jpg'
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
describe 'with querystring' do
|
82
|
+
before do
|
83
|
+
@the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
|
84
|
+
get '/20x20/missing.gif?file=test.gif'
|
85
|
+
end
|
86
|
+
|
87
|
+
it_behaves_like 'a cached image', 200, 'image/jpeg'
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
describe 'with custom headers' do
|
92
|
+
before do
|
93
|
+
# redefine config each time to reset app
|
94
|
+
Anisoptera[:custom].configure do |config|
|
95
|
+
config.base_path = File.join($HERE, 'files')
|
96
|
+
config.headers = {
|
97
|
+
'Cache-Control' => '1234567890',
|
98
|
+
'X-Custom' => 'custom-head'
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
@app = HttpRouter.new do
|
103
|
+
add('/:g/:file').to Anisoptera[:custom].endpoint {|image, params|
|
104
|
+
image_path = params[:file]
|
105
|
+
image.file(image_path).thumb(params[:g])
|
106
|
+
image.encode('jpg')
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'should return encoded content-type' do
|
111
|
+
last_response.headers['Content-Type'].should == 'image/jpg'
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'should overwrite passed headers' do
|
115
|
+
last_response.headers['Cache-Control'].should == '1234567890'
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'should add passed new headers' do
|
119
|
+
last_response.headers['X-Custom'].should == 'custom-head'
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe 'missing image' do
|
126
|
+
before do
|
127
|
+
@the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
|
128
|
+
get '/20x20/testfoo.gif'
|
129
|
+
end
|
130
|
+
|
131
|
+
it_behaves_like 'a cached image', 404, 'image/png'
|
132
|
+
|
133
|
+
it_behaves_like 'resized image data', File.join($HERE,'..','lib','anisoptera', 'error.png'), '20x20', 'jpg'
|
134
|
+
end
|
135
|
+
|
136
|
+
describe 'missing image with error image configured' do
|
137
|
+
before do
|
138
|
+
@the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
|
139
|
+
Anisoptera[:media].config.error_image = File.join($HERE, 'files', 'Chile.gif')
|
140
|
+
get '/20x20/testfoo.gif'
|
141
|
+
end
|
142
|
+
|
143
|
+
it_behaves_like 'a cached image', 404, 'image/gif'
|
144
|
+
|
145
|
+
it_behaves_like 'resized image data', File.join($HERE,'files', 'Chile.gif'), '20x20', 'jpg'
|
146
|
+
end
|
147
|
+
|
148
|
+
describe 'with malformed geometry' do
|
149
|
+
before do
|
150
|
+
@the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
|
151
|
+
get '/20x20wtf/test.gif'
|
152
|
+
end
|
153
|
+
|
154
|
+
it_behaves_like 'a cached image', 500, 'image/png'
|
155
|
+
|
156
|
+
it 'should set X-Error header' do
|
157
|
+
last_response.headers['X-Error'].should == "Didn't recognise the geometry string 20x20wtf."
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe 'with exceptions in custom block' do
|
162
|
+
|
163
|
+
before do
|
164
|
+
@the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
|
165
|
+
|
166
|
+
@app = HttpRouter.new do
|
167
|
+
add('/:g/:file').to Anisoptera[:media].endpoint {|image, params|
|
168
|
+
raise 'Oops!'
|
169
|
+
}
|
170
|
+
end
|
171
|
+
|
172
|
+
get '/20x20/test.gif'
|
173
|
+
end
|
174
|
+
|
175
|
+
it_behaves_like 'a cached image', 500, 'image/png'
|
176
|
+
|
177
|
+
it 'should set X-Error header' do
|
178
|
+
last_response.headers['X-Error'].should == 'Oops!'
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
describe 'with exceptions and custom config.error_status' do
|
184
|
+
before do
|
185
|
+
@the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
|
186
|
+
Anisoptera[:media].config.error_status = 200
|
187
|
+
|
188
|
+
get '/20x20/testfoo.gif'
|
189
|
+
end
|
190
|
+
|
191
|
+
it_behaves_like 'a cached image', 200, 'image/png'
|
192
|
+
|
193
|
+
it 'should set X-Error header' do
|
194
|
+
last_response.headers['X-Error'].should == 'Image not found'
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
198
|
+
|
199
|
+
describe 'with exceptions in custom block and custom error_status' do
|
200
|
+
|
201
|
+
before do
|
202
|
+
Anisoptera[:media].config.error_status = 200
|
203
|
+
@the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
|
204
|
+
|
205
|
+
@app = HttpRouter.new do
|
206
|
+
add('/:g/:file').to Anisoptera[:media].endpoint {|image, params|
|
207
|
+
raise 'Oops!'
|
208
|
+
}
|
209
|
+
end
|
210
|
+
|
211
|
+
get '/20x20/test.gif'
|
212
|
+
end
|
213
|
+
|
214
|
+
it_behaves_like 'a cached image', 200, 'image/png'
|
215
|
+
|
216
|
+
it 'should set X-Error header' do
|
217
|
+
last_response.headers['X-Error'].should == 'Oops!'
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
|
222
|
+
describe 'with exceptions and config.on_error block' do
|
223
|
+
|
224
|
+
before do
|
225
|
+
@the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
|
226
|
+
|
227
|
+
@error_message = ''
|
228
|
+
@error_params = nil
|
229
|
+
@env = nil
|
230
|
+
Anisoptera[:media].config.on_error do |exception, params, env|
|
231
|
+
@error_message = 'Oops!'
|
232
|
+
@error_params = params
|
233
|
+
@env = env
|
234
|
+
end
|
235
|
+
|
236
|
+
@app = HttpRouter.new do
|
237
|
+
add('/:gg/:file').to Anisoptera[:media].endpoint {|image, params|
|
238
|
+
raise 'Oops!'
|
239
|
+
}
|
240
|
+
end
|
241
|
+
|
242
|
+
get '/20x20/test.gif'
|
243
|
+
end
|
244
|
+
|
245
|
+
it 'should have called error block' do
|
246
|
+
@error_message.should == 'Oops!'
|
247
|
+
@error_params.should == {:gg => '20x20', :file => 'test.gif'}
|
248
|
+
@env['SCRIPT_NAME'].should == '/20x20/test.gif'
|
249
|
+
end
|
250
|
+
|
251
|
+
it_behaves_like 'a cached image', 500, 'image/png'
|
252
|
+
|
253
|
+
it 'should set X-Error header' do
|
254
|
+
last_response.headers['X-Error'].should == 'Oops!'
|
255
|
+
end
|
256
|
+
|
257
|
+
end
|
258
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
describe Anisoptera::Commander do
|
4
|
+
|
5
|
+
describe 'building file path' do
|
6
|
+
|
7
|
+
before do
|
8
|
+
@commander = Anisoptera::Commander.new('/data')
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'must raise error if no file name given' do
|
12
|
+
proc {
|
13
|
+
@commander.command
|
14
|
+
}.should raise_error(ArgumentError)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'must correct missing slashes' do
|
18
|
+
@commander.file('foo.jpg').command.should match(/^convert \/data\/foo\.jpg/)
|
19
|
+
@commander.file('/foo.jpg').command.should match(/^convert \/data\/foo\.jpg/)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'commands' do
|
24
|
+
before do
|
25
|
+
@commander = Anisoptera::Commander.new('/data').file('foo.jpg')
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'resize' do
|
29
|
+
it 'should add resize command' do
|
30
|
+
@commander.thumb('10x10').command.should == "convert /data/foo.jpg -resize \"10x10\" -"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'crop' do
|
35
|
+
it 'should add resize command' do
|
36
|
+
@commander.thumb('120x120+10+5').command.should == "convert /data/foo.jpg -crop 120x120+10+5 +repage -"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'scale' do
|
41
|
+
it 'should add scale resize' do
|
42
|
+
@commander.thumb('50x50%').command.should == "convert /data/foo.jpg -resize \"50x50%\" -"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe 'bigger / smaller than' do
|
47
|
+
it 'should add < or >' do
|
48
|
+
@commander.thumb('400x300<').command.should == "convert /data/foo.jpg -resize \"400x300<\" -"
|
49
|
+
@commander.thumb('400x300>').command.should == "convert /data/foo.jpg -resize \"400x300>\" -"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe 'positioning (gravity)' do
|
54
|
+
it 'should resize with gravity' do
|
55
|
+
@commander.thumb('400x300#ne').command.should == "convert /data/foo.jpg -resize 400x300^^ -gravity NorthEast -crop 400x300+0+0 +repage -"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe 'encode' do
|
60
|
+
it 'should add format arguments' do
|
61
|
+
@commander.encode('gif').command.should == 'convert /data/foo.jpg gif:-'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe 'greyscale' do
|
66
|
+
it 'should add colorspace argument' do
|
67
|
+
@commander.greyscale.command.should == 'convert /data/foo.jpg -colorspace Gray -'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe 'combined' do
|
72
|
+
|
73
|
+
it 'should combine commands' do
|
74
|
+
@commander.
|
75
|
+
thumb('100x100#ne').
|
76
|
+
encode('png').
|
77
|
+
command.should == "convert /data/foo.jpg -resize 100x100^^ -gravity NorthEast -crop 100x100+0+0 +repage png:-"
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'should accept - as gravity separator because the hash breaks browsers' do
|
81
|
+
@commander.
|
82
|
+
thumb('100x100-ne').
|
83
|
+
command.should == "convert /data/foo.jpg -resize 100x100^^ -gravity NorthEast -crop 100x100+0+0 +repage -"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
describe 'mime_type' do
|
90
|
+
before do
|
91
|
+
@commander = Anisoptera::Commander.new('/data').file('foo.png')
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should get default from filename' do
|
95
|
+
@commander.mime_type.should == 'image/png'
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should use the one passed to #encode if provided' do
|
99
|
+
@commander.encode('gif').mime_type.should == 'image/gif'
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
Binary file
|
data/spec/files/test.gif
ADDED
Binary file
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
describe Anisoptera::Serializer do
|
4
|
+
before do
|
5
|
+
@data = {:a => 'a', :b => 'b', :c => 11}
|
6
|
+
@encoded = Anisoptera::Serializer.marshal_encode(@data)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "encoding" do
|
10
|
+
it "must convert object into a string" do
|
11
|
+
@encoded.class.should == String
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'decoding' do
|
16
|
+
it 'should recover original object' do
|
17
|
+
decoded = Anisoptera::Serializer.marshal_decode(@encoded)
|
18
|
+
decoded.should == @data
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
data/spec/test_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
Bundler.setup
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
6
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
7
|
+
|
8
|
+
require 'anisoptera'
|
9
|
+
|
10
|
+
|
11
|
+
require 'rspec'
|
12
|
+
|
13
|
+
def mock_time(atime)
|
14
|
+
t = stub('Now')
|
15
|
+
Time.stub!(:now).and_return t
|
16
|
+
t.stub!(:gmtime).and_return t
|
17
|
+
t.should_receive(:strftime).with("%a, %d %b %Y %H:%M:%S GMT").and_return atime
|
18
|
+
atime
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: anisoptera
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.2
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ismael Celis
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2012-12-04 00:00:00 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: eventmachine
|
17
|
+
prerelease: false
|
18
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.12.10
|
24
|
+
type: :runtime
|
25
|
+
version_requirements: *id001
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: thin
|
28
|
+
prerelease: false
|
29
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: "0"
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id002
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: thin_async
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id003
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: rack
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: 1.2.2
|
57
|
+
type: :runtime
|
58
|
+
version_requirements: *id004
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: bundler
|
61
|
+
prerelease: false
|
62
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 1.0.0
|
68
|
+
type: :development
|
69
|
+
version_requirements: *id005
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rack-test
|
72
|
+
prerelease: false
|
73
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: "0"
|
79
|
+
type: :development
|
80
|
+
version_requirements: *id006
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: rspec
|
83
|
+
prerelease: false
|
84
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: "0"
|
90
|
+
type: :development
|
91
|
+
version_requirements: *id007
|
92
|
+
- !ruby/object:Gem::Dependency
|
93
|
+
name: thin-async-test
|
94
|
+
prerelease: false
|
95
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: "0"
|
101
|
+
type: :development
|
102
|
+
version_requirements: *id008
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: http_router
|
105
|
+
prerelease: false
|
106
|
+
requirement: &id009 !ruby/object:Gem::Requirement
|
107
|
+
none: false
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: "0"
|
112
|
+
type: :development
|
113
|
+
version_requirements: *id009
|
114
|
+
description: You'll need an Eventmachine server such as Thin to run this. See README.'
|
115
|
+
email:
|
116
|
+
- ismaelct@gmail.com
|
117
|
+
executables: []
|
118
|
+
|
119
|
+
extensions: []
|
120
|
+
|
121
|
+
extra_rdoc_files: []
|
122
|
+
|
123
|
+
files:
|
124
|
+
- .gitignore
|
125
|
+
- .rspec
|
126
|
+
- Gemfile
|
127
|
+
- README.md
|
128
|
+
- Rakefile
|
129
|
+
- anisoptera.gemspec
|
130
|
+
- examples/http_router.ru
|
131
|
+
- examples/pic1.jpg
|
132
|
+
- lib/anisoptera.rb
|
133
|
+
- lib/anisoptera/app.rb
|
134
|
+
- lib/anisoptera/async_endpoint.rb
|
135
|
+
- lib/anisoptera/commander.rb
|
136
|
+
- lib/anisoptera/endpoint.rb
|
137
|
+
- lib/anisoptera/error.png
|
138
|
+
- lib/anisoptera/serializer.rb
|
139
|
+
- lib/anisoptera/sync_endpoint.rb
|
140
|
+
- lib/anisoptera/version.rb
|
141
|
+
- spec/anisoptera_spec.rb
|
142
|
+
- spec/async_endpoint_spec.rb
|
143
|
+
- spec/commander_spec.rb
|
144
|
+
- spec/files/Chile.gif
|
145
|
+
- spec/files/test.gif
|
146
|
+
- spec/serializer_spec.rb
|
147
|
+
- spec/test_helper.rb
|
148
|
+
homepage: ""
|
149
|
+
licenses: []
|
150
|
+
|
151
|
+
post_install_message:
|
152
|
+
rdoc_options: []
|
153
|
+
|
154
|
+
require_paths:
|
155
|
+
- lib
|
156
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
157
|
+
none: false
|
158
|
+
requirements:
|
159
|
+
- - ">="
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: "0"
|
162
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
163
|
+
none: false
|
164
|
+
requirements:
|
165
|
+
- - ">="
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: "0"
|
168
|
+
requirements: []
|
169
|
+
|
170
|
+
rubyforge_project: anisoptera
|
171
|
+
rubygems_version: 1.8.17
|
172
|
+
signing_key:
|
173
|
+
specification_version: 3
|
174
|
+
summary: Async Rack app for image thumbnailing
|
175
|
+
test_files:
|
176
|
+
- spec/anisoptera_spec.rb
|
177
|
+
- spec/async_endpoint_spec.rb
|
178
|
+
- spec/commander_spec.rb
|
179
|
+
- spec/files/Chile.gif
|
180
|
+
- spec/files/test.gif
|
181
|
+
- spec/serializer_spec.rb
|
182
|
+
- spec/test_helper.rb
|