httpimagestore 1.8.1 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +15 -0
  2. data/Gemfile +7 -7
  3. data/Gemfile.lock +20 -20
  4. data/README.md +165 -37
  5. data/Rakefile +7 -2
  6. data/VERSION +1 -1
  7. data/bin/httpimagestore +74 -41
  8. data/lib/httpimagestore/configuration/file.rb +20 -11
  9. data/lib/httpimagestore/configuration/handler.rb +96 -257
  10. data/lib/httpimagestore/configuration/handler/source_store_base.rb +37 -0
  11. data/lib/httpimagestore/configuration/handler/statement.rb +114 -0
  12. data/lib/httpimagestore/configuration/identify.rb +17 -9
  13. data/lib/httpimagestore/configuration/output.rb +33 -61
  14. data/lib/httpimagestore/configuration/path.rb +2 -2
  15. data/lib/httpimagestore/configuration/request_state.rb +131 -0
  16. data/lib/httpimagestore/configuration/s3.rb +41 -29
  17. data/lib/httpimagestore/configuration/thumbnailer.rb +189 -96
  18. data/lib/httpimagestore/configuration/validate_hmac.rb +170 -0
  19. data/lib/httpimagestore/error_reporter.rb +6 -1
  20. data/lib/httpimagestore/ruby_string_template.rb +10 -19
  21. metadata +40 -102
  22. data/.rspec +0 -1
  23. data/features/cache-control.feature +0 -41
  24. data/features/compatibility.feature +0 -165
  25. data/features/data-uri.feature +0 -55
  26. data/features/encoding.feature +0 -103
  27. data/features/error-reporting.feature +0 -281
  28. data/features/flexi.feature +0 -259
  29. data/features/health-check.feature +0 -29
  30. data/features/request-matching.feature +0 -211
  31. data/features/rewrite.feature +0 -122
  32. data/features/s3-store-and-thumbnail.feature +0 -82
  33. data/features/source-failover.feature +0 -71
  34. data/features/step_definitions/httpimagestore_steps.rb +0 -203
  35. data/features/storage.feature +0 -198
  36. data/features/support/env.rb +0 -116
  37. data/features/support/test-large.jpg +0 -0
  38. data/features/support/test.empty +0 -0
  39. data/features/support/test.jpg +0 -0
  40. data/features/support/test.png +0 -0
  41. data/features/support/test.txt +0 -1
  42. data/features/support/tiny.png +0 -0
  43. data/features/xid-forwarding.feature +0 -49
  44. data/httpimagestore.gemspec +0 -145
  45. data/load_test/load_test.1k.23a022f6e.m1.small-comp.csv +0 -3
  46. data/load_test/load_test.1k.ec9bde794.m1.small.csv +0 -4
  47. data/load_test/load_test.jmx +0 -317
  48. data/load_test/thumbnail_specs.csv +0 -11
  49. data/load_test/thumbnail_specs_v2.csv +0 -10
  50. data/spec/configuration_file_spec.rb +0 -333
  51. data/spec/configuration_handler_spec.rb +0 -255
  52. data/spec/configuration_identify_spec.rb +0 -67
  53. data/spec/configuration_output_spec.rb +0 -821
  54. data/spec/configuration_path_spec.rb +0 -138
  55. data/spec/configuration_s3_spec.rb +0 -911
  56. data/spec/configuration_source_failover_spec.rb +0 -101
  57. data/spec/configuration_spec.rb +0 -90
  58. data/spec/configuration_thumbnailer_spec.rb +0 -483
  59. data/spec/ruby_string_template_spec.rb +0 -61
  60. data/spec/spec_helper.rb +0 -89
  61. data/spec/support/compute.jpg +0 -0
  62. data/spec/support/cuba_response_env.rb +0 -40
  63. data/spec/support/full.cfg +0 -183
  64. data/spec/support/utf_string.txt +0 -1
@@ -0,0 +1,37 @@
1
+ require 'httpimagestore/configuration/handler/statement'
2
+
3
+ module Configuration
4
+ class SourceStoreBase < HandlerStatement
5
+ include ImageName
6
+ include ConditionalInclusion
7
+ include LocalConfiguration
8
+ include GlobalConfiguration
9
+ include PathSpec
10
+
11
+ def initialize(global, image_name, path_spec)
12
+ with_global_configuration(global)
13
+ with_image_name(image_name)
14
+ with_path_spec(path_spec)
15
+ end
16
+
17
+ private
18
+
19
+ def put_sourced_named_image(request_state)
20
+ rendered_path = path_template.render(request_state.with_locals(local_configuration))
21
+
22
+ image = yield @image_name, rendered_path
23
+
24
+ image.source_path = rendered_path
25
+ request_state.images[@image_name] = image
26
+ end
27
+
28
+ def get_named_image_for_storage(request_state)
29
+ image = request_state.images[@image_name]
30
+ rendered_path = path_template.render(request_state.with_locals(local_configuration))
31
+ image.store_path = rendered_path
32
+
33
+ yield @image_name, image, rendered_path
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,114 @@
1
+ require 'httpimagestore/configuration'
2
+ require 'httpimagestore/configuration/handler'
3
+
4
+ module Configuration
5
+ class HandlerStatement < Scope
6
+ # Base class for all statements that are within get/post/put/delete handler
7
+ #
8
+ module LocalConfiguration
9
+ attr_reader :local_configuration
10
+ def config_local(name, value)
11
+ @local_configuration ||= {}
12
+ @local_configuration[name] = value
13
+ end
14
+ end
15
+
16
+ module ImageName
17
+ attr_reader :image_name
18
+
19
+ def with_image_name(image_name)
20
+ @image_name = image_name
21
+ config_local :imagename, @image_name # deprecated
22
+ config_local :image_name, @image_name
23
+ self
24
+ end
25
+ end
26
+
27
+ module GlobalConfiguration
28
+ attr_reader :global
29
+ def with_global_configuration(global)
30
+ @global = global
31
+ end
32
+
33
+ def path_template(path_spec)
34
+ @global.paths[path_spec]
35
+ end
36
+ end
37
+
38
+ module PathSpec
39
+ attr_reader :path_spec
40
+
41
+ def with_path_spec(path_spec)
42
+ @path_spec = path_spec
43
+ self
44
+ end
45
+
46
+ # this is more specific than GlobalConfiguration
47
+ def path_template
48
+ @global.paths[@path_spec]
49
+ end
50
+ end
51
+
52
+ module ConditionalInclusion
53
+ class ImageNameOn
54
+ def initialize(template)
55
+ @template = template.to_template
56
+ end
57
+
58
+ def included?(request_state)
59
+ image_name = request_state[:image_name]
60
+ @template.render(request_state).split(',').include? image_name
61
+ end
62
+ end
63
+
64
+ class VariableMatches
65
+ def initialize(value)
66
+ param_name, template = value.split(':', 2)
67
+ @param_name = param_name.to_sym if param_name
68
+ @template = template.to_template if template
69
+ end
70
+
71
+ def included?(request_state)
72
+ return false if not @param_name
73
+ return !request_state[@param_name].empty? if not @template
74
+ @template.render(request_state) == request_state[@param_name]
75
+ rescue Configuration::VariableNotDefinedError
76
+ false
77
+ end
78
+ end
79
+
80
+ def self.grab_conditions_with_remaining(attributes)
81
+ conditions = []
82
+ attributes = attributes.dup
83
+
84
+ if_image_name_on = attributes.delete('if-image-name-on')
85
+ conditions << ConditionalInclusion::ImageNameOn.new(if_image_name_on) if if_image_name_on
86
+
87
+ if_image_name_on = attributes.delete('if-variable-matches')
88
+ conditions << ConditionalInclusion::VariableMatches.new(if_image_name_on) if if_image_name_on
89
+
90
+ [conditions, attributes]
91
+ end
92
+
93
+ def with_conditions(conditions)
94
+ @conditions ||= []
95
+ @conditions.push(*conditions)
96
+ self
97
+ end
98
+
99
+ def included?(request_state)
100
+ return true if not @conditions or @conditions.empty?
101
+ # some conditions may use local_configuration vars
102
+ request_state = request_state.with_locals(local_configuration) if @local_configuration
103
+ @conditions.all? do |matcher|
104
+ matcher.included?(request_state)
105
+ end
106
+ end
107
+
108
+ def excluded?(request_state)
109
+ not included? request_state
110
+ end
111
+ end
112
+ end
113
+ end
114
+
@@ -5,6 +5,11 @@ require 'httpimagestore/configuration/handler'
5
5
  module Configuration
6
6
  class Identify < HandlerStatement
7
7
  include ClassLogging
8
+ include ImageName
9
+ include LocalConfiguration
10
+ include GlobalConfiguration
11
+ include ConditionalInclusion
12
+ include PerfStats
8
13
 
9
14
  extend Stats
10
15
  def_stats(
@@ -18,18 +23,19 @@ module Configuration
18
23
 
19
24
  def self.parse(configuration, node)
20
25
  image_name = node.grab_values('image name').first
21
- if_image_name_on = node.grab_attributes('if-image-name-on').first
22
26
 
23
- matcher = InclusionMatcher.new(image_name, if_image_name_on) if if_image_name_on
27
+ conditions, remaining = *ConditionalInclusion.grab_conditions_with_remaining(node.attributes)
28
+ remaining.empty? or raise UnexpectedAttributesError.new(node, remaining)
24
29
 
25
- configuration.processors << self.new(configuration.global, image_name, matcher)
26
- end
30
+ iden = self.new(configuration.global, image_name)
31
+ iden.with_conditions(conditions)
27
32
 
28
- include ImageName
29
- include ConditionalInclusion
33
+ configuration.processors << iden
34
+ end
30
35
 
31
- def initialize(global, image_name, matcher = nil)
32
- super(global, image_name, matcher)
36
+ def initialize(global, image_name)
37
+ with_global_configuration(global)
38
+ with_image_name(image_name)
33
39
  end
34
40
 
35
41
  def realize(request_state)
@@ -41,7 +47,9 @@ module Configuration
41
47
  Identify.stats.incr_total_identify_requests
42
48
  Identify.stats.incr_total_identify_requests_bytes image.data.bytesize
43
49
 
44
- id = client.with_headers(request_state.headers).identify(image.data)
50
+ id = measure "identifying", @image_name do
51
+ client.with_headers(request_state.forward_headers).identify(image.data)
52
+ end
45
53
 
46
54
  image.mime_type = id.mime_type if id.mime_type
47
55
  image.width = id.width if id.width
@@ -1,4 +1,4 @@
1
- require 'httpimagestore/configuration/handler'
1
+ require 'httpimagestore/configuration/handler/statement'
2
2
  require 'httpimagestore/ruby_string_template'
3
3
  require 'addressable/uri'
4
4
  require 'base64'
@@ -16,61 +16,18 @@ module Configuration
16
16
  end
17
17
  end
18
18
 
19
- class OutputText
20
- def self.match(node)
21
- node.name == 'output_text'
22
- end
23
-
24
- def self.parse(configuration, node)
25
- configuration.output and raise StatementCollisionError.new(node, 'output')
26
- text = node.grab_values('text').first
27
- status, cache_control = *node.grab_attributes('status', 'cache-control')
28
- configuration.output = OutputText.new(text, status || 200, cache_control)
29
- end
30
-
31
- def initialize(text, status, cache_control)
32
- @text = RubyStringTemplate.new(text || fail("no text?!"))
33
- @status = status || 200
34
- @cache_control = cache_control
35
- end
36
-
37
- def realize(request_state)
38
- # make sure variables are available in request context
39
- status = @status
40
- text = @text.render(request_state)
41
- cache_control = @cache_control
42
- request_state.output do
43
- res['Cache-Control'] = cache_control if cache_control
44
- write_plain status.to_i, text.to_s
45
- end
46
- end
47
- end
48
-
49
- class OutputOK < OutputText
50
- def self.match(node)
51
- node.name == 'output_ok'
52
- end
53
-
54
- def self.parse(configuration, node)
55
- configuration.output and raise StatementCollisionError.new(node, 'output')
56
- cache_control = node.grab_attributes('cache-control').first
57
- configuration.output = OutputOK.new(cache_control)
58
- end
59
-
60
- def initialize(cache_control = nil)
61
- super 'OK', 200, cache_control
62
- end
63
- end
64
- Handler::register_node_parser OutputText
65
-
66
19
  class OutputMultiBase
67
20
  class OutputSpec < HandlerStatement
21
+ include GlobalConfiguration
68
22
  include ImageName
69
23
  include PathSpec
70
24
  include ConditionalInclusion
25
+ include LocalConfiguration
71
26
 
72
- def initialize(global, image_name, scheme, host, port, path_spec, matcher)
73
- super(global, image_name, path_spec, matcher)
27
+ def initialize(global, image_name, scheme, host, port, path_spec)
28
+ with_global_configuration(global)
29
+ with_image_name(image_name)
30
+ with_path_spec(path_spec)
74
31
  @scheme = scheme && scheme.to_template
75
32
  @host = host && host.to_template
76
33
  @port = port && port.to_template
@@ -80,7 +37,7 @@ module Configuration
80
37
  store_path = request_state.images[@image_name].store_path or raise StorePathNotSetForImage.new(@image_name)
81
38
  return store_path unless @path_spec
82
39
 
83
- path_template.render(request_state.with_locals(config_locals, path: store_path))
40
+ path_template.render(request_state.with_locals(local_configuration, path: store_path))
84
41
  end
85
42
 
86
43
  def store_url(request_state)
@@ -96,7 +53,7 @@ module Configuration
96
53
  request_locals[:host] = url.host if url.host
97
54
  request_locals[:port] = url.port if url.port
98
55
 
99
- request_state = request_state.with_locals(config_locals, request_locals)
56
+ request_state = request_state.with_locals(local_configuration, request_locals)
100
57
 
101
58
  # optional rewrites
102
59
  url.scheme = @scheme.render(request_state) if @scheme
@@ -112,9 +69,13 @@ module Configuration
112
69
  nodes = node.values.empty? ? node.children : [node]
113
70
  output_specs = nodes.map do |node|
114
71
  image_name = node.grab_values('image name').first
115
- scheme, host, port, path_spec, if_image_name_on = *node.grab_attributes('scheme', 'host', 'port', 'path', 'if-image-name-on')
116
- matcher = InclusionMatcher.new(image_name, if_image_name_on)
117
- OutputSpec.new(configuration.global, image_name, scheme, host, port, path_spec, matcher)
72
+ scheme, host, port, path_spec, remaining = *node.grab_attributes_with_remaining('scheme', 'host', 'port', 'path')
73
+ conditions, remaining = *HandlerStatement::ConditionalInclusion.grab_conditions_with_remaining(remaining)
74
+ remaining.empty? or raise UnexpectedAttributesError.new(node, remaining)
75
+
76
+ out = OutputSpec.new(configuration.global, image_name, scheme, host, port, path_spec)
77
+ out.with_conditions(conditions)
78
+ out
118
79
  end
119
80
 
120
81
  configuration.output and raise StatementCollisionError.new(node, 'output')
@@ -125,10 +86,12 @@ module Configuration
125
86
  @output_specs = output_specs
126
87
  end
127
88
  end
128
- Handler::register_node_parser OutputOK
129
89
 
130
- class OutputImage
90
+ class OutputImage < HandlerStatement
131
91
  include ClassLogging
92
+ include ImageName
93
+ include LocalConfiguration
94
+ include PerfStats
132
95
 
133
96
  def self.match(node)
134
97
  node.name == 'output_image'
@@ -144,6 +107,7 @@ module Configuration
144
107
  def initialize(name, cache_control)
145
108
  @name = name
146
109
  @cache_control = cache_control
110
+ with_image_name(name)
147
111
  end
148
112
 
149
113
  def realize(request_state)
@@ -157,15 +121,20 @@ module Configuration
157
121
  end
158
122
 
159
123
  cache_control = @cache_control
124
+ _context = self
160
125
  request_state.output do
161
- res['Cache-Control'] = cache_control if cache_control
162
- write 200, mime_type, image.data
126
+ _context.measure "sending response image data", "mime type: #{mime_type} (image #{image.data.bytesize} bytes)" do
127
+ res['Cache-Control'] = cache_control if cache_control
128
+ write 200, mime_type, image.data
129
+ end
163
130
  end
164
131
  end
165
132
  end
166
133
  Handler::register_node_parser OutputImage
167
134
 
168
135
  class OutputDataURIImage < OutputImage
136
+ include PerfStats
137
+
169
138
  def self.match(node)
170
139
  node.name == 'output_data_uri_image'
171
140
  end
@@ -175,9 +144,12 @@ module Configuration
175
144
  fail "image '#{@name}' needs to be identified first to be used in data URI output" unless image.mime_type
176
145
 
177
146
  cache_control = @cache_control
147
+ _context = self
178
148
  request_state.output do
179
- res['Cache-Control'] = cache_control if cache_control
180
- write 200, 'text/uri-list', "data:#{image.mime_type};base64,#{Base64.strict_encode64(image.data)}"
149
+ _context.measure "sending response image data with data URI encoding", "mime type: #{image.mime_type} (image #{image.data.bytesize} bytes)" do
150
+ res['Cache-Control'] = cache_control if cache_control
151
+ write 200, 'text/uri-list', "data:#{image.mime_type};base64,#{Base64.strict_encode64(image.data)}"
152
+ end
181
153
  end
182
154
  end
183
155
  end
@@ -15,7 +15,7 @@ module Configuration
15
15
  end
16
16
  end
17
17
 
18
- class NoValueForPathTemplatePlaceholerError < PathRenderingError
18
+ class NoValueForPathTemplatePlaceholderError < PathRenderingError
19
19
  def initialize(path_name, template, placeholder)
20
20
  super path_name, template, "no value for '\#{#{placeholder}}'"
21
21
  end
@@ -56,7 +56,7 @@ module Configuration
56
56
  locals[name]
57
57
  rescue ConfigurationError => error
58
58
  raise PathRenderingError.new(path_name, template, error.message)
59
- end or raise NoValueForPathTemplatePlaceholerError.new(path_name, template, name)
59
+ end or raise NoValueForPathTemplatePlaceholderError.new(path_name, template, name)
60
60
  end
61
61
  end
62
62
 
@@ -0,0 +1,131 @@
1
+ require 'mime/types'
2
+ require 'digest/sha2'
3
+ require 'securerandom'
4
+
5
+ module Configuration
6
+ class RequestState < Hash
7
+ include ClassLogging
8
+
9
+ class Images < Hash
10
+ def initialize(memory_limit)
11
+ @memory_limit = memory_limit
12
+ super
13
+ end
14
+
15
+ def []=(name, image)
16
+ if member?(name)
17
+ @memory_limit.return fetch(name).data.bytesize
18
+ end
19
+ super
20
+ end
21
+
22
+ def [](name)
23
+ fetch(name){|image_name| raise ImageNotLoadedError.new(image_name)}
24
+ end
25
+ end
26
+
27
+ def initialize(body, matches, path, query_string, request_uri, request_headers, memory_limit, forward_headers)
28
+ super() do |request_state, name|
29
+ # note that request_state may be different object when useing with_locals that creates duplicate
30
+ request_state[name] = request_state.generate_meta_variable(name) or raise VariableNotDefinedError.new(name)
31
+ end
32
+
33
+ # it is OK to overwrite path with a match
34
+ self[:path] = path
35
+
36
+ merge! matches
37
+
38
+ log.debug "processing request with body length: #{body.bytesize} bytes and variables: #{map{|k,v| "#{k}: '#{v}'"}.join(', ')}"
39
+
40
+ @body = body
41
+ @images = Images.new(memory_limit)
42
+ @query_string = query_string
43
+ @request_uri = request_uri
44
+ @request_headers = request_headers
45
+ @memory_limit = memory_limit
46
+ @output_callback = nil
47
+
48
+ @forward_headers = forward_headers
49
+ end
50
+
51
+ attr_reader :body
52
+ attr_reader :images
53
+ attr_reader :memory_limit
54
+ attr_reader :query_string
55
+ attr_reader :request_uri
56
+ attr_reader :request_headers
57
+ attr_reader :forward_headers
58
+
59
+ def with_locals(*locals)
60
+ locals = locals.reduce{|a, b| a.merge(b)}
61
+ log.debug "using additional local variables: #{locals}"
62
+ self.dup.merge!(locals)
63
+ end
64
+
65
+ def output(&callback)
66
+ @output_callback = callback
67
+ end
68
+
69
+ def output_callback
70
+ @output_callback or fail 'no output callback'
71
+ end
72
+
73
+ def fetch_base_variable(name, base_name)
74
+ fetch(base_name, nil) or generate_meta_variable(base_name) or raise NoVariableToGenerateMetaVariableError.new(base_name, name)
75
+ end
76
+
77
+ def generate_meta_variable(name)
78
+ val = case name
79
+ when :basename
80
+ path = Pathname.new(fetch_base_variable(name, :path))
81
+ path.basename(path.extname).to_s
82
+ when :dirname
83
+ Pathname.new(fetch_base_variable(name, :path)).dirname.to_s
84
+ when :extension
85
+ Pathname.new(fetch_base_variable(name, :path)).extname.delete('.')
86
+ when :digest # deprecated
87
+ @body.empty? and raise NoRequestBodyToGenerateMetaVariableError.new(name)
88
+ Digest::SHA2.new.update(@body).to_s[0,16]
89
+ when :input_digest
90
+ @body.empty? and raise NoRequestBodyToGenerateMetaVariableError.new(name)
91
+ Digest::SHA2.new.update(@body).to_s[0,16]
92
+ when :input_sha256
93
+ @body.empty? and raise NoRequestBodyToGenerateMetaVariableError.new(name)
94
+ Digest::SHA2.new.update(@body).to_s
95
+ when :input_image_width
96
+ @images['input'].width or raise NoImageDataForVariableError.new('input', name)
97
+ when :input_image_height
98
+ @images['input'].height or raise NoImageDataForVariableError.new('input', name)
99
+ when :input_image_mime_extension
100
+ @images['input'].mime_extension or raise NoImageDataForVariableError.new('input', name)
101
+ when :image_digest
102
+ Digest::SHA2.new.update(@images[fetch_base_variable(name, :image_name)].data).to_s[0,16]
103
+ when :image_sha256
104
+ Digest::SHA2.new.update(@images[fetch_base_variable(name, :image_name)].data).to_s
105
+ when :mimeextension # deprecated
106
+ image_name = fetch_base_variable(name, :image_name)
107
+ @images[image_name].mime_extension or raise NoImageDataForVariableError.new(image_name, name)
108
+ when :image_mime_extension
109
+ image_name = fetch_base_variable(name, :image_name)
110
+ @images[image_name].mime_extension or raise NoImageDataForVariableError.new(image_name, name)
111
+ when :image_width
112
+ image_name = fetch_base_variable(name, :image_name)
113
+ @images[image_name].width or raise NoImageDataForVariableError.new(image_name, name)
114
+ when :image_height
115
+ image_name = fetch_base_variable(name, :image_name)
116
+ @images[image_name].height or raise NoImageDataForVariableError.new(image_name, name)
117
+ when :uuid
118
+ SecureRandom.uuid
119
+ when :query_string_options
120
+ query_string.sort.map{|kv| kv.join(':')}.join(',')
121
+ end
122
+ if val
123
+ log.debug "generated meta variable '#{name}': #{val}"
124
+ else
125
+ log.debug "could not generated meta variable '#{name}'"
126
+ end
127
+ val
128
+ end
129
+ end
130
+ end
131
+