httpimagestore 1.8.1 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/Gemfile +7 -7
- data/Gemfile.lock +20 -20
- data/README.md +165 -37
- data/Rakefile +7 -2
- data/VERSION +1 -1
- data/bin/httpimagestore +74 -41
- data/lib/httpimagestore/configuration/file.rb +20 -11
- data/lib/httpimagestore/configuration/handler.rb +96 -257
- data/lib/httpimagestore/configuration/handler/source_store_base.rb +37 -0
- data/lib/httpimagestore/configuration/handler/statement.rb +114 -0
- data/lib/httpimagestore/configuration/identify.rb +17 -9
- data/lib/httpimagestore/configuration/output.rb +33 -61
- data/lib/httpimagestore/configuration/path.rb +2 -2
- data/lib/httpimagestore/configuration/request_state.rb +131 -0
- data/lib/httpimagestore/configuration/s3.rb +41 -29
- data/lib/httpimagestore/configuration/thumbnailer.rb +189 -96
- data/lib/httpimagestore/configuration/validate_hmac.rb +170 -0
- data/lib/httpimagestore/error_reporter.rb +6 -1
- data/lib/httpimagestore/ruby_string_template.rb +10 -19
- metadata +40 -102
- data/.rspec +0 -1
- data/features/cache-control.feature +0 -41
- data/features/compatibility.feature +0 -165
- data/features/data-uri.feature +0 -55
- data/features/encoding.feature +0 -103
- data/features/error-reporting.feature +0 -281
- data/features/flexi.feature +0 -259
- data/features/health-check.feature +0 -29
- data/features/request-matching.feature +0 -211
- data/features/rewrite.feature +0 -122
- data/features/s3-store-and-thumbnail.feature +0 -82
- data/features/source-failover.feature +0 -71
- data/features/step_definitions/httpimagestore_steps.rb +0 -203
- data/features/storage.feature +0 -198
- data/features/support/env.rb +0 -116
- data/features/support/test-large.jpg +0 -0
- data/features/support/test.empty +0 -0
- data/features/support/test.jpg +0 -0
- data/features/support/test.png +0 -0
- data/features/support/test.txt +0 -1
- data/features/support/tiny.png +0 -0
- data/features/xid-forwarding.feature +0 -49
- data/httpimagestore.gemspec +0 -145
- data/load_test/load_test.1k.23a022f6e.m1.small-comp.csv +0 -3
- data/load_test/load_test.1k.ec9bde794.m1.small.csv +0 -4
- data/load_test/load_test.jmx +0 -317
- data/load_test/thumbnail_specs.csv +0 -11
- data/load_test/thumbnail_specs_v2.csv +0 -10
- data/spec/configuration_file_spec.rb +0 -333
- data/spec/configuration_handler_spec.rb +0 -255
- data/spec/configuration_identify_spec.rb +0 -67
- data/spec/configuration_output_spec.rb +0 -821
- data/spec/configuration_path_spec.rb +0 -138
- data/spec/configuration_s3_spec.rb +0 -911
- data/spec/configuration_source_failover_spec.rb +0 -101
- data/spec/configuration_spec.rb +0 -90
- data/spec/configuration_thumbnailer_spec.rb +0 -483
- data/spec/ruby_string_template_spec.rb +0 -61
- data/spec/spec_helper.rb +0 -89
- data/spec/support/compute.jpg +0 -0
- data/spec/support/cuba_response_env.rb +0 -40
- data/spec/support/full.cfg +0 -183
- data/spec/support/utf_string.txt +0 -1
data/Rakefile
CHANGED
@@ -17,10 +17,15 @@ Jeweler::Tasks.new do |gem|
|
|
17
17
|
gem.name = "httpimagestore"
|
18
18
|
gem.homepage = "http://github.com/jpastuszek/httpimagestore"
|
19
19
|
gem.license = "MIT"
|
20
|
-
gem.summary = %Q{HTTP
|
21
|
-
gem.description = %Q{
|
20
|
+
gem.summary = %Q{HTTP API server for image thumbnailing and storage}
|
21
|
+
gem.description = %Q{Configurable S3 or file system image storage and processing HTTP API server. It is using HTTP Thumbnailer as image processing backend.}
|
22
22
|
gem.email = "jpastuszek@gmail.com"
|
23
23
|
gem.authors = ["Jakub Pastuszek"]
|
24
|
+
gem.files.exclude "features/**/*"
|
25
|
+
gem.files.exclude "gatling/**/*"
|
26
|
+
gem.files.exclude "spec/**/*"
|
27
|
+
gem.files.exclude "*.gemspec"
|
28
|
+
gem.files.exclude ".rspec"
|
24
29
|
# dependencies defined in Gemfile
|
25
30
|
end
|
26
31
|
Jeweler::RubygemsDotOrgTasks.new
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.9.0
|
data/bin/httpimagestore
CHANGED
@@ -10,7 +10,7 @@ Application.new('httpimagestore', port: 3000, processor_count_factor: 2) do
|
|
10
10
|
argument :config,
|
11
11
|
cast: Pathname,
|
12
12
|
description: 'configuration file path'
|
13
|
-
version
|
13
|
+
version((Pathname.new(__FILE__).dirname + '..' + 'VERSION').read)
|
14
14
|
end
|
15
15
|
|
16
16
|
settings do |settings|
|
@@ -21,6 +21,7 @@ Application.new('httpimagestore', port: 3000, processor_count_factor: 2) do
|
|
21
21
|
require 'httpimagestore/error_reporter'
|
22
22
|
|
23
23
|
class HTTPImageStore < Controller
|
24
|
+
include PerfStats
|
24
25
|
extend Stats
|
25
26
|
def_stats(
|
26
27
|
:workers,
|
@@ -68,48 +69,79 @@ Application.new('httpimagestore', port: 3000, processor_count_factor: 2) do
|
|
68
69
|
|
69
70
|
log.debug{"got request: #{env["REQUEST_METHOD"]} #{env["REQUEST_URI"]}"}
|
70
71
|
env['app.configuration'].handlers.each do |handler|
|
71
|
-
log.debug{"trying handler: #{handler
|
72
|
+
log.debug{"trying handler: #{handler}"}
|
72
73
|
on eval(handler.http_method), *handler.uri_matchers.map{|m| instance_eval(&m.matcher)} do |*args|
|
73
|
-
log.debug{"matched handler: #{handler
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
74
|
+
log.debug{"matched handler: #{handler}"}
|
75
|
+
log.with_meta_context api_method: handler.http_method.upcase, api_handler: handler.to_s do
|
76
|
+
measure "handling request", handler.to_s do
|
77
|
+
# map and decode matched URI segments
|
78
|
+
matches = {}
|
79
|
+
names = handler.uri_matchers
|
80
|
+
.map do |matcher|
|
81
|
+
matcher.names
|
82
|
+
end
|
83
|
+
.flatten
|
84
|
+
|
85
|
+
fail "matched more arguments than named (#{args.length} for #{names.length})" if args.length > names.length
|
86
|
+
fail "matched less arguments than named (#{args.length} for #{names.length})" if args.length < names.length
|
87
|
+
|
88
|
+
names.zip(args)
|
89
|
+
.each do |name, value|
|
90
|
+
fail "name should be a symbol" unless name.is_a? Symbol
|
91
|
+
matches[name] = URI.utf_decode(value)
|
92
|
+
end
|
93
|
+
|
94
|
+
# decode remaining URI components
|
95
|
+
path = (env['PATH_INFO'][1..-1] || '').split('/').map do |part|
|
96
|
+
URI.utf_decode(part)
|
97
|
+
end.join('/')
|
98
|
+
|
99
|
+
# query string already decoded by Rack
|
100
|
+
query_string = req.GET
|
101
|
+
|
102
|
+
# actual request URI
|
103
|
+
request_uri = env['REQUEST_URI']
|
104
|
+
request_headers = env.select{|k,v| k.start_with? 'HTTP_'}.map do |pair|
|
105
|
+
[
|
106
|
+
pair[0].sub(/^HTTP_/, '').gsub('_', '-'),
|
107
|
+
pair[1]
|
108
|
+
]
|
109
|
+
end
|
110
|
+
request_headers = Hash[request_headers]
|
111
|
+
request_headers.delete('VERSION')
|
112
|
+
|
113
|
+
body = measure "reading request body" do
|
114
|
+
req.body.read
|
115
|
+
end
|
116
|
+
|
117
|
+
state = Configuration::RequestState.new(body, matches, path, query_string, request_uri, request_headers, memory_limit, env['xid'] || {})
|
118
|
+
|
119
|
+
measure "validating request" do
|
120
|
+
handler.validators.each do |validator|
|
121
|
+
validator.realize(state) unless validator.respond_to? :excluded? and validator.excluded?(state)
|
122
|
+
end
|
123
|
+
end unless handler.validators.empty?
|
124
|
+
measure "sourcing images" do
|
125
|
+
handler.sources.each do |source|
|
126
|
+
source.realize(state) unless source.respond_to? :excluded? and source.excluded?(state)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
measure "processing images" do
|
130
|
+
handler.processors.each do |processor|
|
131
|
+
processor.realize(state) unless processor.respond_to? :excluded? and processor.excluded?(state)
|
132
|
+
end
|
133
|
+
end unless handler.processors.empty?
|
134
|
+
measure "storing images" do
|
135
|
+
handler.stores.each do |store|
|
136
|
+
store.realize(state) unless store.respond_to? :excluded? and store.excluded?(state)
|
137
|
+
end
|
138
|
+
end unless handler.stores.empty?
|
139
|
+
measure "sending response" do
|
140
|
+
handler.output.realize(state)
|
141
|
+
instance_eval(&state.output_callback)
|
142
|
+
end
|
143
|
+
end
|
109
144
|
end
|
110
|
-
handler.output.realize(state)
|
111
|
-
|
112
|
-
instance_eval &state.output_callback
|
113
145
|
end
|
114
146
|
end
|
115
147
|
|
@@ -144,6 +176,7 @@ Application.new('httpimagestore', port: 3000, processor_count_factor: 2) do
|
|
144
176
|
require 'httpimagestore/configuration/file'
|
145
177
|
require 'httpimagestore/configuration/output'
|
146
178
|
require 'httpimagestore/configuration/s3'
|
179
|
+
require 'httpimagestore/configuration/validate_hmac'
|
147
180
|
|
148
181
|
HTTPImageStore.use Configurator, Configuration.from_file(settings.config)
|
149
182
|
HTTPImageStore
|
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'httpimagestore/configuration/path'
|
2
|
-
require 'httpimagestore/configuration/handler'
|
2
|
+
require 'httpimagestore/configuration/handler/source_store_base'
|
3
3
|
require 'httpimagestore/configuration/source_failover'
|
4
4
|
require 'pathname'
|
5
5
|
require 'addressable/uri'
|
@@ -18,6 +18,7 @@ module Configuration
|
|
18
18
|
end
|
19
19
|
|
20
20
|
class FileSourceStoreBase < SourceStoreBase
|
21
|
+
include PerfStats
|
21
22
|
extend Stats
|
22
23
|
def_stats(
|
23
24
|
:total_file_store,
|
@@ -29,20 +30,24 @@ module Configuration
|
|
29
30
|
def self.parse(configuration, node)
|
30
31
|
image_name = node.grab_values('image name').first
|
31
32
|
node.required_attributes('root', 'path')
|
32
|
-
root_dir, path_spec, if_image_name_on = *node.grab_attributes('root', 'path', 'if-image-name-on')
|
33
|
-
matcher = InclusionMatcher.new(image_name, if_image_name_on)
|
34
33
|
|
35
|
-
|
34
|
+
# TODO: it should be possible to compact that
|
35
|
+
root_dir, path_spec, remaining = *node.grab_attributes_with_remaining('root', 'path')
|
36
|
+
conditions, remaining = *ConditionalInclusion.grab_conditions_with_remaining(remaining)
|
37
|
+
remaining.empty? or raise UnexpectedAttributesError.new(node, remaining)
|
38
|
+
|
39
|
+
file = self.new(
|
36
40
|
configuration.global,
|
37
41
|
image_name,
|
38
|
-
matcher,
|
39
42
|
root_dir,
|
40
43
|
path_spec
|
41
44
|
)
|
45
|
+
file.with_conditions(conditions)
|
46
|
+
file
|
42
47
|
end
|
43
48
|
|
44
|
-
def initialize(global, image_name,
|
45
|
-
super
|
49
|
+
def initialize(global, image_name, root_dir, path_spec)
|
50
|
+
super(global, image_name, path_spec)
|
46
51
|
@root_dir = Pathname.new(root_dir).cleanpath
|
47
52
|
end
|
48
53
|
|
@@ -83,9 +88,11 @@ module Configuration
|
|
83
88
|
|
84
89
|
log.info "sourcing '#{image_name}' from file '#{storage_path}'"
|
85
90
|
begin
|
86
|
-
data =
|
87
|
-
|
88
|
-
|
91
|
+
data = measure "sourcing image from file", image_name do
|
92
|
+
storage_path.open('rb') do |io|
|
93
|
+
request_state.memory_limit.io io
|
94
|
+
io.read
|
95
|
+
end
|
89
96
|
end
|
90
97
|
FileSourceStoreBase.stats.incr_total_file_source
|
91
98
|
FileSourceStoreBase.stats.incr_total_file_source_bytes(data.bytesize)
|
@@ -120,7 +127,9 @@ module Configuration
|
|
120
127
|
image.store_url = file_url(rendered_path)
|
121
128
|
|
122
129
|
log.info "storing '#{image_name}' in file '#{storage_path}' (#{image.data.length} bytes)"
|
123
|
-
|
130
|
+
measure "storing image in file", image_name do
|
131
|
+
storage_path.open('wb'){|io| io.write image.data}
|
132
|
+
end
|
124
133
|
FileSourceStoreBase.stats.incr_total_file_store
|
125
134
|
FileSourceStoreBase.stats.incr_total_file_store_bytes(image.data.bytesize)
|
126
135
|
end
|
@@ -1,6 +1,5 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require 'securerandom'
|
1
|
+
require 'httpimagestore/configuration/request_state'
|
2
|
+
require 'httpimagestore/ruby_string_template'
|
4
3
|
|
5
4
|
module Configuration
|
6
5
|
class ImageNotLoadedError < ConfigurationError
|
@@ -39,123 +38,6 @@ module Configuration
|
|
39
38
|
end
|
40
39
|
end
|
41
40
|
|
42
|
-
class RequestState < Hash
|
43
|
-
include ClassLogging
|
44
|
-
|
45
|
-
class Images < Hash
|
46
|
-
def initialize(memory_limit)
|
47
|
-
@memory_limit = memory_limit
|
48
|
-
super
|
49
|
-
end
|
50
|
-
|
51
|
-
def []=(name, image)
|
52
|
-
if member?(name)
|
53
|
-
@memory_limit.return fetch(name).data.bytesize
|
54
|
-
end
|
55
|
-
super
|
56
|
-
end
|
57
|
-
|
58
|
-
def [](name)
|
59
|
-
fetch(name){|image_name| raise ImageNotLoadedError.new(image_name)}
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def initialize(body = '', matches = {}, path = '', query_string = {}, memory_limit = MemoryLimit.new, headers = {})
|
64
|
-
super() do |request_state, name|
|
65
|
-
# note that request_state may be different object when useing with_locals that creates duplicate
|
66
|
-
request_state[name] = request_state.generate_meta_variable(name) or raise VariableNotDefinedError.new(name)
|
67
|
-
end
|
68
|
-
|
69
|
-
self[:path] = path
|
70
|
-
merge! matches
|
71
|
-
self[:query_string_options] = query_string.sort.map{|kv| kv.join(':')}.join(',')
|
72
|
-
|
73
|
-
log.debug "processing request with body length: #{body.bytesize} bytes and variables: #{map{|k,v| "#{k}: '#{v}'"}.join(', ')}"
|
74
|
-
|
75
|
-
@body = body
|
76
|
-
@images = Images.new(memory_limit)
|
77
|
-
@memory_limit = memory_limit
|
78
|
-
@output_callback = nil
|
79
|
-
|
80
|
-
@headers = headers
|
81
|
-
end
|
82
|
-
|
83
|
-
attr_reader :body
|
84
|
-
attr_reader :images
|
85
|
-
attr_reader :memory_limit
|
86
|
-
attr_reader :headers
|
87
|
-
|
88
|
-
def with_locals(*locals)
|
89
|
-
locals = locals.reduce{|a, b| a.merge(b)}
|
90
|
-
log.debug "using additional local variables: #{locals}"
|
91
|
-
self.dup.merge!(locals)
|
92
|
-
end
|
93
|
-
|
94
|
-
def output(&callback)
|
95
|
-
@output_callback = callback
|
96
|
-
end
|
97
|
-
|
98
|
-
def output_callback
|
99
|
-
@output_callback or fail 'no output callback'
|
100
|
-
end
|
101
|
-
|
102
|
-
def fetch_base_variable(name, base_name)
|
103
|
-
fetch(base_name, nil) or generate_meta_variable(base_name) or raise NoVariableToGenerateMetaVariableError.new(base_name, name)
|
104
|
-
end
|
105
|
-
|
106
|
-
def generate_meta_variable(name)
|
107
|
-
log.debug "generating meta variable: #{name}"
|
108
|
-
val = case name
|
109
|
-
when :basename
|
110
|
-
path = Pathname.new(fetch_base_variable(name, :path))
|
111
|
-
path.basename(path.extname).to_s
|
112
|
-
when :dirname
|
113
|
-
Pathname.new(fetch_base_variable(name, :path)).dirname.to_s
|
114
|
-
when :extension
|
115
|
-
Pathname.new(fetch_base_variable(name, :path)).extname.delete('.')
|
116
|
-
when :digest # deprecated
|
117
|
-
@body.empty? and raise NoRequestBodyToGenerateMetaVariableError.new(name)
|
118
|
-
Digest::SHA2.new.update(@body).to_s[0,16]
|
119
|
-
when :input_digest
|
120
|
-
@body.empty? and raise NoRequestBodyToGenerateMetaVariableError.new(name)
|
121
|
-
Digest::SHA2.new.update(@body).to_s[0,16]
|
122
|
-
when :input_sha256
|
123
|
-
@body.empty? and raise NoRequestBodyToGenerateMetaVariableError.new(name)
|
124
|
-
Digest::SHA2.new.update(@body).to_s
|
125
|
-
when :input_image_width
|
126
|
-
@images['input'].width or raise NoImageDataForVariableError.new('input', name)
|
127
|
-
when :input_image_height
|
128
|
-
@images['input'].height or raise NoImageDataForVariableError.new('input', name)
|
129
|
-
when :input_image_mime_extension
|
130
|
-
@images['input'].mime_extension or raise NoImageDataForVariableError.new('input', name)
|
131
|
-
when :image_digest
|
132
|
-
Digest::SHA2.new.update(@images[fetch_base_variable(name, :image_name)].data).to_s[0,16]
|
133
|
-
when :image_sha256
|
134
|
-
Digest::SHA2.new.update(@images[fetch_base_variable(name, :image_name)].data).to_s
|
135
|
-
when :mimeextension # deprecated
|
136
|
-
image_name = fetch_base_variable(name, :image_name)
|
137
|
-
@images[image_name].mime_extension or raise NoImageDataForVariableError.new(image_name, name)
|
138
|
-
when :image_mime_extension
|
139
|
-
image_name = fetch_base_variable(name, :image_name)
|
140
|
-
@images[image_name].mime_extension or raise NoImageDataForVariableError.new(image_name, name)
|
141
|
-
when :image_width
|
142
|
-
image_name = fetch_base_variable(name, :image_name)
|
143
|
-
@images[image_name].width or raise NoImageDataForVariableError.new(image_name, name)
|
144
|
-
when :image_height
|
145
|
-
image_name = fetch_base_variable(name, :image_name)
|
146
|
-
@images[image_name].height or raise NoImageDataForVariableError.new(image_name, name)
|
147
|
-
when :uuid
|
148
|
-
SecureRandom.uuid
|
149
|
-
end
|
150
|
-
if val
|
151
|
-
log.debug "generated meta variable '#{name}': #{val}"
|
152
|
-
else
|
153
|
-
log.debug "could not generated meta variable '#{name}'"
|
154
|
-
end
|
155
|
-
val
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
41
|
module ImageMetaData
|
160
42
|
attr_accessor :source_path
|
161
43
|
attr_accessor :source_url
|
@@ -163,9 +45,16 @@ module Configuration
|
|
163
45
|
attr_accessor :store_url
|
164
46
|
|
165
47
|
def mime_extension
|
166
|
-
|
167
|
-
|
168
|
-
|
48
|
+
case mime_type
|
49
|
+
when nil then nil
|
50
|
+
when 'image/jpeg' then 'jpg'
|
51
|
+
when 'image/png' then 'png'
|
52
|
+
when 'image/gif' then 'gif'
|
53
|
+
else
|
54
|
+
# TODO: this does not work well; the resoult may not the most common extension (like 'jpeg')
|
55
|
+
mime = MIME::Types[mime_type].first or return nil
|
56
|
+
mime.preferred_extension
|
57
|
+
end
|
169
58
|
end
|
170
59
|
end
|
171
60
|
|
@@ -180,114 +69,6 @@ module Configuration
|
|
180
69
|
end
|
181
70
|
end
|
182
71
|
|
183
|
-
class InclusionMatcher
|
184
|
-
def initialize(value, template)
|
185
|
-
@value = value
|
186
|
-
@template = RubyStringTemplate.new(template) if template
|
187
|
-
end
|
188
|
-
|
189
|
-
def included?(request_state)
|
190
|
-
return true if not @template
|
191
|
-
@template.render(request_state).split(',').include? @value
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
|
-
class HandlerStatement
|
196
|
-
module ImageName
|
197
|
-
attr_reader :image_name
|
198
|
-
|
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
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
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
|
220
|
-
end
|
221
|
-
|
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
|
229
|
-
|
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
|
240
|
-
|
241
|
-
def excluded?(request_state)
|
242
|
-
not included? request_state
|
243
|
-
end
|
244
|
-
end
|
245
|
-
|
246
|
-
def initialize(global, *args)
|
247
|
-
@global = global
|
248
|
-
@config_locals = {}
|
249
|
-
@module_args = args
|
250
|
-
end
|
251
|
-
|
252
|
-
attr_reader :config_locals
|
253
|
-
def config_local(name, value)
|
254
|
-
@config_locals[name] = value
|
255
|
-
end
|
256
|
-
|
257
|
-
def path_template(path_spec)
|
258
|
-
@global.paths[path_spec]
|
259
|
-
end
|
260
|
-
end
|
261
|
-
|
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)
|
269
|
-
end
|
270
|
-
|
271
|
-
private
|
272
|
-
|
273
|
-
def put_sourced_named_image(request_state)
|
274
|
-
rendered_path = path_template.render(request_state.with_locals(config_locals))
|
275
|
-
|
276
|
-
image = yield @image_name, rendered_path
|
277
|
-
|
278
|
-
image.source_path = rendered_path
|
279
|
-
request_state.images[@image_name] = image
|
280
|
-
end
|
281
|
-
|
282
|
-
def get_named_image_for_storage(request_state)
|
283
|
-
image = request_state.images[@image_name]
|
284
|
-
rendered_path = path_template.render(request_state.with_locals(config_locals))
|
285
|
-
image.store_path = rendered_path
|
286
|
-
|
287
|
-
yield @image_name, image, rendered_path
|
288
|
-
end
|
289
|
-
end
|
290
|
-
|
291
72
|
class Matcher
|
292
73
|
def initialize(names, debug_type = '', debug_value = '', &matcher)
|
293
74
|
@names = names
|
@@ -296,8 +77,10 @@ module Configuration
|
|
296
77
|
@debug_value = case debug_value
|
297
78
|
when Regexp
|
298
79
|
"/#{debug_value.source}/"
|
80
|
+
when nil
|
81
|
+
nil
|
299
82
|
else
|
300
|
-
debug_value.
|
83
|
+
debug_value.to_s
|
301
84
|
end
|
302
85
|
end
|
303
86
|
|
@@ -305,15 +88,38 @@ module Configuration
|
|
305
88
|
attr_reader :matcher
|
306
89
|
|
307
90
|
def to_s
|
308
|
-
if @
|
309
|
-
|
91
|
+
if @debug_value
|
92
|
+
if @names.empty?
|
93
|
+
"#{@debug_type}(#{@debug_value})"
|
94
|
+
else
|
95
|
+
"#{@debug_type}(#{@names.join(',')} => #{@debug_value})"
|
96
|
+
end
|
310
97
|
else
|
311
|
-
|
98
|
+
@debug_type
|
312
99
|
end
|
313
100
|
end
|
314
101
|
end
|
315
102
|
|
316
103
|
class Handler < Scope
|
104
|
+
class HandlerConfiguration
|
105
|
+
def initialize(global, http_method, uri_matchers)
|
106
|
+
@global = global
|
107
|
+
@http_method = http_method
|
108
|
+
@uri_matchers = uri_matchers
|
109
|
+
@validators = []
|
110
|
+
@sources = []
|
111
|
+
@processors = []
|
112
|
+
@stores = []
|
113
|
+
@output = nil
|
114
|
+
end
|
115
|
+
|
116
|
+
attr_accessor :global, :http_method, :uri_matchers, :validators, :sources, :processors, :stores, :output
|
117
|
+
|
118
|
+
def to_s
|
119
|
+
"#{@http_method} #{@uri_matchers.join(', ')}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
317
123
|
def self.match(node)
|
318
124
|
node.name == 'put' or
|
319
125
|
node.name == 'post' or
|
@@ -325,20 +131,7 @@ module Configuration
|
|
325
131
|
end
|
326
132
|
|
327
133
|
def self.parse(configuration, node)
|
328
|
-
|
329
|
-
Struct.new(
|
330
|
-
:global,
|
331
|
-
:http_method,
|
332
|
-
:uri_matchers,
|
333
|
-
:sources,
|
334
|
-
:processors,
|
335
|
-
:stores,
|
336
|
-
:output
|
337
|
-
).new
|
338
|
-
|
339
|
-
handler_configuration.global = configuration
|
340
|
-
handler_configuration.http_method = node.name
|
341
|
-
handler_configuration.uri_matchers = node.values.map do |matcher|
|
134
|
+
uri_matchers = node.values.map do |matcher|
|
342
135
|
case matcher
|
343
136
|
# URI matchers
|
344
137
|
when %r{^:([^/]+)/(.*)/$} # :foobar/.*/
|
@@ -381,31 +174,29 @@ module Configuration
|
|
381
174
|
when /^\&([^=]+)=(.+)$/# ?foo=bar
|
382
175
|
name = $1.to_sym
|
383
176
|
value = $2
|
384
|
-
Matcher.new([name], 'QueryKeyValue', "#{
|
177
|
+
Matcher.new([name], 'QueryKeyValue', "#{value}") do
|
385
178
|
->{req.GET[name.to_s] && req.GET[name.to_s] == value && captures.push(req.GET[name.to_s])}
|
386
179
|
end
|
387
180
|
when /^\&:(.+)\?(.*)$/# &:foo?bar
|
388
181
|
name = $1.to_sym
|
389
182
|
default = $2
|
390
|
-
Matcher.new([name], 'QueryKeyDefault', "
|
183
|
+
Matcher.new([name], 'QueryKeyDefault', "<key>|#{default}") do
|
391
184
|
->{captures.push(req.GET[name.to_s] || default)}
|
392
185
|
end
|
393
186
|
when /^\&:(.+)$/# &:foo
|
394
187
|
name = $1.to_sym
|
395
|
-
Matcher.new([name], 'QueryKey', "
|
188
|
+
Matcher.new([name], 'QueryKey', "<key>") do
|
396
189
|
->{req.GET[name.to_s] && captures.push(req.GET[name.to_s])}
|
397
190
|
end
|
398
191
|
# Literal URI segment matcher
|
399
192
|
else # foobar
|
400
|
-
Matcher.new([],
|
193
|
+
Matcher.new([], matcher, nil) do
|
401
194
|
Regexp.escape(matcher)
|
402
195
|
end
|
403
196
|
end
|
404
197
|
end
|
405
|
-
|
406
|
-
handler_configuration
|
407
|
-
handler_configuration.stores = []
|
408
|
-
handler_configuration.output = nil
|
198
|
+
|
199
|
+
handler_configuration = HandlerConfiguration.new(configuration, node.name, uri_matchers)
|
409
200
|
|
410
201
|
node.grab_attributes
|
411
202
|
|
@@ -426,6 +217,54 @@ module Configuration
|
|
426
217
|
end
|
427
218
|
RequestState.logger = Global.logger_for(RequestState)
|
428
219
|
|
220
|
+
class OutputText < Scope
|
221
|
+
def self.match(node)
|
222
|
+
node.name == 'output_text'
|
223
|
+
end
|
224
|
+
|
225
|
+
def self.parse(configuration, node)
|
226
|
+
configuration.output and raise StatementCollisionError.new(node, 'output')
|
227
|
+
text = node.grab_values('text').first
|
228
|
+
status, cache_control = *node.grab_attributes('status', 'cache-control')
|
229
|
+
configuration.output = OutputText.new(text, status || 200, cache_control)
|
230
|
+
end
|
231
|
+
|
232
|
+
def initialize(text, status, cache_control)
|
233
|
+
@text = RubyStringTemplate.new(text || fail("no text?!"))
|
234
|
+
@status = status || 200
|
235
|
+
@cache_control = cache_control
|
236
|
+
end
|
237
|
+
|
238
|
+
def realize(request_state)
|
239
|
+
# make sure variables are available in request context
|
240
|
+
status = @status
|
241
|
+
text = @text.render(request_state)
|
242
|
+
cache_control = @cache_control
|
243
|
+
request_state.output do
|
244
|
+
res['Cache-Control'] = cache_control if cache_control
|
245
|
+
write_plain status.to_i, text.to_s
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
class OutputOK < OutputText
|
251
|
+
def self.match(node)
|
252
|
+
node.name == 'output_ok'
|
253
|
+
end
|
254
|
+
|
255
|
+
def self.parse(configuration, node)
|
256
|
+
configuration.output and raise StatementCollisionError.new(node, 'output')
|
257
|
+
cache_control = node.grab_attributes('cache-control').first
|
258
|
+
configuration.output = OutputOK.new(cache_control)
|
259
|
+
end
|
260
|
+
|
261
|
+
def initialize(cache_control = nil)
|
262
|
+
super 'OK', 200, cache_control
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
429
266
|
Global.register_node_parser Handler
|
267
|
+
Handler::register_node_parser OutputText
|
268
|
+
Handler::register_node_parser OutputOK
|
430
269
|
end
|
431
270
|
|