httpimagestore 1.8.1 → 1.9.0
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.
- 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
|
|