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
@@ -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