rack-thumb-proxy 0.0.8
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 +17 -0
- data/Gemfile +8 -0
- data/LICENSE +22 -0
- data/README.md +187 -0
- data/Rakefile +11 -0
- data/example/config.ru +6 -0
- data/lib/rack-thumb-proxy/configuration.rb +53 -0
- data/lib/rack-thumb-proxy/railtie.rb +11 -0
- data/lib/rack-thumb-proxy/version.rb +7 -0
- data/lib/rack-thumb-proxy/view_helpers.rb +55 -0
- data/lib/rack-thumb-proxy.rb +240 -0
- data/rack-thumb-proxy.gemspec +28 -0
- data/test/fixtures/200x100.gif +0 -0
- data/test/fixtures/250x.gif +0 -0
- data/test/test_configuration_api.rb +46 -0
- data/test/test_helper.rb +27 -0
- data/test/test_rack_thumb_proxy.rb +124 -0
- data/test/test_view_helpers.rb +47 -0
- metadata +136 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Lee Hambley
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
# Rack::Thumb::Proxy
|
2
|
+
|
3
|
+
Resize remotely hosted images not hosted on your servers dynamically.
|
4
|
+
Safely proxy request
|
5
|
+
|
6
|
+
`Rack::Thumb::Proxy` is a project in the spirit of `Rack::Thumb`, but
|
7
|
+
for use in situations when one doesn't host the images statically on
|
8
|
+
one's own server, consider such an example:
|
9
|
+
|
10
|
+
<img src="http://a-site-which-doesnt-run-ssl.com/the/product/image.png" />
|
11
|
+
|
12
|
+
One could download, resize, host and be responsible for this image, but
|
13
|
+
in the days of realtime systems, massive data, clustered storage,
|
14
|
+
etcetera, why bother? One could hot-link the image, but then this
|
15
|
+
doesn't work for cross-protocol, with https in the mix.
|
16
|
+
|
17
|
+
<img src="/media/thumbs/http%3A%2F%2Fa-site-which-doesnt-run-ssl.com%2Fthe%2Fproduct%2Fimage.png" />
|
18
|
+
|
19
|
+
Where, in this case one has predefined `thumbs` as a category. One could
|
20
|
+
also do something such as:
|
21
|
+
|
22
|
+
<img src="/media/50x50/http%3A%2F%2Fa-site-which-doesnt-run-ssl.com%2Fthe%2Fproduct%2Fimage.png" />
|
23
|
+
|
24
|
+
or even
|
25
|
+
|
26
|
+
<img src="/media/50x50/http%3A%2F%2Fa-site-which-doesnt-run-ssl.com%2Fthe%2Fproduct%2Fimage.png" />
|
27
|
+
|
28
|
+
When combined with a CDN or Rack::Cache, this shouldn't cause too heavy
|
29
|
+
a performance penalty, and images from upstream will even be cached
|
30
|
+
locally.
|
31
|
+
|
32
|
+
## Safety
|
33
|
+
|
34
|
+
To ensure that someone doesn't decide to use your server resources to resize
|
35
|
+
their entire collection of cat pictures, there's a hash mechanism which
|
36
|
+
is also available. This works very much like `Rack::Thumb`.
|
37
|
+
|
38
|
+
To use this feature, simply configure `Rack::Thumb::Proxy` with a
|
39
|
+
`secret`, and a `key_length` (the latter may be ommitted and defaults
|
40
|
+
to 10), urls will be then generated with the following appearance:
|
41
|
+
|
42
|
+
<img src="/media/15a5683b74/50x50/http%3A%2F%2Fa-site-which-doesnt-run-ssl.com%2Fthe%2Fproduct%2Fimage.png" />
|
43
|
+
|
44
|
+
The key is calculated as a result of the following pattern:
|
45
|
+
|
46
|
+
"%s\t%s\t%s" % <secret>, <options>, <url encoded image source>
|
47
|
+
|
48
|
+
For example with a terrible secret of `secret`:
|
49
|
+
|
50
|
+
echo "secret\t50x50\thttp%3A%2F%2Fa-site-which-doesnt-run-ssl.com%2Fthe%2Fproduct%2Fimage.png" | openssl dgst -sha1
|
51
|
+
|
52
|
+
The resulting SHA is as you see above. it is recommended that you choose
|
53
|
+
a secret using a token generation tool, if you are using Rails, you have
|
54
|
+
one baked-in simply use `rake secret` from your Rails project root.
|
55
|
+
|
56
|
+
Requests which do not match the expected format will receive a `400 Bad
|
57
|
+
Request` response.
|
58
|
+
|
59
|
+
## (Rails) Helpers
|
60
|
+
|
61
|
+
A helper module is provided which can be used in Rails, Sinatra, or your
|
62
|
+
unit tests. This is loaded automatically via a Railtie into Rails,
|
63
|
+
available from all views. The following methods are defined:
|
64
|
+
|
65
|
+
proxied_image_url("image url", options)
|
66
|
+
proxied_image_tag("image url", options)
|
67
|
+
|
68
|
+
Somewhat of a private API are the following, which you may find useful:
|
69
|
+
|
70
|
+
signature_hash_for("image url", options)
|
71
|
+
|
72
|
+
The image url passed here should not be URL encoded, as
|
73
|
+
`Rack::Thumb::Proxy` will encode it correctly for you.
|
74
|
+
|
75
|
+
## `options`
|
76
|
+
|
77
|
+
"50x" `[String]`Constrain to 50 pixels height, maintaining
|
78
|
+
original aspect ratio
|
79
|
+
|
80
|
+
"x100" `[String]`Constrain to 100 pixels width, maintaining
|
81
|
+
original aspect ratio
|
82
|
+
|
83
|
+
"50x75n" `[String]` Constrain to 50 pixels height, distorting the
|
84
|
+
image to acheive a 75 pixels width with *northern* gravity
|
85
|
+
(see below)
|
86
|
+
|
87
|
+
"50x75" `[String]` Crop to 50 pixels height, without distorting the
|
88
|
+
image to acheive a 75 pixels width
|
89
|
+
|
90
|
+
:label `[Symbol]` Take the options specified in
|
91
|
+
the label (see below)
|
92
|
+
|
93
|
+
{width: 123, height: } `[Hash]` The keys `width`, `height`, and
|
94
|
+
`gravity` are accepted
|
95
|
+
|
96
|
+
## Option Labels
|
97
|
+
|
98
|
+
One can use the configuration API as such to name a label:
|
99
|
+
|
100
|
+
Rack::Thumb::Proxy.configure do
|
101
|
+
option_label :product_thumbnail, "100x100"
|
102
|
+
end
|
103
|
+
|
104
|
+
## Gravity
|
105
|
+
|
106
|
+
Gravity can be specified which will pull the crop (in the case that both
|
107
|
+
width, and height are given), it will focus the cropped area of the
|
108
|
+
image, valid options are `n`, `ne`, `e`, `se`, `s`, `sw`, `w`, `nw`. The
|
109
|
+
default gracity is `c`, which will focus the crop on the centre of the
|
110
|
+
image.
|
111
|
+
|
112
|
+
## No Magic
|
113
|
+
|
114
|
+
If you don't need to resize the image, specifying a magical option of
|
115
|
+
`noop` disables any kind of resizing, this is useful if you just need to
|
116
|
+
use the software a as a proxy. When operating in this mode there is no
|
117
|
+
dependency on imagemagick.
|
118
|
+
|
119
|
+
## Installation
|
120
|
+
|
121
|
+
Add this line to your application's Gemfile:
|
122
|
+
|
123
|
+
gem 'rack-thumb-proxy'
|
124
|
+
|
125
|
+
And then execute:
|
126
|
+
|
127
|
+
$ bundle
|
128
|
+
|
129
|
+
Or install it yourself as:
|
130
|
+
|
131
|
+
$ gem install rack-thumb-proxy
|
132
|
+
|
133
|
+
## Usage
|
134
|
+
|
135
|
+
The included railtie will ensure that this is available in your Rails
|
136
|
+
application, you can simply use something like:
|
137
|
+
|
138
|
+
match '/media', :to => Rack::Thumb::Proxy
|
139
|
+
|
140
|
+
If you need to configure additional options, this can be done in an
|
141
|
+
initializer, or by passing a configuaation hash to the
|
142
|
+
Rack::Thumb::Proxy initialzer. The former is preferred.
|
143
|
+
|
144
|
+
## Example Configuration
|
145
|
+
|
146
|
+
Rack::Thumb::Proxy.configure do
|
147
|
+
prefix "/media/"
|
148
|
+
secret "d94bba3d2e0b4809a570158506"
|
149
|
+
key_length 10
|
150
|
+
end
|
151
|
+
|
152
|
+
When one doesn't want to use the configuration API, the more succinct
|
153
|
+
version would be to do something like:
|
154
|
+
|
155
|
+
# ./config/routes.rb
|
156
|
+
match '/media' => Rack::Thumb::Proxy { prefix: "/",
|
157
|
+
secret: "ABC1234", key_length: 10 }
|
158
|
+
|
159
|
+
# config.ru
|
160
|
+
use Rack::Thumb::Proxy { prefix: "/", secret: "ABC1234", key_length: 10 }
|
161
|
+
|
162
|
+
One complication is that when using the link generator functions, one
|
163
|
+
**must** use the configuration API, otherwise the default path will be
|
164
|
+
`/`.
|
165
|
+
|
166
|
+
## To Do
|
167
|
+
|
168
|
+
* Implement Railtie/helpers.
|
169
|
+
* Ensure the hash signatures are checked.
|
170
|
+
* Make it possible to control the cache control header.
|
171
|
+
* Don't use open-uri.
|
172
|
+
* Check earlier in the process that upstream is an image,
|
173
|
+
don't rely on MiniMagick to blow up on non-image content.
|
174
|
+
* Take the cache-control headers from upstream
|
175
|
+
if we can.
|
176
|
+
* Allow a local cache for the images, perhaps somewhere
|
177
|
+
in `/tmp`.
|
178
|
+
* Actually support option labels, it just looks good in
|
179
|
+
the readme right now, alas.
|
180
|
+
|
181
|
+
## Contributing
|
182
|
+
|
183
|
+
1. Fork it
|
184
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
185
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
186
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
187
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/example/config.ru
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module Rack
|
2
|
+
module Thumb
|
3
|
+
class Proxy
|
4
|
+
|
5
|
+
class Configuration
|
6
|
+
|
7
|
+
attr_reader :cache_control_headers
|
8
|
+
attr_reader :option_labels
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
initialize_defaults!
|
12
|
+
end
|
13
|
+
|
14
|
+
def mount_point(new_mount_point = nil)
|
15
|
+
@mount_point = new_mount_point if new_mount_point
|
16
|
+
return @mount_point
|
17
|
+
end
|
18
|
+
|
19
|
+
def key_length(new_key_length = nil)
|
20
|
+
@key_length = new_key_length if new_key_length
|
21
|
+
return @key_length
|
22
|
+
end
|
23
|
+
|
24
|
+
def secret(new_secret = nil)
|
25
|
+
@secret = new_secret if new_secret
|
26
|
+
return @secret
|
27
|
+
end
|
28
|
+
|
29
|
+
def option_label(label, options)
|
30
|
+
option_labels.merge!(label.to_sym => options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def hash_signatures_in_use?
|
34
|
+
!!@secret
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize_defaults!
|
38
|
+
@secret = nil
|
39
|
+
@key_length = 10
|
40
|
+
@mount_point = '/'
|
41
|
+
@option_labels = {}
|
42
|
+
@cache_control_headers = {'Cache-Control' => 'max-age=86400, public, must-revalidate'}
|
43
|
+
end
|
44
|
+
alias :reset_defaults! :initialize_defaults!
|
45
|
+
private :initialize_defaults!
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Rack
|
2
|
+
module Thumb
|
3
|
+
class Proxy
|
4
|
+
module ViewHelpers
|
5
|
+
|
6
|
+
def proxied_image_url(image_url, options = nil)
|
7
|
+
|
8
|
+
if rack_thumb_proxy_hash_signatures_enabled?
|
9
|
+
signature = hash_signature_for(image_url, options)
|
10
|
+
end
|
11
|
+
|
12
|
+
options = rack_thumb_proxy_options_to_s(options)
|
13
|
+
escaped_image_url = escape_image_url(image_url)
|
14
|
+
mount_point = rack_thumb_proxy_configuration.mount_point
|
15
|
+
|
16
|
+
mount_point + [signature, options, escaped_image_url].compact.join("/")
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
def hash_signature_for(image_url, options = nil)
|
21
|
+
return nil unless rack_thumb_proxy_hash_signatures_enabled?
|
22
|
+
key_length = rack_thumb_proxy_configuration.key_length
|
23
|
+
secret = rack_thumb_proxy_configuration.secret
|
24
|
+
('%s\t%s\t%s' % [secret, options, escape_image_url(image_url)])[0..key_length-1]
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def escape_image_url(image_url)
|
30
|
+
CGI.escape(image_url)
|
31
|
+
end
|
32
|
+
|
33
|
+
def rack_thumb_proxy_options_to_s(options = nil)
|
34
|
+
return nil if options.nil?
|
35
|
+
if options.is_a?(String)
|
36
|
+
options
|
37
|
+
elsif options.is_a?(Hash)
|
38
|
+
sprintf("%sx%s%s", options[:width], options[:height], options[:gravity])
|
39
|
+
else
|
40
|
+
raise RuntimeError, 'Not implemented yet, see the TODO in README.md'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def rack_thumb_proxy_configuration
|
45
|
+
Rack::Thumb::Proxy.configuration
|
46
|
+
end
|
47
|
+
|
48
|
+
def rack_thumb_proxy_hash_signatures_enabled?
|
49
|
+
rack_thumb_proxy_configuration.hash_signatures_in_use?
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,240 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'rack-thumb-proxy/version'
|
5
|
+
require 'rack-thumb-proxy/configuration'
|
6
|
+
require 'rack-thumb-proxy/view_helpers'
|
7
|
+
|
8
|
+
require 'rack-thumb-proxy/railtie' if defined?(Rails)
|
9
|
+
|
10
|
+
module Rack
|
11
|
+
|
12
|
+
module Thumb
|
13
|
+
|
14
|
+
class Proxy
|
15
|
+
|
16
|
+
class << self
|
17
|
+
|
18
|
+
attr_writer :configuration
|
19
|
+
|
20
|
+
def configure(&block)
|
21
|
+
configuration.instance_eval(&block)
|
22
|
+
configuration
|
23
|
+
end
|
24
|
+
|
25
|
+
def configuration
|
26
|
+
@configuration ||= Configuration.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def call(env)
|
30
|
+
new(env).call
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(env)
|
36
|
+
@env = env
|
37
|
+
@path = env['PATH_INFO']
|
38
|
+
end
|
39
|
+
|
40
|
+
def call
|
41
|
+
if request_matches?
|
42
|
+
validate_signature! &&
|
43
|
+
retreive_upstream! &&
|
44
|
+
transform_image! &&
|
45
|
+
format_response!
|
46
|
+
response.finish
|
47
|
+
else
|
48
|
+
[404, {'Content-Length' => 9.to_s, 'Content-Type' => 'text/plain'}, ['Not Found']]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def validate_signature!
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
def retreive_upstream!
|
59
|
+
begin
|
60
|
+
open(request_url, 'rb') do |f|
|
61
|
+
tempfile.binmode
|
62
|
+
tempfile.write(f.read)
|
63
|
+
tempfile.flush
|
64
|
+
end
|
65
|
+
rescue
|
66
|
+
write_error_to_response!
|
67
|
+
return false
|
68
|
+
end
|
69
|
+
return true
|
70
|
+
end
|
71
|
+
|
72
|
+
def format_response!
|
73
|
+
response.status = 200
|
74
|
+
response.headers["Content-Type"] = mime_type_from_request_url
|
75
|
+
response.headers["Content-Length"] = transformed_image_file_size_in_bytes.to_s
|
76
|
+
response.body << read_tempfile
|
77
|
+
true
|
78
|
+
end
|
79
|
+
|
80
|
+
def read_tempfile
|
81
|
+
tempfile.rewind
|
82
|
+
tempfile.read
|
83
|
+
end
|
84
|
+
|
85
|
+
def tempfile
|
86
|
+
@_tempfile ||= Tempfile.new('rack_thumb_proxy')
|
87
|
+
end
|
88
|
+
|
89
|
+
def tempfile_path
|
90
|
+
tempfile.path
|
91
|
+
end
|
92
|
+
|
93
|
+
def transform_image!
|
94
|
+
|
95
|
+
return true unless should_resize?
|
96
|
+
|
97
|
+
begin
|
98
|
+
require 'mapel'
|
99
|
+
|
100
|
+
width, height = dimensions_from_request_options
|
101
|
+
owidth, oheight = dimensions_from_tempfile
|
102
|
+
|
103
|
+
width = [width, owidth].min if width
|
104
|
+
height = [height, oheight].min if height
|
105
|
+
|
106
|
+
cmd = Mapel(tempfile_path)
|
107
|
+
|
108
|
+
if width && height
|
109
|
+
cmd.gravity(request_gravity)
|
110
|
+
cmd.resize!(width, height)
|
111
|
+
else
|
112
|
+
cmd.resize(width, height, 0, 0, '>')
|
113
|
+
end
|
114
|
+
|
115
|
+
cmd.to(tempfile_path).run
|
116
|
+
|
117
|
+
rescue
|
118
|
+
puts $!, $@
|
119
|
+
write_error_to_response!
|
120
|
+
return false
|
121
|
+
end
|
122
|
+
|
123
|
+
true
|
124
|
+
end
|
125
|
+
|
126
|
+
def should_resize?
|
127
|
+
!request_options.empty?
|
128
|
+
end
|
129
|
+
|
130
|
+
def should_verify_hash_signature?
|
131
|
+
configuration.hash_signatures_in_use?
|
132
|
+
end
|
133
|
+
|
134
|
+
def configuration
|
135
|
+
self.class.configuration
|
136
|
+
end
|
137
|
+
|
138
|
+
def request_hash_signature
|
139
|
+
@_request_match_data["hash_signature"]
|
140
|
+
end
|
141
|
+
|
142
|
+
def request_options
|
143
|
+
@_request_match_data["options"]
|
144
|
+
end
|
145
|
+
|
146
|
+
def request_gravity
|
147
|
+
{
|
148
|
+
'nw' => :northwest,
|
149
|
+
'n' => :north,
|
150
|
+
'ne' => :northeast,
|
151
|
+
'w' => :west,
|
152
|
+
'c' => :center,
|
153
|
+
'e' => :east,
|
154
|
+
'sw' => :southwest,
|
155
|
+
's' => :south,
|
156
|
+
'se' => :southeast
|
157
|
+
}.fetch(request_gravity_shorthand, :center)
|
158
|
+
end
|
159
|
+
|
160
|
+
def request_gravity_shorthand
|
161
|
+
@_request_match_data["gravity"]
|
162
|
+
end
|
163
|
+
|
164
|
+
def request_url
|
165
|
+
CGI.unescape(escaped_request_url)
|
166
|
+
end
|
167
|
+
|
168
|
+
def escaped_request_url
|
169
|
+
@_request_match_data["escaped_url"]
|
170
|
+
end
|
171
|
+
|
172
|
+
def request_matches?
|
173
|
+
@_request_match_data = @path.match(routing_pattern)
|
174
|
+
end
|
175
|
+
|
176
|
+
def witdh_from_tempfile
|
177
|
+
dimensions_from_tempfile.first
|
178
|
+
end
|
179
|
+
|
180
|
+
def height_from_tempfile
|
181
|
+
dimensions_from_tempfile.last
|
182
|
+
end
|
183
|
+
|
184
|
+
def dimensions_from_tempfile
|
185
|
+
require 'mapel' unless defined?(Mapel)
|
186
|
+
Mapel.info(tempfile_path)[:dimensions]
|
187
|
+
end
|
188
|
+
|
189
|
+
def width_from_request_options
|
190
|
+
dimensions_from_request_options.first
|
191
|
+
end
|
192
|
+
|
193
|
+
def height_from_request_options
|
194
|
+
dimensions_from_request_options.last
|
195
|
+
end
|
196
|
+
|
197
|
+
def dimensions_from_request_options
|
198
|
+
width, height = request_options.split('x').map(&:to_i).collect { |dim| dim == 0 ? nil : dim }
|
199
|
+
[width, height]
|
200
|
+
end
|
201
|
+
|
202
|
+
def transformed_image_file_size_in_bytes
|
203
|
+
::File.size(tempfile_path)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Examples: http://rubular.com/r/oPRK1t31yv
|
207
|
+
def routing_pattern
|
208
|
+
/^\/(?<hash_signature>[a-z0-9]{10}|)\/?(?<options>(:?[0-9]*x+[0-9]*|))(?<gravity>c|n|ne|e|s|sw|w|nw|)\/?(?<escaped_url>https?.*)$/
|
209
|
+
end
|
210
|
+
|
211
|
+
def response
|
212
|
+
@_response ||= Rack::Response.new
|
213
|
+
end
|
214
|
+
|
215
|
+
def write_error_to_response!
|
216
|
+
response.status = 500
|
217
|
+
response.headers['Content-Type'] = 'text/plain'
|
218
|
+
response.body << $!.message
|
219
|
+
response.body << "\n\n"
|
220
|
+
response.body << $!.backtrace.join("\n")
|
221
|
+
end
|
222
|
+
|
223
|
+
def request_url_file_extension
|
224
|
+
::File.extname(request_url)
|
225
|
+
end
|
226
|
+
|
227
|
+
def mime_type_from_request_url
|
228
|
+
{
|
229
|
+
'.png' => 'image/png',
|
230
|
+
'.gif' => 'image/gif',
|
231
|
+
'.jpg' => 'image/jpeg',
|
232
|
+
'.jpeg' => 'image/jpeg'
|
233
|
+
}.fetch(request_url_file_extension, 'application/octet-stream')
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/rack-thumb-proxy/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
|
6
|
+
gem.authors = ["Lee Hambley"]
|
7
|
+
gem.email = ["lee.hambley@gmail.com"]
|
8
|
+
gem.description = %q{ Rack middleware for resizing proxied requests for images which don't reside on your own servers. }
|
9
|
+
gem.summary = %q{ For more information see https://github.com/leehambley/rack-thumb-proxy }
|
10
|
+
gem.homepage = ""
|
11
|
+
|
12
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
13
|
+
gem.files = `git ls-files`.split("\n")
|
14
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
gem.name = "rack-thumb-proxy"
|
16
|
+
gem.require_paths = ["lib"]
|
17
|
+
|
18
|
+
gem.version = Rack::Thumb::Proxy::VERSION
|
19
|
+
|
20
|
+
gem.add_dependency 'rack'
|
21
|
+
|
22
|
+
gem.add_development_dependency 'minitest', '~> 2.11'
|
23
|
+
gem.add_development_dependency 'webmock', '~> 1.8.0'
|
24
|
+
gem.add_development_dependency 'rack-test', '~> 0.6.1'
|
25
|
+
gem.add_development_dependency 'mapel'
|
26
|
+
gem.add_development_dependency 'dimensions'
|
27
|
+
|
28
|
+
end
|
Binary file
|
Binary file
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TestConfigurationApi < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
def test_it_should_return_a_configuration_object
|
6
|
+
assert Rack::Thumb::Proxy.configuration.is_a?(Rack::Thumb::Proxy::Configuration)
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_default_options
|
10
|
+
Rack::Thumb::Proxy.configuration.reset_defaults!
|
11
|
+
default_configuration = Rack::Thumb::Proxy.configuration
|
12
|
+
assert_equal nil, default_configuration.secret
|
13
|
+
assert_equal 10, default_configuration.key_length
|
14
|
+
assert_equal '/', default_configuration.mount_point
|
15
|
+
assert_equal({'Cache-Control' => 'max-age=86400, public, must-revalidate'}, default_configuration.cache_control_headers)
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_it_should_take_a_block_which_is_class_evaluated_keeping_all_options
|
19
|
+
Rack::Thumb::Proxy.configuration.reset_defaults!
|
20
|
+
configuration = Rack::Thumb::Proxy.configure do
|
21
|
+
secret '123'
|
22
|
+
key_length 10
|
23
|
+
mount_point '/'
|
24
|
+
end
|
25
|
+
assert_equal '123', configuration.secret
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_setting_option_labels_via_the_configuration_api
|
29
|
+
Rack::Thumb::Proxy.configuration.reset_defaults!
|
30
|
+
configuration = Rack::Thumb::Proxy.configure do
|
31
|
+
option_label :thumbnail, '50x75c'
|
32
|
+
end
|
33
|
+
assert configuration.option_labels.has_key?(:thumbnail)
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_hash_signatures_are_correctly_enabled_and_disabled_based_on_the_presense_of_the_secret
|
37
|
+
Rack::Thumb::Proxy.configuration.reset_defaults!
|
38
|
+
configuration = Rack::Thumb::Proxy.configuration
|
39
|
+
refute configuration.secret
|
40
|
+
refute configuration.hash_signatures_in_use?
|
41
|
+
configuration.secret('something truthy')
|
42
|
+
assert configuration.secret
|
43
|
+
assert configuration.hash_signatures_in_use?
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'cgi'
|
5
|
+
|
6
|
+
require 'rack-thumb-proxy'
|
7
|
+
|
8
|
+
require 'rack/test'
|
9
|
+
|
10
|
+
require 'dimensions'
|
11
|
+
|
12
|
+
require 'digest/md5'
|
13
|
+
|
14
|
+
require 'minitest/autorun'
|
15
|
+
require 'minitest/pride'
|
16
|
+
require 'minitest/mock'
|
17
|
+
|
18
|
+
require 'webmock/minitest'
|
19
|
+
|
20
|
+
WebMock.disable_net_connect!
|
21
|
+
|
22
|
+
require 'ruby-debug'
|
23
|
+
require 'mapel'
|
24
|
+
|
25
|
+
class ViewHelperSurrogate
|
26
|
+
include Rack::Thumb::Proxy::ViewHelpers
|
27
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TestRackThumbProxy < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
include Rack::Test::Methods
|
6
|
+
|
7
|
+
def app
|
8
|
+
Rack::Thumb::Proxy
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_it_should_repond_with_not_found_when_the_upstream_url_is_bunk
|
12
|
+
get "/something/that/doesnt/even/match/a/little/bit"
|
13
|
+
assert last_response.not_found?
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_it_should_return_success_with_the_correct_content_length_when_a_matching_url_is_found_for_a_noop_image
|
17
|
+
stub_image_request!('250x.gif', 'http://www.example.com/images/noop-kittens.gif')
|
18
|
+
get '/' + escape('http://www.example.com/images/noop-kittens.gif')
|
19
|
+
assert last_response.ok?
|
20
|
+
assert_equal file_size_for_fixture('250x.gif').to_s, last_response.headers.fetch('Content-Length')
|
21
|
+
assert_equal file_hash_for_fixture('250x.gif'), file_hash_from_string(last_response.body)
|
22
|
+
assert_dimensions last_response.body, 250, 250
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_it_should_return_a_smaller_image_when_resizing_with_minimagick
|
26
|
+
stub_image_request!('250x.gif', 'http://www.example.com/images/kittens.gif')
|
27
|
+
get '/50x50/' + escape('http://www.example.com/images/kittens.gif')
|
28
|
+
assert last_response.ok?
|
29
|
+
assert_dimensions last_response.body, 50, 50
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_it_should_crop_when_the_ratio_cannot_be_maintained
|
33
|
+
stub_image_request!('200x100.gif', 'http://www.example.com/images/sharkjumping.gif')
|
34
|
+
get '/50x50/' + escape('http://www.example.com/images/sharkjumping.gif')
|
35
|
+
assert last_response.ok?
|
36
|
+
assert_dimensions last_response.body, 50, 50
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_it_should_retain_the_aspec_ratio
|
40
|
+
stub_image_request!('200x100.gif', 'http://www.example.com/images/sharkjumping.gif')
|
41
|
+
get '/50x/' + escape('http://www.example.com/images/sharkjumping.gif')
|
42
|
+
assert last_response.ok?
|
43
|
+
assert_dimensions last_response.body, 50, 25
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_it_should_accept_the_gravity_setting_without_breaking_the_resize
|
47
|
+
stub_image_request!('200x100.gif', 'http://www.example.com/images/sharkjumping.gif')
|
48
|
+
get '/50x50e/' + escape('http://www.example.com/images/sharkjumping.gif')
|
49
|
+
assert last_response.ok?
|
50
|
+
east = last_response.body
|
51
|
+
get '/50x50w/' + escape('http://www.example.com/images/sharkjumping.gif')
|
52
|
+
assert last_response.ok?
|
53
|
+
west = last_response.body
|
54
|
+
refute_equal file_hash_from_string(east), file_hash_from_string(west)
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_default_gravity_is_center_when_cropping
|
58
|
+
stub_image_request!('200x100.gif', 'http://www.example.com/images/sharkjumping.gif')
|
59
|
+
get '/75x75/' + escape('http://www.example.com/images/sharkjumping.gif')
|
60
|
+
no_gravity = last_response.body
|
61
|
+
get '/75x75c/' + escape('http://www.example.com/images/sharkjumping.gif')
|
62
|
+
c_gravity = last_response.body
|
63
|
+
assert_equal file_hash_from_string(no_gravity), file_hash_from_string(c_gravity)
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_should_respond_with_the_proper_mimetype_for_known_image_types
|
67
|
+
skip
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_should_respond_with_application_octet_stream_mimetype_for_unknown
|
71
|
+
skip
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def stub_image_request!(file, url)
|
77
|
+
stub_request(:any, url).to_return(
|
78
|
+
body: File.read(filename_for_fixture(file)),
|
79
|
+
headers: {
|
80
|
+
'Content-Length' => file_size_for_fixture(file)
|
81
|
+
},
|
82
|
+
status: 200
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
def escape(string)
|
87
|
+
CGI.escape(string)
|
88
|
+
end
|
89
|
+
|
90
|
+
def file_size_for_fixture(file)
|
91
|
+
File.size(filename_for_fixture(file))
|
92
|
+
end
|
93
|
+
|
94
|
+
def file_hash_from_string(string)
|
95
|
+
Digest::MD5.hexdigest(string)
|
96
|
+
end
|
97
|
+
|
98
|
+
def file_hash_for_fixture(file)
|
99
|
+
file_hash_from_string(File.read(filename_for_fixture(file)))
|
100
|
+
end
|
101
|
+
|
102
|
+
def filename_for_fixture(file)
|
103
|
+
File.expand_path('test/fixtures/' + file)
|
104
|
+
end
|
105
|
+
|
106
|
+
def fixture_file_exists?(file)
|
107
|
+
File.exists?(file) rescue false
|
108
|
+
end
|
109
|
+
|
110
|
+
def assert_dimensions(input, expected_width, expected_height)
|
111
|
+
unless fixture_file_exists?(input)
|
112
|
+
tf = Tempfile.new('assert_dimensions')
|
113
|
+
tf.write input
|
114
|
+
tf.flush
|
115
|
+
tf.rewind
|
116
|
+
input = tf.path
|
117
|
+
end
|
118
|
+
actual_width, actual_height = Dimensions.dimensions(input)
|
119
|
+
assert_equal "#{expected_width}x#{expected_height}", "#{actual_width}x#{actual_height}"
|
120
|
+
ensure
|
121
|
+
tf.close if tf rescue nil
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TestViewHelpers < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
def test_without_any_options_or_special_configuration_the_view_helper_encodes_the_image_url_correctly
|
6
|
+
Rack::Thumb::Proxy.configuration.reset_defaults!
|
7
|
+
assert_equal "/http%3A%2F%2Fwww.example.com%2F1.png", vh.proxied_image_url("http://www.example.com/1.png")
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_without_any_options_with_a_mount_point_configured_the_view_helper_encodes_the_image_url_correctly
|
11
|
+
Rack::Thumb::Proxy.configuration.reset_defaults!
|
12
|
+
Rack::Thumb::Proxy.configuration.mount_point("/mount/")
|
13
|
+
assert_equal "/mount/http%3A%2F%2Fwww.example.com%2F1.png", vh.proxied_image_url("http://www.example.com/1.png")
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_with_a_width_option_only_configured_the_view_helper_encodes_the_image_url_correctly
|
17
|
+
Rack::Thumb::Proxy.configuration.reset_defaults!
|
18
|
+
assert_equal "/50x/http%3A%2F%2Fwww.example.com%2F1.png", vh.proxied_image_url("http://www.example.com/1.png", '50x')
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_with_a_height_option_only_configured_the_view_helper_encodes_the_image_url_correctly
|
22
|
+
Rack::Thumb::Proxy.configuration.reset_defaults!
|
23
|
+
assert_equal "/x50/http%3A%2F%2Fwww.example.com%2F1.png", vh.proxied_image_url("http://www.example.com/1.png", 'x50')
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_with_a_width_and_height_option_configured_the_view_helper_encodes_the_image_url_correctly
|
27
|
+
Rack::Thumb::Proxy.configuration.reset_defaults!
|
28
|
+
assert_equal "/75x50/http%3A%2F%2Fwww.example.com%2F1.png", vh.proxied_image_url("http://www.example.com/1.png", '75x50')
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_with_a_width_and_height_option_as_a_hash_configured_the_view_helper_encodes_the_image_url_correctly
|
32
|
+
Rack::Thumb::Proxy.configuration.reset_defaults!
|
33
|
+
assert_equal "/45x90/http%3A%2F%2Fwww.example.com%2F1.png", vh.proxied_image_url("http://www.example.com/1.png", {width: 45, height: 90})
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_with_a_width_height_and_gravity_options_as_a_hash_configured_the_view_helper_encodes_the_image_url_correctly
|
37
|
+
Rack::Thumb::Proxy.configuration.reset_defaults!
|
38
|
+
assert_equal "/45x90s/http%3A%2F%2Fwww.example.com%2F1.png", vh.proxied_image_url("http://www.example.com/1.png", {width: 45, height: 90, gravity: :s})
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def vh
|
44
|
+
ViewHelperSurrogate.new
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-thumb-proxy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.8
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Lee Hambley
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-02-22 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rack
|
16
|
+
requirement: &70327349437160 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70327349437160
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: minitest
|
27
|
+
requirement: &70327349436340 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.11'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70327349436340
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: webmock
|
38
|
+
requirement: &70327349435680 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 1.8.0
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70327349435680
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rack-test
|
49
|
+
requirement: &70327349435200 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.6.1
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70327349435200
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: mapel
|
60
|
+
requirement: &70327349434720 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70327349434720
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: dimensions
|
71
|
+
requirement: &70327349434020 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *70327349434020
|
80
|
+
description: ! ' Rack middleware for resizing proxied requests for images which don''t
|
81
|
+
reside on your own servers. '
|
82
|
+
email:
|
83
|
+
- lee.hambley@gmail.com
|
84
|
+
executables: []
|
85
|
+
extensions: []
|
86
|
+
extra_rdoc_files: []
|
87
|
+
files:
|
88
|
+
- .gitignore
|
89
|
+
- Gemfile
|
90
|
+
- LICENSE
|
91
|
+
- README.md
|
92
|
+
- Rakefile
|
93
|
+
- example/config.ru
|
94
|
+
- lib/rack-thumb-proxy.rb
|
95
|
+
- lib/rack-thumb-proxy/configuration.rb
|
96
|
+
- lib/rack-thumb-proxy/railtie.rb
|
97
|
+
- lib/rack-thumb-proxy/version.rb
|
98
|
+
- lib/rack-thumb-proxy/view_helpers.rb
|
99
|
+
- rack-thumb-proxy.gemspec
|
100
|
+
- test/fixtures/200x100.gif
|
101
|
+
- test/fixtures/250x.gif
|
102
|
+
- test/test_configuration_api.rb
|
103
|
+
- test/test_helper.rb
|
104
|
+
- test/test_rack_thumb_proxy.rb
|
105
|
+
- test/test_view_helpers.rb
|
106
|
+
homepage: ''
|
107
|
+
licenses: []
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options: []
|
110
|
+
require_paths:
|
111
|
+
- lib
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - ! '>='
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
requirements: []
|
125
|
+
rubyforge_project:
|
126
|
+
rubygems_version: 1.8.10
|
127
|
+
signing_key:
|
128
|
+
specification_version: 3
|
129
|
+
summary: For more information see https://github.com/leehambley/rack-thumb-proxy
|
130
|
+
test_files:
|
131
|
+
- test/fixtures/200x100.gif
|
132
|
+
- test/fixtures/250x.gif
|
133
|
+
- test/test_configuration_api.rb
|
134
|
+
- test/test_helper.rb
|
135
|
+
- test/test_rack_thumb_proxy.rb
|
136
|
+
- test/test_view_helpers.rb
|