httpimagestore 0.5.0 → 1.0.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 (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
+