httpimagestore 1.8.0 → 1.8.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 +20 -19
- data/Gemfile.lock +1 -0
- data/VERSION +1 -1
- data/features/compatibility.feature +11 -0
- data/features/encoding.feature +75 -7
- data/features/rewrite.feature +3 -3
- data/httpimagestore.gemspec +5 -2
- data/lib/httpimagestore/configuration/file.rb +12 -7
- data/lib/httpimagestore/configuration/handler.rb +66 -34
- data/lib/httpimagestore/configuration/identify.rb +5 -6
- data/lib/httpimagestore/configuration/output.rb +21 -34
- data/lib/httpimagestore/configuration/path.rb +13 -0
- data/lib/httpimagestore/configuration/s3.rb +9 -9
- data/lib/httpimagestore/configuration/thumbnailer.rb +25 -24
- data/lib/httpimagestore/error_reporter.rb +4 -1
- data/lib/httpimagestore/ruby_string_template.rb +43 -6
- data/spec/configuration_file_spec.rb +2 -2
- data/spec/configuration_output_spec.rb +4 -4
- data/spec/configuration_s3_spec.rb +2 -2
- data/spec/configuration_thumbnailer_spec.rb +1 -1
- data/spec/ruby_string_template_spec.rb +14 -0
- metadata +19 -3
data/Gemfile
CHANGED
@@ -1,26 +1,27 @@
|
|
1
|
-
source
|
2
|
-
ruby
|
1
|
+
source 'http://rubygems.org'
|
2
|
+
ruby '1.9.3'
|
3
3
|
|
4
|
-
gem
|
5
|
-
#gem
|
6
|
-
gem
|
7
|
-
#gem
|
8
|
-
gem
|
9
|
-
gem
|
10
|
-
gem
|
11
|
-
gem
|
4
|
+
gem 'unicorn-cuba-base', '~> 1.2.2'
|
5
|
+
#gem 'unicorn-cuba-base', path: '../unicorn-cuba-base'
|
6
|
+
gem 'httpthumbnailer-client', '~> 1.2.0'
|
7
|
+
#gem 'httpthumbnailer-client', path: '../httpthumbnailer-client'
|
8
|
+
gem 'aws-sdk', '~> 1.10'
|
9
|
+
gem 'mime-types', '~> 1.17'
|
10
|
+
gem 'sdl4r', '~> 0.9'
|
11
|
+
gem 'msgpack', '~> 0.5'
|
12
|
+
gem 'addressable', '~> 2.3'
|
12
13
|
|
13
14
|
# Add dependencies to develop your gem here.
|
14
15
|
# Include everything needed to run rake, tests, features, etc.
|
15
16
|
group :development do
|
16
|
-
gem
|
17
|
-
gem
|
18
|
-
gem
|
19
|
-
gem
|
20
|
-
gem
|
21
|
-
gem
|
22
|
-
gem
|
23
|
-
#gem
|
24
|
-
gem
|
17
|
+
gem 'faraday', '>= 0.8'
|
18
|
+
gem 'rspec', '~> 2.13'
|
19
|
+
gem 'cucumber', '>= 0'
|
20
|
+
gem 'jeweler', '~> 1.8.4'
|
21
|
+
gem 'rdoc', '~> 3.9'
|
22
|
+
gem 'daemon', '~> 1'
|
23
|
+
gem 'prawn', '= 0.8.4'
|
24
|
+
#gem 'httpthumbnailer', path: '../httpthumbnailer'
|
25
|
+
gem 'httpthumbnailer', '~> 1.2.0'
|
25
26
|
end
|
26
27
|
|
data/Gemfile.lock
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.8.
|
1
|
+
1.8.1
|
@@ -152,3 +152,14 @@ Feature: Image list based thumbnailing and S3 storage
|
|
152
152
|
Then response status will be 200
|
153
153
|
And response content type will be image/jpeg
|
154
154
|
Then response body will contain JPEG image of size 100x141
|
155
|
+
|
156
|
+
@compatibility
|
157
|
+
Scenario: Input file extension should be based on content detected mime type and not on provided path
|
158
|
+
Given test.jpg file content as request body
|
159
|
+
When I do PUT request http://localhost:3000/thumbnail/blah/test/image/test.gif
|
160
|
+
Then response status will be 400
|
161
|
+
And response content type will be text/plain
|
162
|
+
And response body will be CRLF ended lines
|
163
|
+
"""
|
164
|
+
no thumbnailing specs were selected, please use at least one of: small, tiny_png, bad
|
165
|
+
"""
|
data/features/encoding.feature
CHANGED
@@ -2,34 +2,102 @@ Feature: Encoded UTF-8 URI support
|
|
2
2
|
HTTP Image Store should be able to decode UTF-8 characters form URI using URI decode and also support JavaScript encode() format.
|
3
3
|
|
4
4
|
Background:
|
5
|
+
Given S3 settings in AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_S3_TEST_BUCKET environment variables
|
5
6
|
Given httpthumbnailer server is running at http://localhost:3100/health_check
|
6
7
|
Given httpimagestore server is running at http://localhost:3000/health_check with the following configuration
|
7
8
|
"""
|
9
|
+
s3 key="@AWS_ACCESS_KEY_ID@" secret="@AWS_SECRET_ACCESS_KEY@" ssl=false
|
8
10
|
path "path" "#{path}"
|
9
11
|
|
10
|
-
post "encoding" "encoded" {
|
12
|
+
post "file" "encoding" "encoded" {
|
11
13
|
store_file "input" root="/tmp" path="path"
|
12
14
|
output_store_uri "input" path="path"
|
13
15
|
}
|
14
16
|
|
15
|
-
post "encoding" "decoded" {
|
17
|
+
post "file" "encoding" "decoded" {
|
16
18
|
store_file "input" root="/tmp" path="path"
|
17
19
|
output_store_path "input" path="path"
|
18
20
|
}
|
21
|
+
|
22
|
+
post "s3" "encoding" "encoded" {
|
23
|
+
store_s3 "input" bucket="@AWS_S3_TEST_BUCKET@" path="path"
|
24
|
+
output_store_uri "input" path="path"
|
25
|
+
}
|
26
|
+
|
27
|
+
post "s3" "encoding" "decoded" {
|
28
|
+
store_s3 "input" bucket="@AWS_S3_TEST_BUCKET@" path="path"
|
29
|
+
output_store_path "input" path="path"
|
30
|
+
}
|
19
31
|
"""
|
20
32
|
|
21
|
-
Scenario: JavaScript encode() + URL encoded variable decoding and URL encoding
|
33
|
+
Scenario: JavaScript encode() + URL encoded variable decoding and URL encoding for file based storage
|
22
34
|
Given test.png file content as request body
|
23
|
-
When I do POST request http://localhost:3000/encoding/encoded/triple%20kro%25u0301l.png
|
35
|
+
When I do POST request http://localhost:3000/file/encoding/encoded/triple%20kro%25u0301l.png
|
36
|
+
And response content type will be text/uri-list
|
24
37
|
And response body will be CRLF ended lines
|
25
38
|
"""
|
26
|
-
/triple%
|
39
|
+
/triple%20kr%C3%B3l.png
|
27
40
|
"""
|
28
41
|
|
29
|
-
Scenario: URL encoded variable decoding
|
42
|
+
Scenario: JavaScript encode() + URL encoded variable decoding and URL encoding for S3 based storage
|
30
43
|
Given test.png file content as request body
|
31
|
-
When I do POST request http://localhost:3000/encoding/
|
44
|
+
When I do POST request http://localhost:3000/s3/encoding/encoded/triple%20kro%25u0301l.png
|
45
|
+
And response content type will be text/uri-list
|
46
|
+
And response body will be CRLF ended lines
|
47
|
+
"""
|
48
|
+
/triple%20kr%C3%B3l.png
|
49
|
+
"""
|
50
|
+
|
51
|
+
Scenario: URL encoded variable decoding and URL encoding with ? for file based storage
|
52
|
+
Given test.png file content as request body
|
53
|
+
When I do POST request http://localhost:3000/file/encoding/encoded/hello%3Fworld.png
|
54
|
+
And response content type will be text/uri-list
|
55
|
+
And response body will be CRLF ended lines
|
56
|
+
"""
|
57
|
+
/hello%3Fworld.png
|
58
|
+
"""
|
59
|
+
|
60
|
+
Scenario: URL encoded variable decoding and URL encoding with ? for S3 based storage
|
61
|
+
Given test.png file content as request body
|
62
|
+
When I do POST request http://localhost:3000/s3/encoding/encoded/hello%3Fworld.png
|
63
|
+
And response content type will be text/uri-list
|
64
|
+
And response body will be CRLF ended lines
|
65
|
+
"""
|
66
|
+
/hello%3Fworld.png
|
67
|
+
"""
|
68
|
+
|
69
|
+
Scenario: URL encoded variable decoding for file based storage
|
70
|
+
Given test.png file content as request body
|
71
|
+
When I do POST request http://localhost:3000/file/encoding/decoded/triple%20kr%C3%B3l.png
|
72
|
+
And response content type will be text/plain
|
32
73
|
And response body will be CRLF ended lines
|
33
74
|
"""
|
34
75
|
triple król.png
|
35
76
|
"""
|
77
|
+
|
78
|
+
Scenario: URL encoded variable decoding for S3 based storage
|
79
|
+
Given test.png file content as request body
|
80
|
+
When I do POST request http://localhost:3000/s3/encoding/decoded/triple%20kr%C3%B3l.png
|
81
|
+
And response content type will be text/plain
|
82
|
+
And response body will be CRLF ended lines
|
83
|
+
"""
|
84
|
+
triple król.png
|
85
|
+
"""
|
86
|
+
|
87
|
+
Scenario: URL path normalization with ? for file based storage
|
88
|
+
Given test.png file content as request body
|
89
|
+
When I do POST request http://localhost:3000/file/encoding/decoded/hello%3Fworld.png
|
90
|
+
And response content type will be text/plain
|
91
|
+
And response body will be CRLF ended lines
|
92
|
+
"""
|
93
|
+
hello?world.png
|
94
|
+
"""
|
95
|
+
|
96
|
+
Scenario: URL path normalization with ? S3 file based storage
|
97
|
+
Given test.png file content as request body
|
98
|
+
When I do POST request http://localhost:3000/s3/encoding/decoded/hello%3Fworld.png
|
99
|
+
And response content type will be text/plain
|
100
|
+
And response body will be CRLF ended lines
|
101
|
+
"""
|
102
|
+
hello?world.png
|
103
|
+
"""
|
data/features/rewrite.feature
CHANGED
@@ -52,7 +52,7 @@ Feature: Rewrite of the output path and URL
|
|
52
52
|
|
53
53
|
post "rewrite" "demo" {
|
54
54
|
store_file "input" root="/tmp" path="input_digest"
|
55
|
-
output_store_url "input" scheme="ftp" host="example.com" port="
|
55
|
+
output_store_url "input" scheme="ftp" host="example.com" port="421" path="demo"
|
56
56
|
}
|
57
57
|
"""
|
58
58
|
|
@@ -109,7 +109,7 @@ Feature: Rewrite of the output path and URL
|
|
109
109
|
When I do POST request http://localhost:3000/rewrite/url/port?number=41
|
110
110
|
And response body will be CRLF ended lines
|
111
111
|
"""
|
112
|
-
file
|
112
|
+
file://localhost:41/b0fe25319ba5909a
|
113
113
|
"""
|
114
114
|
|
115
115
|
Scenario: Store URL rewriting demo
|
@@ -117,6 +117,6 @@ Feature: Rewrite of the output path and URL
|
|
117
117
|
When I do POST request http://localhost:3000/rewrite/demo
|
118
118
|
And response body will be CRLF ended lines
|
119
119
|
"""
|
120
|
-
ftp://example.com:
|
120
|
+
ftp://example.com:421/image/b0fe25319ba5909aa97fded546847a96d7fdf26e18715b0cfccfcbee52dce57e.jpg
|
121
121
|
"""
|
122
122
|
|
data/httpimagestore.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "httpimagestore"
|
8
|
-
s.version = "1.8.
|
8
|
+
s.version = "1.8.1"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Jakub Pastuszek"]
|
12
|
-
s.date = "2014-08-
|
12
|
+
s.date = "2014-08-28"
|
13
13
|
s.description = "Thumbnails images using httpthumbnailer and stored data on HTTP server (S3)"
|
14
14
|
s.email = "jpastuszek@gmail.com"
|
15
15
|
s.executables = ["httpimagestore"]
|
@@ -98,6 +98,7 @@ Gem::Specification.new do |s|
|
|
98
98
|
s.add_runtime_dependency(%q<mime-types>, ["~> 1.17"])
|
99
99
|
s.add_runtime_dependency(%q<sdl4r>, ["~> 0.9"])
|
100
100
|
s.add_runtime_dependency(%q<msgpack>, ["~> 0.5"])
|
101
|
+
s.add_runtime_dependency(%q<addressable>, ["~> 2.3"])
|
101
102
|
s.add_development_dependency(%q<faraday>, [">= 0.8"])
|
102
103
|
s.add_development_dependency(%q<rspec>, ["~> 2.13"])
|
103
104
|
s.add_development_dependency(%q<cucumber>, [">= 0"])
|
@@ -113,6 +114,7 @@ Gem::Specification.new do |s|
|
|
113
114
|
s.add_dependency(%q<mime-types>, ["~> 1.17"])
|
114
115
|
s.add_dependency(%q<sdl4r>, ["~> 0.9"])
|
115
116
|
s.add_dependency(%q<msgpack>, ["~> 0.5"])
|
117
|
+
s.add_dependency(%q<addressable>, ["~> 2.3"])
|
116
118
|
s.add_dependency(%q<faraday>, [">= 0.8"])
|
117
119
|
s.add_dependency(%q<rspec>, ["~> 2.13"])
|
118
120
|
s.add_dependency(%q<cucumber>, [">= 0"])
|
@@ -129,6 +131,7 @@ Gem::Specification.new do |s|
|
|
129
131
|
s.add_dependency(%q<mime-types>, ["~> 1.17"])
|
130
132
|
s.add_dependency(%q<sdl4r>, ["~> 0.9"])
|
131
133
|
s.add_dependency(%q<msgpack>, ["~> 0.5"])
|
134
|
+
s.add_dependency(%q<addressable>, ["~> 2.3"])
|
132
135
|
s.add_dependency(%q<faraday>, [">= 0.8"])
|
133
136
|
s.add_dependency(%q<rspec>, ["~> 2.13"])
|
134
137
|
s.add_dependency(%q<cucumber>, [">= 0"])
|
@@ -2,7 +2,7 @@ require 'httpimagestore/configuration/path'
|
|
2
2
|
require 'httpimagestore/configuration/handler'
|
3
3
|
require 'httpimagestore/configuration/source_failover'
|
4
4
|
require 'pathname'
|
5
|
-
require 'uri'
|
5
|
+
require 'addressable/uri'
|
6
6
|
|
7
7
|
module Configuration
|
8
8
|
class FileStorageOutsideOfRootDirError < ConfigurationError
|
@@ -42,22 +42,27 @@ module Configuration
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def initialize(global, image_name, matcher, root_dir, path_spec)
|
45
|
-
super global, image_name, matcher
|
45
|
+
super global, image_name, matcher, path_spec
|
46
46
|
@root_dir = Pathname.new(root_dir).cleanpath
|
47
|
-
@path_spec = path_spec
|
48
47
|
end
|
49
48
|
|
50
49
|
def storage_path(rendered_path)
|
51
50
|
path = Pathname.new(rendered_path)
|
52
51
|
|
53
52
|
storage_path = (@root_dir + path).cleanpath
|
54
|
-
storage_path.to_s =~ /^#{@root_dir.to_s}/ or raise FileStorageOutsideOfRootDirError.new(
|
53
|
+
storage_path.to_s =~ /^#{@root_dir.to_s}/ or raise FileStorageOutsideOfRootDirError.new(image_name, path)
|
55
54
|
|
56
55
|
storage_path
|
57
56
|
end
|
58
57
|
|
58
|
+
def file_url(rendered_path)
|
59
|
+
uri = rendered_path.to_uri
|
60
|
+
uri.scheme = 'file'
|
61
|
+
uri
|
62
|
+
end
|
63
|
+
|
59
64
|
def to_s
|
60
|
-
"FileSource[image_name: '#{
|
65
|
+
"FileSource[image_name: '#{image_name}' root_dir: '#{@root_dir}' path_spec: '#{path_spec}']"
|
61
66
|
end
|
62
67
|
end
|
63
68
|
|
@@ -86,7 +91,7 @@ module Configuration
|
|
86
91
|
FileSourceStoreBase.stats.incr_total_file_source_bytes(data.bytesize)
|
87
92
|
|
88
93
|
image = Image.new(data)
|
89
|
-
image.source_url =
|
94
|
+
image.source_url = file_url(rendered_path)
|
90
95
|
image
|
91
96
|
rescue Errno::ENOENT
|
92
97
|
raise NoSuchFileError.new(image_name, rendered_path)
|
@@ -112,7 +117,7 @@ module Configuration
|
|
112
117
|
get_named_image_for_storage(request_state) do |image_name, image, rendered_path|
|
113
118
|
storage_path = storage_path(rendered_path)
|
114
119
|
|
115
|
-
image.store_url =
|
120
|
+
image.store_url = file_url(rendered_path)
|
116
121
|
|
117
122
|
log.info "storing '#{image_name}' in file '#{storage_path}' (#{image.data.length} bytes)"
|
118
123
|
storage_path.open('wb'){|io| io.write image.data}
|
@@ -85,7 +85,8 @@ module Configuration
|
|
85
85
|
attr_reader :memory_limit
|
86
86
|
attr_reader :headers
|
87
87
|
|
88
|
-
def with_locals(locals)
|
88
|
+
def with_locals(*locals)
|
89
|
+
locals = locals.reduce{|a, b| a.merge(b)}
|
89
90
|
log.debug "using additional local variables: #{locals}"
|
90
91
|
self.dup.merge!(locals)
|
91
92
|
end
|
@@ -98,10 +99,6 @@ module Configuration
|
|
98
99
|
@output_callback or fail 'no output callback'
|
99
100
|
end
|
100
101
|
|
101
|
-
def render_template(template)
|
102
|
-
RubyStringTemplate.new(template).render(self)
|
103
|
-
end
|
104
|
-
|
105
102
|
def fetch_base_variable(name, base_name)
|
106
103
|
fetch(base_name, nil) or generate_meta_variable(base_name) or raise NoVariableToGenerateMetaVariableError.new(base_name, name)
|
107
104
|
end
|
@@ -195,51 +192,86 @@ module Configuration
|
|
195
192
|
end
|
196
193
|
end
|
197
194
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
end
|
195
|
+
class HandlerStatement
|
196
|
+
module ImageName
|
197
|
+
attr_reader :image_name
|
202
198
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
199
|
+
def initialize(global, *args)
|
200
|
+
@image_name = args.pop
|
201
|
+
|
202
|
+
super(global, *args)
|
203
|
+
|
204
|
+
config_local :imagename, @image_name # deprecated
|
205
|
+
config_local :image_name, @image_name
|
207
206
|
end
|
208
207
|
end
|
209
208
|
|
210
|
-
|
211
|
-
|
209
|
+
module PathSpec
|
210
|
+
attr_reader :path_spec
|
211
|
+
|
212
|
+
def initialize(global, *args)
|
213
|
+
@path_spec = args.pop
|
214
|
+
super(global, *args)
|
215
|
+
end
|
216
|
+
|
217
|
+
def path_template
|
218
|
+
@global.paths[@path_spec]
|
219
|
+
end
|
212
220
|
end
|
213
|
-
end
|
214
221
|
|
215
|
-
|
216
|
-
|
222
|
+
module ConditionalInclusion
|
223
|
+
def initialize(global, *args)
|
224
|
+
@matchers = []
|
225
|
+
matcher = args.pop
|
226
|
+
@matchers << matcher if matcher
|
227
|
+
super(global, *args)
|
228
|
+
end
|
217
229
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
230
|
+
def inclusion_matcher(matcher)
|
231
|
+
@matchers << matcher
|
232
|
+
end
|
233
|
+
|
234
|
+
def included?(request_state)
|
235
|
+
return true if @matchers.empty?
|
236
|
+
@matchers.any? do |matcher|
|
237
|
+
matcher.included?(request_state)
|
238
|
+
end
|
239
|
+
end
|
222
240
|
|
223
|
-
|
224
|
-
|
225
|
-
|
241
|
+
def excluded?(request_state)
|
242
|
+
not included? request_state
|
243
|
+
end
|
226
244
|
end
|
227
245
|
|
228
|
-
|
246
|
+
def initialize(global, *args)
|
247
|
+
@global = global
|
248
|
+
@config_locals = {}
|
249
|
+
@module_args = args
|
250
|
+
end
|
229
251
|
|
230
|
-
|
252
|
+
attr_reader :config_locals
|
253
|
+
def config_local(name, value)
|
254
|
+
@config_locals[name] = value
|
255
|
+
end
|
231
256
|
|
232
|
-
def
|
233
|
-
@
|
257
|
+
def path_template(path_spec)
|
258
|
+
@global.paths[path_spec]
|
234
259
|
end
|
260
|
+
end
|
235
261
|
|
236
|
-
|
237
|
-
|
238
|
-
|
262
|
+
class SourceStoreBase < HandlerStatement
|
263
|
+
include ImageName
|
264
|
+
include PathSpec
|
265
|
+
include ConditionalInclusion
|
266
|
+
|
267
|
+
def initialize(global, image_name, matcher, path_spec)
|
268
|
+
super(global, image_name, path_spec, matcher)
|
239
269
|
end
|
240
270
|
|
271
|
+
private
|
272
|
+
|
241
273
|
def put_sourced_named_image(request_state)
|
242
|
-
rendered_path =
|
274
|
+
rendered_path = path_template.render(request_state.with_locals(config_locals))
|
243
275
|
|
244
276
|
image = yield @image_name, rendered_path
|
245
277
|
|
@@ -249,7 +281,7 @@ module Configuration
|
|
249
281
|
|
250
282
|
def get_named_image_for_storage(request_state)
|
251
283
|
image = request_state.images[@image_name]
|
252
|
-
rendered_path =
|
284
|
+
rendered_path = path_template.render(request_state.with_locals(config_locals))
|
253
285
|
image.store_path = rendered_path
|
254
286
|
|
255
287
|
yield @image_name, image, rendered_path
|
@@ -3,7 +3,7 @@ require 'httpimagestore/ruby_string_template'
|
|
3
3
|
require 'httpimagestore/configuration/handler'
|
4
4
|
|
5
5
|
module Configuration
|
6
|
-
class Identify
|
6
|
+
class Identify < HandlerStatement
|
7
7
|
include ClassLogging
|
8
8
|
|
9
9
|
extend Stats
|
@@ -12,8 +12,6 @@ module Configuration
|
|
12
12
|
:total_identify_requests_bytes
|
13
13
|
)
|
14
14
|
|
15
|
-
include ConditionalInclusion
|
16
|
-
|
17
15
|
def self.match(node)
|
18
16
|
node.name == 'identify'
|
19
17
|
end
|
@@ -27,10 +25,11 @@ module Configuration
|
|
27
25
|
configuration.processors << self.new(configuration.global, image_name, matcher)
|
28
26
|
end
|
29
27
|
|
28
|
+
include ImageName
|
29
|
+
include ConditionalInclusion
|
30
|
+
|
30
31
|
def initialize(global, image_name, matcher = nil)
|
31
|
-
|
32
|
-
@image_name = image_name
|
33
|
-
inclusion_matcher matcher if matcher
|
32
|
+
super(global, image_name, matcher)
|
34
33
|
end
|
35
34
|
|
36
35
|
def realize(request_state)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'httpimagestore/configuration/handler'
|
2
2
|
require 'httpimagestore/ruby_string_template'
|
3
|
-
require 'uri'
|
3
|
+
require 'addressable/uri'
|
4
4
|
require 'base64'
|
5
5
|
|
6
6
|
module Configuration
|
@@ -64,60 +64,47 @@ module Configuration
|
|
64
64
|
Handler::register_node_parser OutputText
|
65
65
|
|
66
66
|
class OutputMultiBase
|
67
|
-
class OutputSpec
|
67
|
+
class OutputSpec < HandlerStatement
|
68
|
+
include ImageName
|
69
|
+
include PathSpec
|
68
70
|
include ConditionalInclusion
|
69
|
-
attr_reader :image_name
|
70
|
-
attr_reader :path_spec
|
71
71
|
|
72
72
|
def initialize(global, image_name, scheme, host, port, path_spec, matcher)
|
73
|
-
|
74
|
-
@
|
75
|
-
@
|
76
|
-
@
|
77
|
-
@port = port
|
78
|
-
@path_spec = path_spec
|
79
|
-
inclusion_matcher matcher
|
73
|
+
super(global, image_name, path_spec, matcher)
|
74
|
+
@scheme = scheme && scheme.to_template
|
75
|
+
@host = host && host.to_template
|
76
|
+
@port = port && port.to_template
|
80
77
|
end
|
81
78
|
|
82
79
|
def store_path(request_state)
|
83
80
|
store_path = request_state.images[@image_name].store_path or raise StorePathNotSetForImage.new(@image_name)
|
84
81
|
return store_path unless @path_spec
|
85
82
|
|
86
|
-
|
87
|
-
image_name: @image_name,
|
88
|
-
path: store_path
|
89
|
-
}
|
90
|
-
|
91
|
-
rendered_path(request_state.with_locals(locals))
|
83
|
+
path_template.render(request_state.with_locals(config_locals, path: store_path))
|
92
84
|
end
|
93
85
|
|
94
86
|
def store_url(request_state)
|
95
87
|
url = request_state.images[@image_name].store_url or raise StoreURLNotSetForImage.new(@image_name)
|
96
88
|
url = url.dup
|
89
|
+
store_path = request_state.images[@image_name].store_path or raise StorePathNotSetForImage.new(@image_name)
|
97
90
|
|
98
|
-
|
99
|
-
|
100
|
-
path: URI.utf_decode(url.path),
|
91
|
+
request_locals = {
|
92
|
+
path: store_path,
|
101
93
|
url: url.to_s
|
102
94
|
}
|
103
|
-
|
104
|
-
|
105
|
-
|
95
|
+
request_locals[:scheme] = url.scheme if url.scheme
|
96
|
+
request_locals[:host] = url.host if url.host
|
97
|
+
request_locals[:port] = url.port if url.port
|
106
98
|
|
107
|
-
request_state = request_state.with_locals(
|
99
|
+
request_state = request_state.with_locals(config_locals, request_locals)
|
108
100
|
|
109
101
|
# optional rewrites
|
110
|
-
url.scheme =
|
111
|
-
url.host =
|
112
|
-
url.port =
|
113
|
-
url.path =
|
114
|
-
|
115
|
-
url
|
116
|
-
end
|
102
|
+
url.scheme = @scheme.render(request_state) if @scheme
|
103
|
+
url.host = @host.render(request_state) if @host
|
104
|
+
(url.host ||= 'localhost'; url.port = @port.render(request_state).to_i) if @port
|
105
|
+
url.path = path_template.render(request_state).to_uri if @path_spec
|
117
106
|
|
118
|
-
|
119
|
-
def rendered_path(request_state)
|
120
|
-
Pathname.new(@global.paths[@path_spec].render(request_state)).cleanpath.to_s
|
107
|
+
url.normalize
|
121
108
|
end
|
122
109
|
end
|
123
110
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'httpimagestore/ruby_string_template'
|
2
2
|
require 'digest/sha2'
|
3
|
+
require 'addressable/uri'
|
3
4
|
|
4
5
|
module Configuration
|
5
6
|
class PathNotDefinedError < ConfigurationError
|
@@ -21,6 +22,14 @@ module Configuration
|
|
21
22
|
end
|
22
23
|
|
23
24
|
class Path < RubyStringTemplate
|
25
|
+
class RenderedPath < String
|
26
|
+
def to_uri
|
27
|
+
uri_path = self.gsub(/^\/?([a-zA-Z])[\|:][\\\/]/){"/#{$1.downcase}:/"} # fix windows backslash
|
28
|
+
uri_path = Addressable::URI::SLASH + uri_path if uri_path[0] != Addressable::URI::SLASH # make absolute
|
29
|
+
Addressable::URI.new(path: uri_path).normalize
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
24
33
|
def self.match(node)
|
25
34
|
node.name == 'path'
|
26
35
|
end
|
@@ -50,6 +59,10 @@ module Configuration
|
|
50
59
|
end or raise NoValueForPathTemplatePlaceholerError.new(path_name, template, name)
|
51
60
|
end
|
52
61
|
end
|
62
|
+
|
63
|
+
def render(locals = {})
|
64
|
+
RenderedPath.new(super)
|
65
|
+
end
|
53
66
|
end
|
54
67
|
Global.register_node_parser Path
|
55
68
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'aws-sdk'
|
2
2
|
require 'digest/sha2'
|
3
3
|
require 'msgpack'
|
4
|
+
require 'addressable/uri'
|
4
5
|
require 'httpimagestore/aws_sdk_regions_hack'
|
5
6
|
require 'httpimagestore/configuration/path'
|
6
7
|
require 'httpimagestore/configuration/handler'
|
@@ -177,7 +178,7 @@ module Configuration
|
|
177
178
|
end
|
178
179
|
|
179
180
|
def private_url
|
180
|
-
s3_object.url_for(:read, expires: 60 * 60 * 24 * 365 * 20)
|
181
|
+
s3_object.url_for(:read, expires: 60 * 60 * 24 * 365 * 20)
|
181
182
|
end
|
182
183
|
|
183
184
|
def public_url
|
@@ -237,19 +238,19 @@ module Configuration
|
|
237
238
|
end
|
238
239
|
|
239
240
|
def private_url
|
240
|
-
url = @cache_file.header['private_url'] and return URI(url)
|
241
|
+
url = @cache_file.header['private_url'] and return Addressable::URI.parse(url)
|
241
242
|
dirty! :private_url
|
242
243
|
url = super
|
243
244
|
@cache_file.header['private_url'] = url.to_s
|
244
|
-
url
|
245
|
+
Addressable::URI.parse(url)
|
245
246
|
end
|
246
247
|
|
247
248
|
def public_url
|
248
|
-
url = @cache_file.header['public_url'] and return URI(url)
|
249
|
+
url = @cache_file.header['public_url'] and return Addressable::URI.parse(url)
|
249
250
|
dirty! :public_url
|
250
251
|
url = super
|
251
252
|
@cache_file.header['public_url'] = url.to_s
|
252
|
-
url
|
253
|
+
Addressable::URI.parse(url)
|
253
254
|
end
|
254
255
|
|
255
256
|
def content_type
|
@@ -314,9 +315,8 @@ module Configuration
|
|
314
315
|
end
|
315
316
|
|
316
317
|
def initialize(global, image_name, matcher, bucket, path_spec, public_access, cache_control, prefix, cache_root)
|
317
|
-
super global, image_name, matcher
|
318
|
+
super global, image_name, matcher, path_spec
|
318
319
|
@bucket = bucket
|
319
|
-
@path_spec = path_spec
|
320
320
|
@public_access = public_access
|
321
321
|
@cache_control = cache_control
|
322
322
|
@prefix = prefix
|
@@ -333,7 +333,7 @@ module Configuration
|
|
333
333
|
log.warn "not using S3 object cache for image '#{image_name}'", error
|
334
334
|
end
|
335
335
|
|
336
|
-
|
336
|
+
config_local :bucket, @bucket
|
337
337
|
end
|
338
338
|
|
339
339
|
def client
|
@@ -406,7 +406,7 @@ module Configuration
|
|
406
406
|
end
|
407
407
|
|
408
408
|
def to_s
|
409
|
-
"S3Source[image_name: '#{
|
409
|
+
"S3Source[image_name: '#{image_name}' bucket: '#{@bucket}' prefix: '#{@prefix}' path_spec: '#{path_spec}']"
|
410
410
|
end
|
411
411
|
end
|
412
412
|
Handler::register_node_parser S3Source
|
@@ -31,7 +31,13 @@ module Configuration
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
class
|
34
|
+
class NoSpecSelectedError < RuntimeError
|
35
|
+
def initialize(specs)
|
36
|
+
super "no thumbnailing specs were selected, please use at least one of: #{specs.join(', ')}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Thumbnail < HandlerStatement
|
35
41
|
include ClassLogging
|
36
42
|
|
37
43
|
extend Stats
|
@@ -55,34 +61,27 @@ module Configuration
|
|
55
61
|
attr_reader :remote_error
|
56
62
|
end
|
57
63
|
|
58
|
-
class ThumbnailSpec
|
59
|
-
|
60
|
-
def initialize(image_name, sepc_name, template)
|
61
|
-
super(template) do |locals, name|
|
62
|
-
locals[name] or raise NoValueForSpecTemplatePlaceholerError.new(image_name, sepc_name, name, template)
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
64
|
+
class ThumbnailSpec < HandlerStatement
|
65
|
+
include ImageName
|
67
66
|
include ConditionalInclusion
|
68
67
|
|
69
68
|
def initialize(image_name, method, width, height, format, options = {}, matcher = nil)
|
70
|
-
|
71
|
-
@method =
|
72
|
-
@width =
|
73
|
-
@height =
|
74
|
-
@format =
|
75
|
-
|
76
|
-
|
69
|
+
super(nil, image_name, matcher)
|
70
|
+
@method = method.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholerError.new(image_name, 'method', key, method)}
|
71
|
+
@width = width.to_s.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholerError.new(image_name, 'width', key, width)}
|
72
|
+
@height = height.to_s.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholerError.new(image_name, 'height', key, height)}
|
73
|
+
@format = format.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholerError.new(image_name, 'format', key, format)}
|
74
|
+
|
75
|
+
@options = options.merge(options) do |option, old, template|
|
76
|
+
template.to_s.to_template.with_missing_resolver{|locals, field| raise NoValueForSpecTemplatePlaceholerError.new(image_name, option, field, template)}
|
77
|
+
end
|
77
78
|
end
|
78
79
|
|
79
|
-
attr_reader :image_name
|
80
|
-
|
81
80
|
def render(locals = {})
|
82
81
|
options = @options.inject({}){|h, v| h[v.first] = v.last.render(locals); h}
|
83
82
|
nested_options = options['options'] ? Hash[options.delete('options').to_s.split(',').map{|pair| pair.split(':', 2)}] : {}
|
84
83
|
{
|
85
|
-
|
84
|
+
image_name =>
|
86
85
|
[
|
87
86
|
@method.render(locals),
|
88
87
|
@width.render(locals),
|
@@ -94,8 +93,6 @@ module Configuration
|
|
94
93
|
end
|
95
94
|
end
|
96
95
|
|
97
|
-
include ConditionalInclusion
|
98
|
-
|
99
96
|
def self.match(node)
|
100
97
|
node.name == 'thumbnail'
|
101
98
|
end
|
@@ -142,12 +139,13 @@ module Configuration
|
|
142
139
|
)
|
143
140
|
end
|
144
141
|
|
142
|
+
include ConditionalInclusion
|
143
|
+
|
145
144
|
def initialize(global, source_image_name, specs, use_multipart_api, matcher)
|
146
|
-
|
145
|
+
super(global, matcher)
|
147
146
|
@source_image_name = source_image_name
|
148
147
|
@specs = specs
|
149
148
|
@use_multipart_api = use_multipart_api
|
150
|
-
inclusion_matcher matcher
|
151
149
|
end
|
152
150
|
|
153
151
|
def realize(request_state)
|
@@ -159,6 +157,9 @@ module Configuration
|
|
159
157
|
end.each do |spec|
|
160
158
|
rendered_specs.merge! spec.render(request_state)
|
161
159
|
end
|
160
|
+
|
161
|
+
rendered_specs.empty? and raise NoSpecSelectedError.new(@specs.map(&:image_name))
|
162
|
+
|
162
163
|
source_image = request_state.images[@source_image_name]
|
163
164
|
|
164
165
|
thumbnails = {}
|
@@ -16,7 +16,10 @@ class ErrorReporter < Controller
|
|
16
16
|
write_error status, error
|
17
17
|
end
|
18
18
|
|
19
|
-
on error
|
19
|
+
on error(
|
20
|
+
Configuration::ZeroBodyLengthError,
|
21
|
+
Configuration::NoSpecSelectedError
|
22
|
+
) do |error|
|
20
23
|
write_error 400, error
|
21
24
|
end
|
22
25
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
class RubyStringTemplate
|
1
|
+
class RubyStringTemplate < String
|
2
2
|
class NoValueForTemplatePlaceholderError < ArgumentError
|
3
3
|
def initialize(name, template)
|
4
4
|
super "no value for '\#{#{name}}' in template '#{template}'"
|
@@ -6,18 +6,32 @@ class RubyStringTemplate
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def initialize(template, &resolver)
|
9
|
-
|
10
|
-
@
|
9
|
+
super(template.to_s)
|
10
|
+
@resolvers = []
|
11
|
+
@resolvers << resolver if resolver
|
12
|
+
@resolvers << ->(locals, name){locals[name]}
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize_copy(source)
|
16
|
+
super
|
17
|
+
# copy resolvers array
|
18
|
+
resolvers =
|
19
|
+
source.instance_eval do
|
20
|
+
resolvers = @resolvers
|
21
|
+
end
|
22
|
+
@resolvers = resolvers.dup
|
11
23
|
end
|
12
24
|
|
13
25
|
def render(locals = {})
|
14
|
-
template =
|
26
|
+
template = self.to_s
|
15
27
|
values = {}
|
16
28
|
|
29
|
+
# use gsub with block instead!
|
17
30
|
template.scan(/#\{[^\}]+\}/um).uniq.each do |placeholder|
|
18
31
|
name = placeholder.match(/#\{([^\}]*)\}/u).captures.first.to_sym
|
19
|
-
|
20
|
-
value
|
32
|
+
|
33
|
+
value = nil
|
34
|
+
@resolvers.find{|resolver| value = resolver.call(locals, name)} or fail NoValueForTemplatePlaceholderError.new(name, self)
|
21
35
|
values[placeholder] = value.to_s
|
22
36
|
end
|
23
37
|
|
@@ -27,5 +41,28 @@ class RubyStringTemplate
|
|
27
41
|
|
28
42
|
template
|
29
43
|
end
|
44
|
+
|
45
|
+
def to_template
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
#def inspect
|
50
|
+
#super + "[#{@resolvers}]"
|
51
|
+
#end
|
52
|
+
|
53
|
+
def add_missing_resolver(&resolver)
|
54
|
+
@resolvers << resolver
|
55
|
+
end
|
56
|
+
|
57
|
+
def with_missing_resolver(&resolver)
|
58
|
+
new_template = self.dup
|
59
|
+
new_template.add_missing_resolver(&resolver)
|
60
|
+
new_template
|
61
|
+
end
|
30
62
|
end
|
31
63
|
|
64
|
+
class String
|
65
|
+
def to_template
|
66
|
+
RubyStringTemplate.new(self)
|
67
|
+
end
|
68
|
+
end
|
@@ -52,7 +52,7 @@ describe Configuration do
|
|
52
52
|
it 'should have source path and url' do
|
53
53
|
subject.handlers[0].sources[0].realize(state)
|
54
54
|
|
55
|
-
state.images['original'].source_path.should == "test.in"
|
55
|
+
state.images['original'].source_path.to_s.should == "test.in"
|
56
56
|
state.images['original'].source_url.to_s.should == "file:/test.in"
|
57
57
|
end
|
58
58
|
|
@@ -75,7 +75,7 @@ describe Configuration do
|
|
75
75
|
|
76
76
|
it 'should provide image name to be used as #{image_name}' do
|
77
77
|
subject.handlers[0].sources[0].realize(state)
|
78
|
-
state.images['test-image-name'].source_path.should == 'test-image-name.jpg'
|
78
|
+
state.images['test-image-name'].source_path.to_s.should == 'test-image-name.jpg'
|
79
79
|
state.images['test-image-name'].data.should == 'hello world'
|
80
80
|
end
|
81
81
|
end
|
@@ -543,7 +543,7 @@ describe Configuration do
|
|
543
543
|
env.res.data.should == "file://localhost/abc/test.out\r\n"
|
544
544
|
end
|
545
545
|
|
546
|
-
it 'should allow rewriting port component' do
|
546
|
+
it 'should allow rewriting port component (defaults host to localhost)' do
|
547
547
|
subject = Configuration.read(<<-'EOF')
|
548
548
|
path "out" "abc/test.out"
|
549
549
|
|
@@ -560,13 +560,13 @@ describe Configuration do
|
|
560
560
|
|
561
561
|
env.instance_eval &state.output_callback
|
562
562
|
env.res['Content-Type'].should == 'text/uri-list'
|
563
|
-
env.res.data.should == "file
|
563
|
+
env.res.data.should == "file://localhost:21/abc/test.out\r\n"
|
564
564
|
end
|
565
565
|
|
566
566
|
it 'should allow using variables for all supported rewrites' do
|
567
567
|
state = Configuration::RequestState.new('abc',
|
568
568
|
remote: 'example.com',
|
569
|
-
remote_port:
|
569
|
+
remote_port: 421,
|
570
570
|
proto: 'ftp'
|
571
571
|
)
|
572
572
|
subject = Configuration.read(<<-'EOF')
|
@@ -586,7 +586,7 @@ describe Configuration do
|
|
586
586
|
|
587
587
|
env.instance_eval &state.output_callback
|
588
588
|
env.res['Content-Type'].should == 'text/uri-list'
|
589
|
-
env.res.data.should == "ftp://example.com:
|
589
|
+
env.res.data.should == "ftp://example.com:421/hello/abc/world/test-xyz.out\r\n"
|
590
590
|
end
|
591
591
|
end
|
592
592
|
|
@@ -218,7 +218,7 @@ else
|
|
218
218
|
it 'should keep private source URL' do
|
219
219
|
subject.handlers[0].sources[0].realize(state)
|
220
220
|
|
221
|
-
state.images['original'].source_url.should be_a URI
|
221
|
+
state.images['original'].source_url.should be_a Addressable::URI
|
222
222
|
state.images['original'].source_url.to_s.should == 'https://s3-eu-west-1.amazonaws.com/test/ghost.jpg?' + ENV['AWS_ACCESS_KEY_ID']
|
223
223
|
end
|
224
224
|
|
@@ -232,7 +232,7 @@ else
|
|
232
232
|
EOF
|
233
233
|
subject.handlers[0].sources[0].realize(state)
|
234
234
|
|
235
|
-
state.images['original'].source_url.should be_a URI
|
235
|
+
state.images['original'].source_url.should be_a Addressable::URI
|
236
236
|
state.images['original'].source_url.to_s.should == 'https://s3-eu-west-1.amazonaws.com/test/ghost.jpg'
|
237
237
|
end
|
238
238
|
|
@@ -1,8 +1,8 @@
|
|
1
1
|
require_relative 'spec_helper'
|
2
2
|
require 'httpimagestore/configuration'
|
3
|
-
require 'httpimagestore/configuration/output'
|
4
3
|
MemoryLimit.logger = Configuration::Scope.logger = RootLogger.new('/dev/null')
|
5
4
|
|
5
|
+
require 'httpimagestore/configuration/output'
|
6
6
|
require 'httpimagestore/configuration/thumbnailer'
|
7
7
|
|
8
8
|
describe Configuration do
|
@@ -42,6 +42,20 @@ describe RubyStringTemplate do
|
|
42
42
|
subject.render(hello: 'hello', test: 123)
|
43
43
|
}.to raise_error RubyStringTemplate::NoValueForTemplatePlaceholderError, %q{no value for '#{world}' in template '>#{hello}-#{world}#{test}<'}
|
44
44
|
end
|
45
|
+
|
46
|
+
it 'should allow using custom missing value resolver' do
|
47
|
+
template = subject
|
48
|
+
expect {
|
49
|
+
template.with_missing_resolver do |locals, key|
|
50
|
+
fail "missing key: #{key}"
|
51
|
+
end
|
52
|
+
.render(hello: 'hello', test: 123)
|
53
|
+
}.to raise_error RuntimeError, "missing key: world"
|
54
|
+
|
55
|
+
expect {
|
56
|
+
template.render(hello: 'hello', test: 123)
|
57
|
+
}.to raise_error RubyStringTemplate::NoValueForTemplatePlaceholderError, %q{no value for '#{world}' in template '>#{hello}-#{world}#{test}<'}
|
58
|
+
end
|
45
59
|
end
|
46
60
|
end
|
47
61
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: httpimagestore
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.8.
|
4
|
+
version: 1.8.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-08-
|
12
|
+
date: 2014-08-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: unicorn-cuba-base
|
@@ -107,6 +107,22 @@ dependencies:
|
|
107
107
|
- - ~>
|
108
108
|
- !ruby/object:Gem::Version
|
109
109
|
version: '0.5'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: addressable
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '2.3'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ~>
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '2.3'
|
110
126
|
- !ruby/object:Gem::Dependency
|
111
127
|
name: faraday
|
112
128
|
requirement: !ruby/object:Gem::Requirement
|
@@ -323,7 +339,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
323
339
|
version: '0'
|
324
340
|
segments:
|
325
341
|
- 0
|
326
|
-
hash:
|
342
|
+
hash: 3314645469105412308
|
327
343
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
328
344
|
none: false
|
329
345
|
requirements:
|