httpimagestore 0.5.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/Gemfile +10 -12
  2. data/Gemfile.lock +57 -55
  3. data/README.md +829 -0
  4. data/VERSION +1 -1
  5. data/bin/httpimagestore +114 -180
  6. data/features/cache-control.feature +26 -90
  7. data/features/compatibility.feature +129 -0
  8. data/features/error-reporting.feature +207 -0
  9. data/features/health-check.feature +30 -0
  10. data/features/s3-store-and-thumbnail.feature +65 -0
  11. data/features/step_definitions/httpimagestore_steps.rb +66 -26
  12. data/features/support/env.rb +32 -5
  13. data/features/support/test.empty +0 -0
  14. data/httpimagestore.gemspec +60 -47
  15. data/lib/httpimagestore/aws_sdk_regions_hack.rb +23 -0
  16. data/lib/httpimagestore/configuration/file.rb +120 -0
  17. data/lib/httpimagestore/configuration/handler.rb +239 -0
  18. data/lib/httpimagestore/configuration/output.rb +119 -0
  19. data/lib/httpimagestore/configuration/path.rb +77 -0
  20. data/lib/httpimagestore/configuration/s3.rb +194 -0
  21. data/lib/httpimagestore/configuration/thumbnailer.rb +244 -0
  22. data/lib/httpimagestore/configuration.rb +126 -29
  23. data/lib/httpimagestore/error_reporter.rb +36 -0
  24. data/lib/httpimagestore/ruby_string_template.rb +26 -0
  25. data/load_test/load_test.1k.23a022f6e.m1.small-comp.csv +3 -0
  26. data/load_test/load_test.1k.ec9bde794.m1.small.csv +4 -0
  27. data/load_test/load_test.jmx +344 -0
  28. data/load_test/thumbnail_specs.csv +11 -0
  29. data/spec/configuration_file_spec.rb +309 -0
  30. data/spec/configuration_handler_spec.rb +124 -0
  31. data/spec/configuration_output_spec.rb +338 -0
  32. data/spec/configuration_path_spec.rb +92 -0
  33. data/spec/configuration_s3_spec.rb +571 -0
  34. data/spec/configuration_spec.rb +80 -105
  35. data/spec/configuration_thumbnailer_spec.rb +417 -0
  36. data/spec/ruby_string_template_spec.rb +43 -0
  37. data/spec/spec_helper.rb +61 -0
  38. data/spec/support/compute.jpg +0 -0
  39. data/spec/support/cuba_response_env.rb +40 -0
  40. data/spec/support/full.cfg +49 -0
  41. metadata +138 -84
  42. data/README.rdoc +0 -23
  43. data/features/httpimagestore.feature +0 -167
  44. data/lib/httpimagestore/image_path.rb +0 -54
  45. data/lib/httpimagestore/s3_service.rb +0 -37
  46. data/lib/httpimagestore/thumbnail_class.rb +0 -13
  47. data/spec/image_path_spec.rb +0 -72
  48. data/spec/test.cfg +0 -8
@@ -5,17 +5,17 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "httpimagestore"
8
- s.version = "0.5.0"
8
+ s.version = "1.0.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 = "2012-11-14"
12
+ s.date = "2013-07-16"
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"]
16
16
  s.extra_rdoc_files = [
17
17
  "LICENSE.txt",
18
- "README.rdoc"
18
+ "README.md"
19
19
  ]
20
20
  s.files = [
21
21
  ".document",
@@ -23,88 +23,101 @@ Gem::Specification.new do |s|
23
23
  "Gemfile",
24
24
  "Gemfile.lock",
25
25
  "LICENSE.txt",
26
- "README.rdoc",
26
+ "README.md",
27
27
  "Rakefile",
28
28
  "VERSION",
29
29
  "bin/httpimagestore",
30
30
  "features/cache-control.feature",
31
- "features/httpimagestore.feature",
31
+ "features/compatibility.feature",
32
+ "features/error-reporting.feature",
33
+ "features/health-check.feature",
34
+ "features/s3-store-and-thumbnail.feature",
32
35
  "features/step_definitions/httpimagestore_steps.rb",
33
36
  "features/support/env.rb",
34
37
  "features/support/test-large.jpg",
38
+ "features/support/test.empty",
35
39
  "features/support/test.jpg",
36
40
  "features/support/test.txt",
37
41
  "httpimagestore.gemspec",
42
+ "lib/httpimagestore/aws_sdk_regions_hack.rb",
38
43
  "lib/httpimagestore/configuration.rb",
39
- "lib/httpimagestore/image_path.rb",
40
- "lib/httpimagestore/s3_service.rb",
41
- "lib/httpimagestore/thumbnail_class.rb",
44
+ "lib/httpimagestore/configuration/file.rb",
45
+ "lib/httpimagestore/configuration/handler.rb",
46
+ "lib/httpimagestore/configuration/output.rb",
47
+ "lib/httpimagestore/configuration/path.rb",
48
+ "lib/httpimagestore/configuration/s3.rb",
49
+ "lib/httpimagestore/configuration/thumbnailer.rb",
50
+ "lib/httpimagestore/error_reporter.rb",
51
+ "lib/httpimagestore/ruby_string_template.rb",
52
+ "load_test/load_test.1k.23a022f6e.m1.small-comp.csv",
53
+ "load_test/load_test.1k.ec9bde794.m1.small.csv",
54
+ "load_test/load_test.jmx",
55
+ "load_test/thumbnail_specs.csv",
56
+ "spec/configuration_file_spec.rb",
57
+ "spec/configuration_handler_spec.rb",
58
+ "spec/configuration_output_spec.rb",
59
+ "spec/configuration_path_spec.rb",
60
+ "spec/configuration_s3_spec.rb",
42
61
  "spec/configuration_spec.rb",
43
- "spec/image_path_spec.rb",
62
+ "spec/configuration_thumbnailer_spec.rb",
63
+ "spec/ruby_string_template_spec.rb",
44
64
  "spec/spec_helper.rb",
45
- "spec/test.cfg"
65
+ "spec/support/compute.jpg",
66
+ "spec/support/cuba_response_env.rb",
67
+ "spec/support/full.cfg"
46
68
  ]
47
69
  s.homepage = "http://github.com/jpastuszek/httpimagestore"
48
70
  s.licenses = ["MIT"]
49
71
  s.require_paths = ["lib"]
50
- s.rubygems_version = "1.8.15"
72
+ s.rubygems_version = "1.8.25"
51
73
  s.summary = "HTTP based image storage and thumbnailer"
52
74
 
53
75
  if s.respond_to? :specification_version then
54
76
  s.specification_version = 3
55
77
 
56
78
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
57
- s.add_runtime_dependency(%q<sinatra>, [">= 1.2.6"])
58
- s.add_runtime_dependency(%q<mongrel>, [">= 1.2.0.pre2"])
59
- s.add_runtime_dependency(%q<s3>, ["~> 0.3"])
60
- s.add_runtime_dependency(%q<httpthumbnailer-client>, ["~> 0.1.0"])
61
- s.add_runtime_dependency(%q<ruby-ip>, ["~> 0.9"])
62
- s.add_runtime_dependency(%q<cli>, ["~> 1.1.0"])
63
- s.add_runtime_dependency(%q<mime-types>, ["~> 1.17.2"])
64
- s.add_runtime_dependency(%q<retry-this>, ["~> 1.1"])
65
- s.add_development_dependency(%q<rspec>, ["~> 2.8.0"])
79
+ s.add_runtime_dependency(%q<unicorn-cuba-base>, ["~> 1.0"])
80
+ s.add_runtime_dependency(%q<httpthumbnailer-client>, ["~> 1.0"])
81
+ s.add_runtime_dependency(%q<aws-sdk>, ["~> 1.10"])
82
+ s.add_runtime_dependency(%q<mime-types>, ["~> 1.17"])
83
+ s.add_runtime_dependency(%q<sdl4r>, ["~> 0.9"])
84
+ s.add_development_dependency(%q<httpclient>, [">= 2.3"])
85
+ s.add_development_dependency(%q<rspec>, ["~> 2.13"])
66
86
  s.add_development_dependency(%q<cucumber>, [">= 0"])
67
- s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
68
- s.add_development_dependency(%q<simplecov>, [">= 0"])
87
+ s.add_development_dependency(%q<jeweler>, ["~> 1.8.4"])
69
88
  s.add_development_dependency(%q<rdoc>, ["~> 3.9"])
70
89
  s.add_development_dependency(%q<daemon>, ["~> 1"])
71
- s.add_development_dependency(%q<httpthumbnailer>, ["~> 0.2"])
72
90
  s.add_development_dependency(%q<prawn>, ["= 0.8.4"])
91
+ s.add_development_dependency(%q<httpthumbnailer>, [">= 0"])
73
92
  else
74
- s.add_dependency(%q<sinatra>, [">= 1.2.6"])
75
- s.add_dependency(%q<mongrel>, [">= 1.2.0.pre2"])
76
- s.add_dependency(%q<s3>, ["~> 0.3"])
77
- s.add_dependency(%q<httpthumbnailer-client>, ["~> 0.1.0"])
78
- s.add_dependency(%q<ruby-ip>, ["~> 0.9"])
79
- s.add_dependency(%q<cli>, ["~> 1.1.0"])
80
- s.add_dependency(%q<mime-types>, ["~> 1.17.2"])
81
- s.add_dependency(%q<retry-this>, ["~> 1.1"])
82
- s.add_dependency(%q<rspec>, ["~> 2.8.0"])
93
+ s.add_dependency(%q<unicorn-cuba-base>, ["~> 1.0"])
94
+ s.add_dependency(%q<httpthumbnailer-client>, ["~> 1.0"])
95
+ s.add_dependency(%q<aws-sdk>, ["~> 1.10"])
96
+ s.add_dependency(%q<mime-types>, ["~> 1.17"])
97
+ s.add_dependency(%q<sdl4r>, ["~> 0.9"])
98
+ s.add_dependency(%q<httpclient>, [">= 2.3"])
99
+ s.add_dependency(%q<rspec>, ["~> 2.13"])
83
100
  s.add_dependency(%q<cucumber>, [">= 0"])
84
- s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
85
- s.add_dependency(%q<simplecov>, [">= 0"])
101
+ s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
86
102
  s.add_dependency(%q<rdoc>, ["~> 3.9"])
87
103
  s.add_dependency(%q<daemon>, ["~> 1"])
88
- s.add_dependency(%q<httpthumbnailer>, ["~> 0.2"])
89
104
  s.add_dependency(%q<prawn>, ["= 0.8.4"])
105
+ s.add_dependency(%q<httpthumbnailer>, [">= 0"])
90
106
  end
91
107
  else
92
- s.add_dependency(%q<sinatra>, [">= 1.2.6"])
93
- s.add_dependency(%q<mongrel>, [">= 1.2.0.pre2"])
94
- s.add_dependency(%q<s3>, ["~> 0.3"])
95
- s.add_dependency(%q<httpthumbnailer-client>, ["~> 0.1.0"])
96
- s.add_dependency(%q<ruby-ip>, ["~> 0.9"])
97
- s.add_dependency(%q<cli>, ["~> 1.1.0"])
98
- s.add_dependency(%q<mime-types>, ["~> 1.17.2"])
99
- s.add_dependency(%q<retry-this>, ["~> 1.1"])
100
- s.add_dependency(%q<rspec>, ["~> 2.8.0"])
108
+ s.add_dependency(%q<unicorn-cuba-base>, ["~> 1.0"])
109
+ s.add_dependency(%q<httpthumbnailer-client>, ["~> 1.0"])
110
+ s.add_dependency(%q<aws-sdk>, ["~> 1.10"])
111
+ s.add_dependency(%q<mime-types>, ["~> 1.17"])
112
+ s.add_dependency(%q<sdl4r>, ["~> 0.9"])
113
+ s.add_dependency(%q<httpclient>, [">= 2.3"])
114
+ s.add_dependency(%q<rspec>, ["~> 2.13"])
101
115
  s.add_dependency(%q<cucumber>, [">= 0"])
102
- s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
103
- s.add_dependency(%q<simplecov>, [">= 0"])
116
+ s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
104
117
  s.add_dependency(%q<rdoc>, ["~> 3.9"])
105
118
  s.add_dependency(%q<daemon>, ["~> 1"])
106
- s.add_dependency(%q<httpthumbnailer>, ["~> 0.2"])
107
119
  s.add_dependency(%q<prawn>, ["= 0.8.4"])
120
+ s.add_dependency(%q<httpthumbnailer>, [">= 0"])
108
121
  end
109
122
  end
110
123
 
@@ -0,0 +1,23 @@
1
+ ## HACK: Auto select region based on location_constraint
2
+ module AWS
3
+ class S3
4
+ class BucketCollection
5
+ def [](name)
6
+ # if name is DNS compatible we still cannot use it for writes if it does contain dots
7
+ return S3::Bucket.new(name.to_s, :owner => nil, :config => config) if client.dns_compatible_bucket_name?(name) and not name.include? '.'
8
+
9
+ # save region mapping for bucket for futher requests
10
+ @@location_cache = {} unless defined? @@location_cache
11
+ # if we have it cased use it; else try to fetch it and if it is nil bucket is in standard region
12
+ region = @@location_cache[name] || @@location_cache[name] = S3::Bucket.new(name.to_s, :owner => nil, :config => config).location_constraint || @@location_cache[name] = :standard
13
+
14
+ # no need to specify region if bucket is in standard region
15
+ return S3::Bucket.new(name.to_s, :owner => nil, :config => config) if region == :standard
16
+
17
+ # use same config but with region specified for buckets that are not DNS compatible or have dots and are not in standard region
18
+ S3::Bucket.new(name.to_s, :owner => nil, :config => config.with(region: region))
19
+ end
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,120 @@
1
+ require 'httpimagestore/configuration/path'
2
+ require 'httpimagestore/configuration/handler'
3
+ require 'pathname'
4
+
5
+ module Configuration
6
+ class FileStorageOutsideOfRootDirError < ConfigurationError
7
+ def initialize(image_name, file_path)
8
+ super "error while processing image '#{image_name}': file storage path '#{file_path.to_s}' outside of root direcotry"
9
+ end
10
+ end
11
+
12
+ class NoSuchFileError < ConfigurationError
13
+ def initialize(image_name, file_path)
14
+ super "error while processing image '#{image_name}': file '#{file_path.to_s}' not found"
15
+ end
16
+ end
17
+
18
+ class FileSourceStoreBase < SourceStoreBase
19
+ extend Stats
20
+ def_stats(
21
+ :total_file_store,
22
+ :total_file_store_bytes,
23
+ :total_file_source,
24
+ :total_file_source_bytes
25
+ )
26
+
27
+ def self.parse(configuration, node)
28
+ image_name = node.grab_values('image name').first
29
+ node.required_attributes('root', 'path')
30
+ root_dir, path_spec, if_image_name_on = *node.grab_attributes('root', 'path', 'if-image-name-on')
31
+ matcher = InclusionMatcher.new(image_name, if_image_name_on)
32
+
33
+ self.new(
34
+ configuration.global,
35
+ image_name,
36
+ matcher,
37
+ root_dir,
38
+ path_spec
39
+ )
40
+ end
41
+
42
+ def initialize(global, image_name, matcher, root_dir, path_spec)
43
+ super global, image_name, matcher
44
+ @root_dir = Pathname.new(root_dir).cleanpath
45
+ @path_spec = path_spec
46
+ end
47
+
48
+ def storage_path(rendered_path)
49
+ path = Pathname.new(rendered_path)
50
+
51
+ storage_path = (@root_dir + path).cleanpath
52
+ storage_path.to_s =~ /^#{@root_dir.to_s}/ or raise FileStorageOutsideOfRootDirError.new(@image_name, path)
53
+
54
+ storage_path
55
+ end
56
+ end
57
+
58
+ class FileSource < FileSourceStoreBase
59
+ include ClassLogging
60
+
61
+ def self.match(node)
62
+ node.name == 'source_file'
63
+ end
64
+
65
+ def self.parse(configuration, node)
66
+ configuration.image_sources << super
67
+ end
68
+
69
+ def realize(request_state)
70
+ put_sourced_named_image(request_state) do |image_name, rendered_path|
71
+ storage_path = storage_path(rendered_path)
72
+
73
+ log.info "sourcing '#{image_name}' from file '#{storage_path}'"
74
+ begin
75
+ data = storage_path.open('r') do |io|
76
+ request_state.memory_limit.io io
77
+ io.read
78
+ end
79
+ FileSourceStoreBase.stats.incr_total_file_source
80
+ FileSourceStoreBase.stats.incr_total_file_source_bytes(data.bytesize)
81
+
82
+ image = Image.new(data)
83
+ image.source_url = "file://#{rendered_path}"
84
+ image
85
+ rescue Errno::ENOENT
86
+ raise NoSuchFileError.new(image_name, rendered_path)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ Handler::register_node_parser FileSource
92
+
93
+ class FileStore < FileSourceStoreBase
94
+ include ClassLogging
95
+
96
+ def self.match(node)
97
+ node.name == 'store_file'
98
+ end
99
+
100
+ def self.parse(configuration, node)
101
+ configuration.stores << super
102
+ end
103
+
104
+ def realize(request_state)
105
+ get_named_image_for_storage(request_state) do |image_name, image, rendered_path|
106
+ storage_path = storage_path(rendered_path)
107
+
108
+ image.store_url = "file://#{rendered_path.to_s}"
109
+
110
+ log.info "storing '#{image_name}' in file '#{storage_path}' (#{image.data.length} bytes)"
111
+ storage_path.open('w'){|io| io.write image.data}
112
+ FileSourceStoreBase.stats.incr_total_file_store
113
+ FileSourceStoreBase.stats.incr_total_file_store_bytes(image.data.bytesize)
114
+ end
115
+ end
116
+ end
117
+ Handler::register_node_parser FileStore
118
+ StatsReporter << FileSourceStoreBase.stats
119
+ end
120
+
@@ -0,0 +1,239 @@
1
+ require 'mime/types'
2
+
3
+ module Configuration
4
+ class ImageNotLoadedError < ConfigurationError
5
+ def initialize(image_name)
6
+ super "image '#{image_name}' not loaded"
7
+ end
8
+ end
9
+
10
+ class ZeroBodyLengthError < ConfigurationError
11
+ def initialize
12
+ super 'empty body - expected image data'
13
+ end
14
+ end
15
+
16
+ class RequestState
17
+ class Images < Hash
18
+ def initialize(memory_limit)
19
+ @memory_limit = memory_limit
20
+ super
21
+ end
22
+
23
+ def []=(name, image)
24
+ if member?(name)
25
+ @memory_limit.return fetch(name).data.bytesize
26
+ end
27
+ super
28
+ end
29
+
30
+ def [](name)
31
+ fetch(name){|image_name| raise ImageNotLoadedError.new(image_name)}
32
+ end
33
+ end
34
+
35
+ def initialize(body = '', locals = {}, memory_limit = MemoryLimit.new)
36
+ @images = Images.new(memory_limit)
37
+ @locals = {body: body}.merge(locals)
38
+ @memory_limit = memory_limit
39
+ @output_callback = nil
40
+ end
41
+
42
+ attr_reader :images
43
+ attr_reader :locals
44
+ attr_reader :memory_limit
45
+
46
+ def output(&callback)
47
+ @output_callback = callback
48
+ end
49
+
50
+ def output_callback
51
+ @output_callback or fail 'no output callback'
52
+ end
53
+ end
54
+
55
+ module ImageMetaData
56
+ attr_accessor :source_path
57
+ attr_accessor :source_url
58
+ attr_accessor :store_path
59
+ attr_accessor :store_url
60
+
61
+ def mime_extension
62
+ return nil unless mime_type
63
+ mime = MIME::Types[mime_type].first
64
+ mime.extensions.select{|e| e.length == 3}.first or mime.extensions.first
65
+ end
66
+ end
67
+
68
+ class Image < Struct.new(:data, :mime_type)
69
+ include ImageMetaData
70
+ end
71
+
72
+ class InputSource
73
+ def realize(request_state)
74
+ request_state.locals[:body].empty? and raise ZeroBodyLengthError
75
+ request_state.images['input'] = Image.new(request_state.locals[:body])
76
+ end
77
+ end
78
+
79
+ class OutputOK
80
+ def realize(request_state)
81
+ request_state.output do
82
+ write_plain 200, 'OK'
83
+ end
84
+ end
85
+ end
86
+
87
+ class InclusionMatcher
88
+ def initialize(value, template)
89
+ @value = value
90
+ @template = RubyStringTemplate.new(template) if template
91
+ end
92
+
93
+ def included?(request_state)
94
+ return true if not @template
95
+ @template.render(request_state.locals).split(',').include? @value
96
+ end
97
+ end
98
+
99
+ module ConditionalInclusion
100
+ def inclusion_matcher(matcher)
101
+ (@matchers ||= []) << matcher if matcher
102
+ end
103
+
104
+ def included?(request_state)
105
+ return true unless @matchers
106
+ @matchers.any? do |matcher|
107
+ matcher.included?(request_state)
108
+ end
109
+ end
110
+
111
+ def excluded?(request_state)
112
+ not included? request_state
113
+ end
114
+ end
115
+
116
+ class SourceStoreBase
117
+ include ConditionalInclusion
118
+
119
+ def initialize(global, image_name, matcher)
120
+ @global = global
121
+ @image_name = image_name
122
+ @locals = {imagename: @image_name}
123
+ inclusion_matcher matcher
124
+ end
125
+
126
+ private
127
+
128
+ attr_accessor :image_name
129
+
130
+ def local(name, value)
131
+ @locals[name] = value
132
+ end
133
+
134
+ def rendered_path(request_state)
135
+ path = @global.paths[@path_spec]
136
+ Pathname.new(path.render(@locals.merge(request_state.locals))).cleanpath.to_s
137
+ end
138
+
139
+ def put_sourced_named_image(request_state)
140
+ rendered_path = rendered_path(request_state)
141
+
142
+ image = yield @image_name, rendered_path
143
+
144
+ image.source_path = rendered_path
145
+ request_state.images[@image_name] = image
146
+ end
147
+
148
+ def get_named_image_for_storage(request_state)
149
+ image = request_state.images[@image_name]
150
+ local :mimeextension, image.mime_extension
151
+
152
+ rendered_path = rendered_path(request_state)
153
+ image.store_path = rendered_path
154
+
155
+ yield @image_name, image, rendered_path
156
+ end
157
+ end
158
+
159
+ class Matcher
160
+ def initialize(name, &matcher)
161
+ @name = name
162
+ @matcher = matcher
163
+ end
164
+
165
+ attr_reader :name
166
+ attr_reader :matcher
167
+ end
168
+
169
+ class Handler < Scope
170
+ def self.match(node)
171
+ node.name == 'put' or
172
+ node.name == 'post' or
173
+ node.name == 'get'
174
+ end
175
+
176
+ def self.pre(configuration)
177
+ configuration.handlers ||= []
178
+ end
179
+
180
+ def self.parse(configuration, node)
181
+ handler_configuration =
182
+ Struct.new(
183
+ :global,
184
+ :http_method,
185
+ :uri_matchers,
186
+ :image_sources,
187
+ :stores,
188
+ :output
189
+ ).new
190
+
191
+ handler_configuration.global = configuration
192
+ handler_configuration.http_method = node.name
193
+ handler_configuration.uri_matchers = node.values.map do |matcher|
194
+ case matcher
195
+ when %r{^:[^/]+/.*/$}
196
+ name, regexp = *matcher.match(%r{^:([^/]+)/(.*)/$}).captures
197
+ Matcher.new(name.to_sym) do
198
+ Regexp.new("(#{regexp})")
199
+ end
200
+ when /^:.+\?$/
201
+ name = matcher.sub(/^:(.+)\?$/, '\1').to_sym
202
+ Matcher.new(name) do
203
+ ->{match(name) || captures.push('')}
204
+ end
205
+ when /^:/
206
+ name = matcher.sub(/^:/, '').to_sym
207
+ Matcher.new(name) do
208
+ name
209
+ end
210
+ else
211
+ Matcher.new(nil) do
212
+ matcher
213
+ end
214
+ end
215
+ end
216
+ handler_configuration.image_sources = []
217
+ handler_configuration.stores = []
218
+ handler_configuration.output = nil
219
+
220
+ node.grab_attributes
221
+
222
+ if handler_configuration.http_method != 'get'
223
+ handler_configuration.image_sources << InputSource.new
224
+ end
225
+
226
+ configuration.handlers << handler_configuration
227
+
228
+ self.new(handler_configuration).parse(node)
229
+
230
+ handler_configuration.output = OutputOK.new unless handler_configuration.output
231
+ end
232
+
233
+ def self.post(configuration)
234
+ log.warn 'no handlers configured' if configuration.handlers.empty?
235
+ end
236
+ end
237
+ Global.register_node_parser Handler
238
+ end
239
+
@@ -0,0 +1,119 @@
1
+ require 'httpimagestore/configuration/handler'
2
+
3
+ module Configuration
4
+ class StorePathNotSetForImage < ConfigurationError
5
+ def initialize(image_name)
6
+ super "store path not set for image '#{image_name}'"
7
+ end
8
+ end
9
+
10
+ class StoreURLNotSetForImage < ConfigurationError
11
+ def initialize(image_name)
12
+ super "store URL not set for image '#{image_name}'"
13
+ end
14
+ end
15
+
16
+ class OutputMultiBase
17
+ class ImageName < String
18
+ include ConditionalInclusion
19
+
20
+ def initialize(name, matcher)
21
+ super name
22
+ inclusion_matcher matcher
23
+ end
24
+ end
25
+
26
+ def self.parse(configuration, node)
27
+ nodes = node.values.empty? ? node.children : [node]
28
+ names = nodes.map do |node|
29
+ image_name = node.grab_values('image name').first
30
+ matcher = InclusionMatcher.new(image_name, node.grab_attributes('if-image-name-on').first)
31
+ ImageName.new(image_name, matcher)
32
+ end
33
+
34
+ configuration.output and raise StatementCollisionError.new(node, 'output')
35
+ configuration.output = self.new(names)
36
+ end
37
+
38
+ def initialize(names)
39
+ @names = names
40
+ end
41
+ end
42
+
43
+ class OutputImage
44
+ include ClassLogging
45
+
46
+ def self.match(node)
47
+ node.name == 'output_image'
48
+ end
49
+
50
+ def self.parse(configuration, node)
51
+ configuration.output and raise StatementCollisionError.new(node, 'output')
52
+ image_name = node.grab_values('image name').first
53
+ cache_control = node.grab_attributes('cache-control').first
54
+ configuration.output = OutputImage.new(image_name, cache_control)
55
+ end
56
+
57
+ def initialize(name, cache_control)
58
+ @name = name
59
+ @cache_control = cache_control
60
+ end
61
+
62
+ def realize(request_state)
63
+ image = request_state.images[@name]
64
+ mime_type =
65
+ if image.mime_type
66
+ image.mime_type
67
+ else
68
+ log.warn "image '#{@name}' has no mime type; sending 'application/octet-stream' content type"
69
+ 'application/octet-stream'
70
+ end
71
+
72
+ cache_control = @cache_control
73
+ request_state.output do
74
+ res['Cache-Control'] = cache_control if cache_control
75
+ write 200, mime_type, image.data
76
+ end
77
+ end
78
+ end
79
+ Handler::register_node_parser OutputImage
80
+
81
+ class OutputStorePath < OutputMultiBase
82
+ def self.match(node)
83
+ node.name == 'output_store_path'
84
+ end
85
+
86
+ def realize(request_state)
87
+ paths = @names.select do |name|
88
+ name.included?(request_state)
89
+ end.map do |name|
90
+ request_state.images[name].store_path or raise StorePathNotSetForImage.new(name)
91
+ end
92
+
93
+ request_state.output do
94
+ write_plain 200, paths
95
+ end
96
+ end
97
+ end
98
+ Handler::register_node_parser OutputStorePath
99
+
100
+ class OutputStoreURL < OutputMultiBase
101
+ def self.match(node)
102
+ node.name == 'output_store_url'
103
+ end
104
+
105
+ def realize(request_state)
106
+ urls = @names.select do |name|
107
+ name.included?(request_state)
108
+ end.map do |name|
109
+ request_state.images[name].store_url or raise StoreURLNotSetForImage.new(name)
110
+ end
111
+
112
+ request_state.output do
113
+ write_url_list 200, urls
114
+ end
115
+ end
116
+ end
117
+ Handler::register_node_parser OutputStoreURL
118
+ end
119
+