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
@@ -4,7 +4,7 @@ require 'msgpack'
4
4
  require 'addressable/uri'
5
5
  require 'httpimagestore/aws_sdk_regions_hack'
6
6
  require 'httpimagestore/configuration/path'
7
- require 'httpimagestore/configuration/handler'
7
+ require 'httpimagestore/configuration/handler/source_store_base'
8
8
  require 'httpimagestore/configuration/source_failover'
9
9
 
10
10
  module Configuration
@@ -123,7 +123,7 @@ module Configuration
123
123
  io.write [header.length].pack('L') # header length
124
124
  io.write header
125
125
  io.write data
126
- rescue => error
126
+ rescue
127
127
  unlink # remove broken cache file
128
128
  raise
129
129
  end
@@ -296,15 +296,17 @@ module Configuration
296
296
  node.required_attributes('bucket', 'path')
297
297
  node.valid_attribute_values('public_access', true, false, nil)
298
298
 
299
- bucket, path_spec, public_access, cache_control, prefix, cache_root, if_image_name_on =
300
- *node.grab_attributes('bucket', 'path', 'public', 'cache-control', 'prefix', 'cache-root', 'if-image-name-on')
299
+ bucket, path_spec, public_access, cache_control, prefix, cache_root, remaining =
300
+ *node.grab_attributes_with_remaining('bucket', 'path', 'public', 'cache-control', 'prefix', 'cache-root')
301
+ conditions, remaining = *ConditionalInclusion.grab_conditions_with_remaining(remaining)
302
+ remaining.empty? or raise UnexpectedAttributesError.new(node, remaining)
303
+
301
304
  public_access = false if public_access.nil?
302
305
  prefix = '' if prefix.nil?
303
306
 
304
- self.new(
307
+ s3 = self.new(
305
308
  configuration.global,
306
309
  image_name,
307
- InclusionMatcher.new(image_name, if_image_name_on),
308
310
  bucket,
309
311
  path_spec,
310
312
  public_access,
@@ -312,10 +314,13 @@ module Configuration
312
314
  prefix,
313
315
  cache_root
314
316
  )
317
+ s3.with_conditions(conditions)
318
+ s3
315
319
  end
316
320
 
317
- def initialize(global, image_name, matcher, bucket, path_spec, public_access, cache_control, prefix, cache_root)
318
- super global, image_name, matcher, path_spec
321
+ def initialize(global, image_name, bucket, path_spec, public_access, cache_control, prefix, cache_root)
322
+ super(global, image_name, path_spec)
323
+
319
324
  @bucket = bucket
320
325
  @public_access = public_access
321
326
  @cache_control = cache_control
@@ -379,6 +384,8 @@ module Configuration
379
384
  end
380
385
 
381
386
  class S3Source < S3SourceStoreBase
387
+ include PerfStats
388
+
382
389
  def self.match(node)
383
390
  node.name == 'source_s3'
384
391
  end
@@ -391,16 +398,18 @@ module Configuration
391
398
  put_sourced_named_image(request_state) do |image_name, rendered_path|
392
399
  log.info "sourcing '#{image_name}' image from S3 '#{@bucket}' bucket under '#{rendered_path}' key"
393
400
 
394
- object(rendered_path) do |object|
395
- data = request_state.memory_limit.get do |limit|
396
- object.read(limit + 1)
397
- end
398
- S3SourceStoreBase.stats.incr_total_s3_source
399
- S3SourceStoreBase.stats.incr_total_s3_source_bytes(data.bytesize)
401
+ measure "sourcing image from S3", "image: '#{image_name}' bucket: '#{@bucket}'" do
402
+ object(rendered_path) do |object|
403
+ data = request_state.memory_limit.get do |limit|
404
+ object.read(limit + 1)
405
+ end
406
+ S3SourceStoreBase.stats.incr_total_s3_source
407
+ S3SourceStoreBase.stats.incr_total_s3_source_bytes(data.bytesize)
400
408
 
401
- image = Image.new(data, object.content_type)
402
- image.source_url = url(object)
403
- image
409
+ image = Image.new(data, object.content_type)
410
+ image.source_url = url(object)
411
+ image
412
+ end
404
413
  end
405
414
  end
406
415
  end
@@ -413,6 +422,8 @@ module Configuration
413
422
  SourceFailover::register_node_parser S3Source
414
423
 
415
424
  class S3Store < S3SourceStoreBase
425
+ include PerfStats
426
+
416
427
  def self.match(node)
417
428
  node.name == 'store_s3'
418
429
  end
@@ -426,21 +437,22 @@ module Configuration
426
437
  acl = @public_access ? :public_read : :private
427
438
 
428
439
  log.info "storing '#{image_name}' image in S3 '#{@bucket}' bucket under '#{rendered_path}' key with #{acl} access"
440
+ measure "storing image in S3", "image: '#{image_name}' bucket: '#{@bucket}'" do
441
+ object(rendered_path) do |object|
442
+ image.mime_type or log.warn "storing '#{image_name}' in S3 '#{@bucket}' bucket under '#{rendered_path}' key with unknown mime type"
429
443
 
430
- object(rendered_path) do |object|
431
- image.mime_type or log.warn "storing '#{image_name}' in S3 '#{@bucket}' bucket under '#{rendered_path}' key with unknown mime type"
444
+ options = {}
445
+ options[:single_request] = true
446
+ options[:content_type] = image.mime_type if image.mime_type
447
+ options[:acl] = acl
448
+ options[:cache_control] = @cache_control if @cache_control
432
449
 
433
- options = {}
434
- options[:single_request] = true
435
- options[:content_type] = image.mime_type if image.mime_type
436
- options[:acl] = acl
437
- options[:cache_control] = @cache_control if @cache_control
450
+ object.write(image.data, options)
451
+ S3SourceStoreBase.stats.incr_total_s3_store
452
+ S3SourceStoreBase.stats.incr_total_s3_store_bytes(image.data.bytesize)
438
453
 
439
- object.write(image.data, options)
440
- S3SourceStoreBase.stats.incr_total_s3_store
441
- S3SourceStoreBase.stats.incr_total_s3_store_bytes(image.data.bytesize)
442
-
443
- image.store_url = url(object)
454
+ image.store_url = url(object)
455
+ end
444
456
  end
445
457
  end
446
458
  end
@@ -1,6 +1,6 @@
1
1
  require 'httpthumbnailer-client'
2
2
  require 'httpimagestore/ruby_string_template'
3
- require 'httpimagestore/configuration/handler'
3
+ require 'httpimagestore/configuration/handler/statement'
4
4
 
5
5
  module Configuration
6
6
  class Thumnailer
@@ -25,20 +25,49 @@ module Configuration
25
25
  end
26
26
  Global.register_node_parser Thumnailer
27
27
 
28
- class NoValueForSpecTemplatePlaceholerError < ConfigurationError
28
+ class NoValueForSpecTemplatePlaceholderError < ConfigurationError
29
29
  def initialize(image_name, spec_name, value_name, template)
30
30
  super "cannot generate specification for thumbnail '#{image_name}': cannot generate value for attribute '#{spec_name}' from template '#{template}': no value for \#{#{value_name}}"
31
31
  end
32
32
  end
33
33
 
34
+ class UnknownThumbnailingDirectiveError < ConfigurationError
35
+ def initialize(source_image_name, image_name, directive)
36
+ super "unknown directive '#{directive}' for thumbnail specification '#{image_name}' for image '#{source_image_name}'"
37
+ end
38
+ end
39
+
34
40
  class NoSpecSelectedError < RuntimeError
35
41
  def initialize(specs)
36
- super "no thumbnailing specs were selected, please use at least one of: #{specs.join(', ')}"
42
+ super "no thumbnail specs were selected, please use at least one of: #{specs.join(', ')}"
43
+ end
44
+ end
45
+
46
+ class InvalidSpecError < RuntimeError
47
+ def initialize(spec_name, cause)
48
+ super "thumbnail spec '#{spec_name}' is invalid: #{cause}"
49
+ end
50
+ end
51
+
52
+ class InvalidOptionsSpecError < InvalidSpecError
53
+ def initialize(spec_name, spec, cause)
54
+ super spec_name, "options spec '#{spec}' format is invalid: #{cause}"
55
+ end
56
+ end
57
+
58
+ class InvalidEditsSpecError < InvalidSpecError
59
+ def initialize(spec_name, spec, cause)
60
+ super spec_name, "edits spec '#{spec}' format is invalid: #{cause}"
37
61
  end
38
62
  end
39
63
 
40
64
  class Thumbnail < HandlerStatement
41
65
  include ClassLogging
66
+ include ImageName
67
+ include LocalConfiguration
68
+ include GlobalConfiguration
69
+ include ConditionalInclusion
70
+ include PerfStats
42
71
 
43
72
  extend Stats
44
73
  def_stats(
@@ -63,33 +92,89 @@ module Configuration
63
92
 
64
93
  class ThumbnailSpec < HandlerStatement
65
94
  include ImageName
95
+ include LocalConfiguration
66
96
  include ConditionalInclusion
67
97
 
68
- def initialize(image_name, method, width, height, format, options = {}, matcher = nil)
69
- super(nil, image_name, matcher)
70
- @method = method.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholerError.new(image_name, 'method', key, method)}
71
- @width = width.to_s.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholerError.new(image_name, 'width', key, width)}
72
- @height = height.to_s.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholerError.new(image_name, 'height', key, height)}
73
- @format = format.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholerError.new(image_name, 'format', key, format)}
98
+ class EditSpec
99
+ include HandlerStatement::ConditionalInclusion
100
+
101
+ def initialize(name, args, options, edit_no)
102
+ @name = name
103
+ @args = args.map.with_index do |arg, arg_no|
104
+ arg.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholderError.new(image_name, "edit #{edit_no + 1} argument #{arg_no + 1}", key, arg)}
105
+ end
106
+
107
+ @options = options.merge(options) do |option, old, template|
108
+ template.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholderError.new(image_name, "edit #{edit_no + 1} option '#{option}' value", key, arg)}
109
+ end
110
+ end
111
+
112
+ def render(request_state = {})
113
+ args = @args.map.with_index do |arg, arg_no|
114
+ arg.render(request_state)
115
+ end
116
+
117
+ options = @options.merge(@options) do |option, old, template|
118
+ template.render(request_state)
119
+ end
120
+
121
+ HTTPThumbnailerClient::ThumbnailSpec::EditSpec.new(@name, args, options)
122
+ end
123
+ end
124
+
125
+ def initialize(image_name, method, width, height, format, options = {}, edits = [])
126
+ with_image_name(image_name)
127
+ @method = method.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholderError.new(image_name, 'method', key, method)}
128
+ @width = width.to_s.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholderError.new(image_name, 'width', key, width)}
129
+ @height = height.to_s.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholderError.new(image_name, 'height', key, height)}
130
+ @format = format.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholderError.new(image_name, 'format', key, format)}
74
131
 
75
132
  @options = options.merge(options) do |option, old, template|
76
- template.to_s.to_template.with_missing_resolver{|locals, field| raise NoValueForSpecTemplatePlaceholerError.new(image_name, option, field, template)}
133
+ template.to_s.to_template.with_missing_resolver{|locals, field| raise NoValueForSpecTemplatePlaceholderError.new(image_name, option, field, template)}
77
134
  end
135
+
136
+ @edits = edits
78
137
  end
79
138
 
80
- def render(locals = {})
81
- options = @options.inject({}){|h, v| h[v.first] = v.last.render(locals); h}
82
- nested_options = options['options'] ? Hash[options.delete('options').to_s.split(',').map{|pair| pair.split(':', 2)}] : {}
83
- {
84
- image_name =>
85
- [
86
- @method.render(locals),
87
- @width.render(locals),
88
- @height.render(locals),
89
- @format.render(locals),
90
- nested_options.merge(options)
91
- ]
92
- }
139
+ def render(request_state = {})
140
+ options = @options.inject({}){|h, v| h[v.first] = v.last.render(request_state); h}
141
+
142
+ # NOTE: normally options will be passed as options=String; but may be supplied each by each as in the configuration with key=value pairs
143
+ nested_options = begin
144
+ opts = options.delete('options') || ''
145
+ HTTPThumbnailerClient::ThumbnailSpec.parse_options(HTTPThumbnailerClient::ThumbnailSpec.split_args(opts))
146
+ rescue HTTPThumbnailerClient::ThumbnailSpec::InvalidFormatError => error
147
+ raise InvalidOptionsSpecError.new(image_name, opts, error)
148
+ end
149
+
150
+ edits_option = HTTPThumbnailerClient::ThumbnailSpec.split_edits(options.delete('edits') || '').map do |edit|
151
+ begin
152
+ HTTPThumbnailerClient::ThumbnailSpec::EditSpec.from_string(edit)
153
+ rescue HTTPThumbnailerClient::ThumbnailSpec::InvalidFormatError => error
154
+ raise InvalidEditsSpecError.new(image_name, edit, error)
155
+ end
156
+ end
157
+
158
+ edits = @edits.select do |edit|
159
+ edit.included?(request_state)
160
+ end.map do |edit|
161
+ edit.render(request_state)
162
+ end
163
+
164
+ spec = begin
165
+ HTTPThumbnailerClient::ThumbnailSpec.new(
166
+ @method.render(request_state),
167
+ @width.render(request_state),
168
+ @height.render(request_state),
169
+ @format.render(request_state),
170
+ nested_options.merge(options),
171
+ edits | edits_option
172
+ )
173
+ rescue HTTPThumbnailerClient::ThumbnailSpec::InvalidFormatError => error
174
+ raise InvalidSpecError.new(image_name, error)
175
+ end
176
+
177
+ Struct.new(:name, :spec).new(image_name, spec)
93
178
  end
94
179
  end
95
180
 
@@ -98,13 +183,19 @@ module Configuration
98
183
  end
99
184
 
100
185
  def self.parse(configuration, node)
186
+ # if we don't have any subnodes use this node as single subnode and parse as if they were subnodes
101
187
  use_multipart_api = node.values.length == 1 ? true : false
102
188
 
103
- nodes = use_multipart_api ? node.children : [node]
189
+ nodes = use_multipart_api ? node.children : [node]
104
190
  source_image_name = use_multipart_api ? node.grab_values('source image name').first : nil # parsed later
105
191
 
192
+ general_conditions = []
193
+ if use_multipart_api
194
+ general_conditions, remaining = *ConditionalInclusion.grab_conditions_with_remaining(node.attributes)
195
+ remaining.empty? or raise UnexpectedAttributesError.new(node, remaining)
196
+ end
197
+
106
198
  nodes.empty? and raise NoValueError.new(node, 'thumbnail image name')
107
- matcher = nil
108
199
 
109
200
  specs = nodes.map do |node|
110
201
  if use_multipart_api
@@ -113,9 +204,26 @@ module Configuration
113
204
  source_image_name, image_name = *node.grab_values('source image name', 'thumbnail image name')
114
205
  end
115
206
 
116
- operation, width, height, format, if_image_name_on, remaining = *node.grab_attributes_with_remaining('operation', 'width', 'height', 'format', 'if-image-name-on')
207
+ operation, width, height, format, remaining = *node.grab_attributes_with_remaining('operation', 'width', 'height', 'format')
208
+
209
+ conditions, remaining = *ConditionalInclusion.grab_conditions_with_remaining(remaining)
210
+
211
+ edits = []
212
+ # check for subnodes
213
+ subnodes = node.children.group_by{|node| node.name}
214
+ edit_nodes = subnodes.delete('edit')
215
+ edit_nodes and edit_nodes.each.with_index do |node, edit_no|
216
+ edit_conditions, edit_options = *ConditionalInclusion.grab_conditions_with_remaining(node.attributes)
217
+
218
+ args = node.values.dup
219
+
220
+ edit = ThumbnailSpec::EditSpec.new(args.shift, args, edit_options, edit_no)
221
+ edit.with_conditions(edit_conditions)
222
+
223
+ edits << edit
224
+ end
117
225
 
118
- matcher = InclusionMatcher.new(image_name, if_image_name_on) if if_image_name_on
226
+ subnodes.keys.empty? or raise UnknownThumbnailingDirectiveError.new(source_image_name, image_name, subnodes.keys.join(' and '))
119
227
 
120
228
  ThumbnailSpec.new(
121
229
  image_name,
@@ -124,41 +232,45 @@ module Configuration
124
232
  height || 'input',
125
233
  format || 'input',
126
234
  remaining || {},
127
- matcher
128
- )
235
+ edits
236
+ ).with_conditions(conditions)
129
237
  end
130
238
 
131
- matcher = InclusionMatcher.new(source_image_name, node.grab_attributes('if-image-name-on').first) if use_multipart_api
132
-
133
- configuration.processors << self.new(
239
+ thum = self.new(
134
240
  configuration.global,
135
241
  source_image_name,
136
242
  specs,
137
243
  use_multipart_api,
138
- matcher
139
244
  )
245
+ thum.with_conditions(general_conditions)
246
+ configuration.processors << thum
140
247
  end
141
248
 
142
- include ConditionalInclusion
143
-
144
- def initialize(global, source_image_name, specs, use_multipart_api, matcher)
145
- super(global, matcher)
249
+ def initialize(global, source_image_name, specs, use_multipart_api)
250
+ with_global_configuration(global)
251
+ with_image_name(source_image_name)
146
252
  @source_image_name = source_image_name
147
- @specs = specs
253
+ @specs = specs.freeze
148
254
  @use_multipart_api = use_multipart_api
149
255
  end
150
256
 
151
257
  def realize(request_state)
152
258
  client = @global.thumbnailer or fail 'thumbnailer configuration'
153
259
 
154
- rendered_specs = {}
155
- @specs.select do |spec|
260
+ specs = @specs.select do |spec|
156
261
  spec.included?(request_state)
157
- end.each do |spec|
158
- rendered_specs.merge! spec.render(request_state)
159
262
  end
160
263
 
161
- rendered_specs.empty? and raise NoSpecSelectedError.new(@specs.map(&:image_name))
264
+ if specs.empty?
265
+ # in single part form we are OK to skip the thumbnailing all thogether
266
+ return if not @use_multipart_api
267
+ # in multipart for at least one thumbnail spec should be selected
268
+ raise NoSpecSelectedError.new(@specs.map(&:image_name))
269
+ end
270
+
271
+ rendered_specs = specs.map do |spec|
272
+ spec.render(request_state)
273
+ end
162
274
 
163
275
  source_image = request_state.images[@source_image_name]
164
276
 
@@ -167,69 +279,38 @@ module Configuration
167
279
  input_width = nil
168
280
  input_height = nil
169
281
 
282
+ log.info "thumbnailing '#{@source_image_name}' to specs: #{rendered_specs.map{|s| "#{s.name} -> #{s.spec}"}.join('; ')}"
170
283
  Thumbnail.stats.incr_total_thumbnail_requests
171
284
  Thumbnail.stats.incr_total_thumbnail_requests_bytes source_image.data.bytesize
172
285
 
173
- if @use_multipart_api
174
- log.info "thumbnailing '#{@source_image_name}' to multiple specs: #{rendered_specs}"
175
-
176
- # need to reference to local so they are available within thumbnail() block context
177
- source_image_name = @source_image_name
178
- logger = log
179
-
180
- begin
181
- thumbnails = client.with_headers(request_state.headers).thumbnail(source_image.data) do
182
- rendered_specs.each_pair do |name, spec|
183
- begin
184
- thumbnail(*spec)
185
- rescue HTTPThumbnailerClient::HTTPThumbnailerClientError => error
186
- logger.warn 'got thumbnailer error while passing specs', error
187
- raise ThumbnailingError.new(source_image_name, name, error)
188
- end
189
- end
190
- end
191
- rescue HTTPThumbnailerClient::HTTPThumbnailerClientError => error
192
- logger.warn 'got thumbnailer error while sending input data', error
193
- raise ThumbnailingError.new(source_image_name, nil, error)
194
- end
195
-
196
- input_mime_type = thumbnails.input_mime_type
197
- input_width = thumbnails.input_width
198
- input_height = thumbnails.input_height
199
-
200
- # check each thumbnail for errors
201
- thumbnails = Hash[rendered_specs.keys.zip(thumbnails)]
202
- thumbnails.each do |name, thumbnail|
203
- if thumbnail.kind_of? HTTPThumbnailerClient::HTTPThumbnailerClientError
204
- error = thumbnail
205
- log.warn 'got single thumbnail error', error
206
- raise ThumbnailingError.new(@source_image_name, name, error)
207
- end
286
+ thumbnails = begin
287
+ measure "thumbnailing image", "'#{@source_image_name}' to specs: #{rendered_specs.map{|s| "#{s.name} -> #{s.spec}"}.join('; ')}" do
288
+ client.with_headers(request_state.forward_headers).thumbnail(source_image.data, *rendered_specs.map(&:spec))
208
289
  end
290
+ rescue HTTPThumbnailerClient::HTTPThumbnailerClientError => error
291
+ log.warn 'got thumbnailer error', error
292
+ raise ThumbnailingError.new(@source_image_name, rendered_specs.length == 1 ? rendered_specs.first.name : nil, error)
293
+ end
209
294
 
210
- # borrow from memory limit - note that we might have already used too much memory
211
- thumbnails.each do |name, thumbnail|
212
- request_state.memory_limit.borrow(thumbnail.data.bytesize, "thumbnail '#{name}'")
213
- end
214
- else
215
- name, rendered_spec = *rendered_specs.first
216
- log.info "thumbnailing '#{@source_image_name}' to '#{name}' with spec: #{rendered_spec}"
217
-
218
- begin
219
- thumbnail = client.with_headers(request_state.headers).thumbnail(source_image.data, *rendered_spec)
220
- request_state.memory_limit.borrow(thumbnail.data.bytesize, "thumbnail '#{name}'")
221
-
222
- input_mime_type = thumbnail.input_mime_type
223
- input_width = thumbnail.input_width
224
- input_height = thumbnail.input_height
295
+ input_mime_type = thumbnails.input_mime_type
296
+ input_width = thumbnails.input_width
297
+ input_height = thumbnails.input_height
225
298
 
226
- thumbnails[name] = thumbnail
227
- rescue HTTPThumbnailerClient::HTTPThumbnailerClientError => error
228
- log.warn 'got thumbnailer error', error
299
+ # check each thumbnail for errors
300
+ thumbnails = Hash[rendered_specs.map(&:name).zip(thumbnails)]
301
+ thumbnails.each do |name, thumbnail|
302
+ if thumbnail.kind_of? HTTPThumbnailerClient::HTTPThumbnailerClientError
303
+ error = thumbnail
304
+ log.warn 'got single thumbnail error', error
229
305
  raise ThumbnailingError.new(@source_image_name, name, error)
230
306
  end
231
307
  end
232
308
 
309
+ # borrow from memory limit - note that we might have already used too much memory
310
+ thumbnails.each do |name, thumbnail|
311
+ request_state.memory_limit.borrow(thumbnail.data.bytesize, "thumbnail '#{name}'")
312
+ end
313
+
233
314
  # copy input source path and url
234
315
  thumbnails.each do |name, thumbnail|
235
316
  thumbnail.extend ImageMetaData
@@ -247,7 +328,19 @@ module Configuration
247
328
 
248
329
  request_state.images.merge! thumbnails
249
330
  end
331
+
332
+ def included?(request_state)
333
+ # TODO this is to complicated!
334
+ if @use_multipart_api
335
+ super(request_state)
336
+ else
337
+ @specs.any? do |spec|
338
+ spec.included?(request_state)
339
+ end
340
+ end
341
+ end
250
342
  end
343
+
251
344
  Handler::register_node_parser Thumbnail
252
345
  StatsReporter << Thumbnail.stats
253
346
  end