httpimagestore 0.5.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|