httpimagestore 1.6.0 → 1.7.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 (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