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.
- data/Gemfile +10 -12
- data/Gemfile.lock +57 -55
- data/README.md +829 -0
- data/VERSION +1 -1
- data/bin/httpimagestore +114 -180
- data/features/cache-control.feature +26 -90
- data/features/compatibility.feature +129 -0
- data/features/error-reporting.feature +207 -0
- data/features/health-check.feature +30 -0
- data/features/s3-store-and-thumbnail.feature +65 -0
- data/features/step_definitions/httpimagestore_steps.rb +66 -26
- data/features/support/env.rb +32 -5
- data/features/support/test.empty +0 -0
- data/httpimagestore.gemspec +60 -47
- data/lib/httpimagestore/aws_sdk_regions_hack.rb +23 -0
- data/lib/httpimagestore/configuration/file.rb +120 -0
- data/lib/httpimagestore/configuration/handler.rb +239 -0
- data/lib/httpimagestore/configuration/output.rb +119 -0
- data/lib/httpimagestore/configuration/path.rb +77 -0
- data/lib/httpimagestore/configuration/s3.rb +194 -0
- data/lib/httpimagestore/configuration/thumbnailer.rb +244 -0
- data/lib/httpimagestore/configuration.rb +126 -29
- data/lib/httpimagestore/error_reporter.rb +36 -0
- data/lib/httpimagestore/ruby_string_template.rb +26 -0
- data/load_test/load_test.1k.23a022f6e.m1.small-comp.csv +3 -0
- data/load_test/load_test.1k.ec9bde794.m1.small.csv +4 -0
- data/load_test/load_test.jmx +344 -0
- data/load_test/thumbnail_specs.csv +11 -0
- data/spec/configuration_file_spec.rb +309 -0
- data/spec/configuration_handler_spec.rb +124 -0
- data/spec/configuration_output_spec.rb +338 -0
- data/spec/configuration_path_spec.rb +92 -0
- data/spec/configuration_s3_spec.rb +571 -0
- data/spec/configuration_spec.rb +80 -105
- data/spec/configuration_thumbnailer_spec.rb +417 -0
- data/spec/ruby_string_template_spec.rb +43 -0
- data/spec/spec_helper.rb +61 -0
- data/spec/support/compute.jpg +0 -0
- data/spec/support/cuba_response_env.rb +40 -0
- data/spec/support/full.cfg +49 -0
- metadata +138 -84
- data/README.rdoc +0 -23
- data/features/httpimagestore.feature +0 -167
- data/lib/httpimagestore/image_path.rb +0 -54
- data/lib/httpimagestore/s3_service.rb +0 -37
- data/lib/httpimagestore/thumbnail_class.rb +0 -13
- data/spec/image_path_spec.rb +0 -72
- data/spec/test.cfg +0 -8
data/httpimagestore.gemspec
CHANGED
@@ -5,17 +5,17 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "httpimagestore"
|
8
|
-
s.version = "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 = "
|
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.
|
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.
|
26
|
+
"README.md",
|
27
27
|
"Rakefile",
|
28
28
|
"VERSION",
|
29
29
|
"bin/httpimagestore",
|
30
30
|
"features/cache-control.feature",
|
31
|
-
"features/
|
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/
|
40
|
-
"lib/httpimagestore/
|
41
|
-
"lib/httpimagestore/
|
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/
|
62
|
+
"spec/configuration_thumbnailer_spec.rb",
|
63
|
+
"spec/ruby_string_template_spec.rb",
|
44
64
|
"spec/spec_helper.rb",
|
45
|
-
"spec/
|
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.
|
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<
|
58
|
-
s.add_runtime_dependency(%q<
|
59
|
-
s.add_runtime_dependency(%q<
|
60
|
-
s.add_runtime_dependency(%q<
|
61
|
-
s.add_runtime_dependency(%q<
|
62
|
-
s.
|
63
|
-
s.
|
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.
|
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<
|
75
|
-
s.add_dependency(%q<
|
76
|
-
s.add_dependency(%q<
|
77
|
-
s.add_dependency(%q<
|
78
|
-
s.add_dependency(%q<
|
79
|
-
s.add_dependency(%q<
|
80
|
-
s.add_dependency(%q<
|
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.
|
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<
|
93
|
-
s.add_dependency(%q<
|
94
|
-
s.add_dependency(%q<
|
95
|
-
s.add_dependency(%q<
|
96
|
-
s.add_dependency(%q<
|
97
|
-
s.add_dependency(%q<
|
98
|
-
s.add_dependency(%q<
|
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.
|
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
|
+
|