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.
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
+