httpimagestore 1.6.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/Gemfile +4 -3
  2. data/Gemfile.lock +12 -16
  3. data/README.md +73 -0
  4. data/VERSION +1 -1
  5. data/bin/httpimagestore +7 -7
  6. data/features/data-uri.feature +55 -0
  7. data/features/error-reporting.feature +21 -3
  8. data/features/source-failover.feature +71 -0
  9. data/features/step_definitions/httpimagestore_steps.rb +27 -12
  10. data/features/storage.feature +26 -25
  11. data/features/support/tiny.png +0 -0
  12. data/features/xid-forwarding.feature +49 -0
  13. data/httpimagestore.gemspec +19 -23
  14. data/lib/httpimagestore/configuration/file.rb +6 -0
  15. data/lib/httpimagestore/configuration/handler.rb +4 -1
  16. data/lib/httpimagestore/configuration/identify.rb +2 -2
  17. data/lib/httpimagestore/configuration/output.rb +20 -1
  18. data/lib/httpimagestore/configuration/s3.rb +15 -9
  19. data/lib/httpimagestore/configuration/source_failover.rb +51 -0
  20. data/lib/httpimagestore/configuration/thumbnailer.rb +7 -7
  21. data/lib/httpimagestore/error_reporter.rb +9 -1
  22. data/lib/httpimagestore/ruby_string_template.rb +12 -7
  23. data/load_test/load_test.jmx +50 -77
  24. data/load_test/thumbnail_specs_v2.csv +10 -0
  25. data/spec/configuration_file_spec.rb +27 -2
  26. data/spec/configuration_identify_spec.rb +25 -2
  27. data/spec/configuration_s3_spec.rb +29 -3
  28. data/spec/configuration_source_failover_spec.rb +101 -0
  29. data/spec/configuration_thumbnailer_spec.rb +63 -8
  30. data/spec/ruby_string_template_spec.rb +4 -0
  31. data/spec/support/full.cfg +167 -33
  32. metadata +19 -23
  33. data/.idea/.name +0 -1
  34. data/.idea/.rakeTasks +0 -7
  35. data/.idea/codeStyleSettings.xml +0 -13
  36. data/.idea/dictionaries/wcc.xml +0 -8
  37. data/.idea/encodings.xml +0 -5
  38. data/.idea/httpimagestore.iml +0 -69
  39. data/.idea/jenkinsSettings.xml +0 -9
  40. data/.idea/misc.xml +0 -5
  41. data/.idea/modules.xml +0 -9
  42. data/.idea/scopes/scope_settings.xml +0 -5
  43. data/.idea/vcs.xml +0 -7
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "httpimagestore"
8
- s.version = "1.6.0"
8
+ s.version = "1.7.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Jakub Pastuszek"]
12
- s.date = "2013-11-04"
12
+ s.date = "2014-07-28"
13
13
  s.description = "Thumbnails images using httpthumbnailer and stored data on HTTP server (S3)"
14
14
  s.email = "jpastuszek@gmail.com"
15
15
  s.executables = ["httpimagestore"]
@@ -19,17 +19,6 @@ Gem::Specification.new do |s|
19
19
  ]
20
20
  s.files = [
21
21
  ".document",
22
- ".idea/.name",
23
- ".idea/.rakeTasks",
24
- ".idea/codeStyleSettings.xml",
25
- ".idea/dictionaries/wcc.xml",
26
- ".idea/encodings.xml",
27
- ".idea/httpimagestore.iml",
28
- ".idea/jenkinsSettings.xml",
29
- ".idea/misc.xml",
30
- ".idea/modules.xml",
31
- ".idea/scopes/scope_settings.xml",
32
- ".idea/vcs.xml",
33
22
  ".rspec",
34
23
  "Gemfile",
35
24
  "Gemfile.lock",
@@ -40,11 +29,13 @@ Gem::Specification.new do |s|
40
29
  "bin/httpimagestore",
41
30
  "features/cache-control.feature",
42
31
  "features/compatibility.feature",
32
+ "features/data-uri.feature",
43
33
  "features/error-reporting.feature",
44
34
  "features/flexi.feature",
45
35
  "features/health-check.feature",
46
36
  "features/request-matching.feature",
47
37
  "features/s3-store-and-thumbnail.feature",
38
+ "features/source-failover.feature",
48
39
  "features/step_definitions/httpimagestore_steps.rb",
49
40
  "features/storage.feature",
50
41
  "features/support/env.rb",
@@ -53,6 +44,8 @@ Gem::Specification.new do |s|
53
44
  "features/support/test.jpg",
54
45
  "features/support/test.png",
55
46
  "features/support/test.txt",
47
+ "features/support/tiny.png",
48
+ "features/xid-forwarding.feature",
56
49
  "httpimagestore.gemspec",
57
50
  "lib/httpimagestore/aws_sdk_regions_hack.rb",
58
51
  "lib/httpimagestore/configuration.rb",
@@ -62,6 +55,7 @@ Gem::Specification.new do |s|
62
55
  "lib/httpimagestore/configuration/output.rb",
63
56
  "lib/httpimagestore/configuration/path.rb",
64
57
  "lib/httpimagestore/configuration/s3.rb",
58
+ "lib/httpimagestore/configuration/source_failover.rb",
65
59
  "lib/httpimagestore/configuration/thumbnailer.rb",
66
60
  "lib/httpimagestore/error_reporter.rb",
67
61
  "lib/httpimagestore/ruby_string_template.rb",
@@ -69,12 +63,14 @@ Gem::Specification.new do |s|
69
63
  "load_test/load_test.1k.ec9bde794.m1.small.csv",
70
64
  "load_test/load_test.jmx",
71
65
  "load_test/thumbnail_specs.csv",
66
+ "load_test/thumbnail_specs_v2.csv",
72
67
  "spec/configuration_file_spec.rb",
73
68
  "spec/configuration_handler_spec.rb",
74
69
  "spec/configuration_identify_spec.rb",
75
70
  "spec/configuration_output_spec.rb",
76
71
  "spec/configuration_path_spec.rb",
77
72
  "spec/configuration_s3_spec.rb",
73
+ "spec/configuration_source_failover_spec.rb",
78
74
  "spec/configuration_spec.rb",
79
75
  "spec/configuration_thumbnailer_spec.rb",
80
76
  "spec/ruby_string_template_spec.rb",
@@ -86,15 +82,15 @@ Gem::Specification.new do |s|
86
82
  s.homepage = "http://github.com/jpastuszek/httpimagestore"
87
83
  s.licenses = ["MIT"]
88
84
  s.require_paths = ["lib"]
89
- s.rubygems_version = "1.8.25"
85
+ s.rubygems_version = "1.8.23"
90
86
  s.summary = "HTTP based image storage and thumbnailer"
91
87
 
92
88
  if s.respond_to? :specification_version then
93
89
  s.specification_version = 3
94
90
 
95
91
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
96
- s.add_runtime_dependency(%q<unicorn-cuba-base>, ["~> 1.1.2"])
97
- s.add_runtime_dependency(%q<httpthumbnailer-client>, ["~> 1.1.1"])
92
+ s.add_runtime_dependency(%q<unicorn-cuba-base>, ["~> 1.2.0"])
93
+ s.add_runtime_dependency(%q<httpthumbnailer-client>, ["~> 1.2.0"])
98
94
  s.add_runtime_dependency(%q<aws-sdk>, ["~> 1.10"])
99
95
  s.add_runtime_dependency(%q<mime-types>, ["~> 1.17"])
100
96
  s.add_runtime_dependency(%q<sdl4r>, ["~> 0.9"])
@@ -106,10 +102,10 @@ Gem::Specification.new do |s|
106
102
  s.add_development_dependency(%q<rdoc>, ["~> 3.9"])
107
103
  s.add_development_dependency(%q<daemon>, ["~> 1"])
108
104
  s.add_development_dependency(%q<prawn>, ["= 0.8.4"])
109
- s.add_development_dependency(%q<httpthumbnailer>, [">= 0"])
105
+ s.add_development_dependency(%q<httpthumbnailer>, ["~> 1.2.0"])
110
106
  else
111
- s.add_dependency(%q<unicorn-cuba-base>, ["~> 1.1.2"])
112
- s.add_dependency(%q<httpthumbnailer-client>, ["~> 1.1.1"])
107
+ s.add_dependency(%q<unicorn-cuba-base>, ["~> 1.2.0"])
108
+ s.add_dependency(%q<httpthumbnailer-client>, ["~> 1.2.0"])
113
109
  s.add_dependency(%q<aws-sdk>, ["~> 1.10"])
114
110
  s.add_dependency(%q<mime-types>, ["~> 1.17"])
115
111
  s.add_dependency(%q<sdl4r>, ["~> 0.9"])
@@ -121,11 +117,11 @@ Gem::Specification.new do |s|
121
117
  s.add_dependency(%q<rdoc>, ["~> 3.9"])
122
118
  s.add_dependency(%q<daemon>, ["~> 1"])
123
119
  s.add_dependency(%q<prawn>, ["= 0.8.4"])
124
- s.add_dependency(%q<httpthumbnailer>, [">= 0"])
120
+ s.add_dependency(%q<httpthumbnailer>, ["~> 1.2.0"])
125
121
  end
126
122
  else
127
- s.add_dependency(%q<unicorn-cuba-base>, ["~> 1.1.2"])
128
- s.add_dependency(%q<httpthumbnailer-client>, ["~> 1.1.1"])
123
+ s.add_dependency(%q<unicorn-cuba-base>, ["~> 1.2.0"])
124
+ s.add_dependency(%q<httpthumbnailer-client>, ["~> 1.2.0"])
129
125
  s.add_dependency(%q<aws-sdk>, ["~> 1.10"])
130
126
  s.add_dependency(%q<mime-types>, ["~> 1.17"])
131
127
  s.add_dependency(%q<sdl4r>, ["~> 0.9"])
@@ -137,7 +133,7 @@ Gem::Specification.new do |s|
137
133
  s.add_dependency(%q<rdoc>, ["~> 3.9"])
138
134
  s.add_dependency(%q<daemon>, ["~> 1"])
139
135
  s.add_dependency(%q<prawn>, ["= 0.8.4"])
140
- s.add_dependency(%q<httpthumbnailer>, [">= 0"])
136
+ s.add_dependency(%q<httpthumbnailer>, ["~> 1.2.0"])
141
137
  end
142
138
  end
143
139
 
@@ -1,5 +1,6 @@
1
1
  require 'httpimagestore/configuration/path'
2
2
  require 'httpimagestore/configuration/handler'
3
+ require 'httpimagestore/configuration/source_failover'
3
4
  require 'pathname'
4
5
  require 'uri'
5
6
 
@@ -54,6 +55,10 @@ module Configuration
54
55
 
55
56
  storage_path
56
57
  end
58
+
59
+ def to_s
60
+ "FileSource[image_name: '#{@image_name}' root_dir: '#{@root_dir}' path_spec: '#{@path_spec}']"
61
+ end
57
62
  end
58
63
 
59
64
  class FileSource < FileSourceStoreBase
@@ -90,6 +95,7 @@ module Configuration
90
95
  end
91
96
  end
92
97
  Handler::register_node_parser FileSource
98
+ SourceFailover::register_node_parser FileSource
93
99
 
94
100
  class FileStore < FileSourceStoreBase
95
101
  include ClassLogging
@@ -60,7 +60,7 @@ module Configuration
60
60
  end
61
61
  end
62
62
 
63
- def initialize(body = '', matches = {}, path = '', query_string = {}, memory_limit = MemoryLimit.new)
63
+ def initialize(body = '', matches = {}, path = '', query_string = {}, memory_limit = MemoryLimit.new, headers = {})
64
64
  super() do |request_state, name|
65
65
  # note that request_state may be different object when useing with_locals that creates duplicate
66
66
  request_state[name] = request_state.generate_meta_variable(name) or raise VariableNotDefinedError.new(name)
@@ -76,11 +76,14 @@ module Configuration
76
76
  @images = Images.new(memory_limit)
77
77
  @memory_limit = memory_limit
78
78
  @output_callback = nil
79
+
80
+ @headers = headers
79
81
  end
80
82
 
81
83
  attr_reader :body
82
84
  attr_reader :images
83
85
  attr_reader :memory_limit
86
+ attr_reader :headers
84
87
 
85
88
  def with_locals(locals)
86
89
  log.debug "using additional local variables: #{locals}"
@@ -8,7 +8,7 @@ module Configuration
8
8
 
9
9
  extend Stats
10
10
  def_stats(
11
- :total_identify_requests,
11
+ :total_identify_requests,
12
12
  :total_identify_requests_bytes
13
13
  )
14
14
 
@@ -42,7 +42,7 @@ module Configuration
42
42
  Identify.stats.incr_total_identify_requests
43
43
  Identify.stats.incr_total_identify_requests_bytes image.data.bytesize
44
44
 
45
- id = client.identify(image.data)
45
+ id = client.with_headers(request_state.headers).identify(image.data)
46
46
 
47
47
  image.mime_type = id.mime_type if id.mime_type
48
48
  image.width = id.width if id.width
@@ -1,6 +1,7 @@
1
1
  require 'httpimagestore/configuration/handler'
2
2
  require 'httpimagestore/ruby_string_template'
3
3
  require 'uri'
4
+ require 'base64'
4
5
 
5
6
  module Configuration
6
7
  class StorePathNotSetForImage < ConfigurationError
@@ -131,7 +132,7 @@ module Configuration
131
132
  configuration.output and raise StatementCollisionError.new(node, 'output')
132
133
  image_name = node.grab_values('image name').first
133
134
  cache_control = node.grab_attributes('cache-control').first
134
- configuration.output = OutputImage.new(image_name, cache_control)
135
+ configuration.output = self.new(image_name, cache_control)
135
136
  end
136
137
 
137
138
  def initialize(name, cache_control)
@@ -158,6 +159,24 @@ module Configuration
158
159
  end
159
160
  Handler::register_node_parser OutputImage
160
161
 
162
+ class OutputDataURIImage < OutputImage
163
+ def self.match(node)
164
+ node.name == 'output_data_uri_image'
165
+ end
166
+
167
+ def realize(request_state)
168
+ image = request_state.images[@name]
169
+ fail "image '#{@name}' needs to be identified first to be used in data URI output" unless image.mime_type
170
+
171
+ cache_control = @cache_control
172
+ request_state.output do
173
+ res['Cache-Control'] = cache_control if cache_control
174
+ write 200, 'text/uri-list', "data:#{image.mime_type};base64,#{Base64.strict_encode64(image.data)}"
175
+ end
176
+ end
177
+ end
178
+ Handler::register_node_parser OutputDataURIImage
179
+
161
180
  class OutputStorePath < OutputMultiBase
162
181
  def self.match(node)
163
182
  node.name == 'output_store_path'
@@ -4,6 +4,7 @@ require 'msgpack'
4
4
  require 'httpimagestore/aws_sdk_regions_hack'
5
5
  require 'httpimagestore/configuration/path'
6
6
  require 'httpimagestore/configuration/handler'
7
+ require 'httpimagestore/configuration/source_failover'
7
8
 
8
9
  module Configuration
9
10
  class S3NotConfiguredError < ConfigurationError
@@ -43,7 +44,7 @@ module Configuration
43
44
  node.grab_values
44
45
  node.required_attributes('key', 'secret')
45
46
  node.valid_attribute_values('ssl', true, false, nil)
46
-
47
+
47
48
  key, secret, ssl = node.grab_attributes('key', 'secret', 'ssl')
48
49
  ssl = true if ssl.nil?
49
50
 
@@ -191,7 +192,7 @@ module Configuration
191
192
  class CacheObject < S3Object
192
193
  extend Stats
193
194
  def_stats(
194
- :total_s3_cache_hits,
195
+ :total_s3_cache_hits,
195
196
  :total_s3_cache_misses,
196
197
  :total_s3_cache_errors,
197
198
  )
@@ -274,7 +275,7 @@ module Configuration
274
275
 
275
276
  extend Stats
276
277
  def_stats(
277
- :total_s3_store,
278
+ :total_s3_store,
278
279
  :total_s3_store_bytes,
279
280
  :total_s3_source,
280
281
  :total_s3_source_bytes
@@ -286,18 +287,18 @@ module Configuration
286
287
  node.required_attributes('bucket', 'path')
287
288
  node.valid_attribute_values('public_access', true, false, nil)
288
289
 
289
- bucket, path_spec, public_access, cache_control, prefix, cache_root, if_image_name_on =
290
+ bucket, path_spec, public_access, cache_control, prefix, cache_root, if_image_name_on =
290
291
  *node.grab_attributes('bucket', 'path', 'public', 'cache-control', 'prefix', 'cache-root', 'if-image-name-on')
291
292
  public_access = false if public_access.nil?
292
293
  prefix = '' if prefix.nil?
293
294
 
294
295
  self.new(
295
- configuration.global,
296
- image_name,
296
+ configuration.global,
297
+ image_name,
297
298
  InclusionMatcher.new(image_name, if_image_name_on),
298
- bucket,
299
- path_spec,
300
- public_access,
299
+ bucket,
300
+ path_spec,
301
+ public_access,
301
302
  cache_control,
302
303
  prefix,
303
304
  cache_root
@@ -395,8 +396,13 @@ module Configuration
395
396
  end
396
397
  end
397
398
  end
399
+
400
+ def to_s
401
+ "S3Source[image_name: '#{@image_name}' bucket: '#{@bucket}' prefix: '#{@prefix}' path_spec: '#{@path_spec}']"
402
+ end
398
403
  end
399
404
  Handler::register_node_parser S3Source
405
+ SourceFailover::register_node_parser S3Source
400
406
 
401
407
  class S3Store < S3SourceStoreBase
402
408
  def self.match(node)
@@ -0,0 +1,51 @@
1
+ require 'httpimagestore/configuration/handler'
2
+
3
+ module Configuration
4
+ class SourceFailoverAllFailedError < ConfigurationError
5
+ attr_reader :sources, :errors
6
+
7
+ def initialize(sources, errors)
8
+ @sources = sources
9
+ @errors = errors
10
+ super "all sources failed: #{sources.zip(errors).map{|s, e| "#{s}(#{e.class.name}: #{e.message})"}.join(', ')}"
11
+ end
12
+ end
13
+
14
+ class SourceFailover < Scope
15
+ include ClassLogging
16
+
17
+ def self.match(node)
18
+ node.name == 'source_failover'
19
+ end
20
+
21
+ def self.parse(configuration, node)
22
+ # support only sources
23
+ handler_configuration = Struct.new(
24
+ :global,
25
+ :sources
26
+ ).new
27
+ handler_configuration.global = configuration.global
28
+ handler_configuration.sources = []
29
+
30
+ failover = self.new(handler_configuration)
31
+ configuration.sources << failover
32
+ failover.parse(node)
33
+ end
34
+
35
+ def realize(request_state)
36
+ errors = []
37
+ @configuration.sources.each do |source|
38
+ begin
39
+ log.debug "trying source: #{source}"
40
+ return source.realize(request_state) unless source.respond_to? :excluded? and source.excluded?(request_state)
41
+ rescue => error
42
+ errors << error
43
+ log.warn "source #{source} failed; trying next source", error
44
+ end
45
+ end
46
+ log.error "all sources: #{@configuration.sources.map(&:to_s).join(', ')} failed; giving up"
47
+ raise SourceFailoverAllFailedError.new(@configuration.sources.to_a, errors)
48
+ end
49
+ end
50
+ Handler::register_node_parser SourceFailover
51
+ end
@@ -36,7 +36,7 @@ module Configuration
36
36
 
37
37
  extend Stats
38
38
  def_stats(
39
- :total_thumbnail_requests,
39
+ :total_thumbnail_requests,
40
40
  :total_thumbnail_requests_bytes,
41
41
  :total_thumbnail_thumbnails,
42
42
  :total_thumbnail_thumbnails_bytes
@@ -134,9 +134,9 @@ module Configuration
134
134
  matcher = InclusionMatcher.new(source_image_name, node.grab_attributes('if-image-name-on').first) if use_multipart_api
135
135
 
136
136
  configuration.processors << self.new(
137
- configuration.global,
138
- source_image_name,
139
- specs,
137
+ configuration.global,
138
+ source_image_name,
139
+ specs,
140
140
  use_multipart_api,
141
141
  matcher
142
142
  )
@@ -177,7 +177,7 @@ module Configuration
177
177
  logger = log
178
178
 
179
179
  begin
180
- thumbnails = client.thumbnail(source_image.data) do
180
+ thumbnails = client.with_headers(request_state.headers).thumbnail(source_image.data) do
181
181
  rendered_specs.each_pair do |name, spec|
182
182
  begin
183
183
  thumbnail(*spec)
@@ -202,7 +202,7 @@ module Configuration
202
202
  if thumbnail.kind_of? HTTPThumbnailerClient::HTTPThumbnailerClientError
203
203
  error = thumbnail
204
204
  log.warn 'got single thumbnail error', error
205
- raise ThumbnailingError.new(@source_image_name, name, error)
205
+ raise ThumbnailingError.new(@source_image_name, name, error)
206
206
  end
207
207
  end
208
208
 
@@ -215,7 +215,7 @@ module Configuration
215
215
  log.info "thumbnailing '#{@source_image_name}' to '#{name}' with spec: #{rendered_spec}"
216
216
 
217
217
  begin
218
- thumbnail = client.thumbnail(source_image.data, *rendered_spec)
218
+ thumbnail = client.with_headers(request_state.headers).thumbnail(source_image.data, *rendered_spec)
219
219
  request_state.memory_limit.borrow(thumbnail.data.bytesize, "thumbnail '#{name}'")
220
220
 
221
221
  input_mime_type = thumbnail.input_mime_type
@@ -1,4 +1,4 @@
1
- class ErrorReporter < Controler
1
+ class ErrorReporter < Controller
2
2
  self.define do
3
3
  on error(
4
4
  Configuration::S3NoSuchKeyError,
@@ -20,6 +20,14 @@ class ErrorReporter < Controler
20
20
  write_error 400, error
21
21
  end
22
22
 
23
+ on error Configuration::SourceFailoverAllFailedError do |error|
24
+ if [Configuration::S3NoSuchKeyError, Configuration::NoSuchFileError].member? error.errors.first.class
25
+ write_error 404, error
26
+ else
27
+ write_error 500, error
28
+ end
29
+ end
30
+
23
31
  run DefaultErrorReporter
24
32
  end
25
33
  end
@@ -4,22 +4,27 @@ class RubyStringTemplate
4
4
  super "no value for '\#{#{name}}' in template '#{template}'"
5
5
  end
6
6
  end
7
-
7
+
8
8
  def initialize(template, &resolver)
9
9
  @template = template.to_s
10
10
  @resolver = resolver ? resolver : ->(locals, name){locals[name]}
11
11
  end
12
12
 
13
13
  def render(locals = {})
14
- template = @template
15
- while tag = template.match(/(#\{[^\}]+\})/m)
16
- tag = tag.captures.first
17
- name = tag.match(/#\{([^\}]*)\}/).captures.first.to_sym
14
+ template = @template.dup
15
+ values = {}
16
+
17
+ template.scan(/#\{[^\}]+\}/um).uniq.each do |placeholder|
18
+ name = placeholder.match(/#\{([^\}]*)\}/u).captures.first.to_sym
18
19
  value = @resolver.call(locals, name)
19
20
  value or fail NoValueForTemplatePlaceholderError.new(name, @template)
20
- value = value.to_s
21
- template = template.gsub(tag, value)
21
+ values[placeholder] = value.to_s
22
22
  end
23
+
24
+ values.each_pair do |placeholder, value|
25
+ template.gsub!(placeholder, value)
26
+ end
27
+
23
28
  template
24
29
  end
25
30
  end