httpimagestore 1.2.0 → 1.3.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 +5 -2
- data/Gemfile.lock +24 -14
- data/README.md +161 -105
- data/bin/httpimagestore +6 -2
- data/features/cache-control.feature +2 -2
- data/features/compatibility.feature +4 -4
- data/features/error-reporting.feature +49 -3
- data/features/flexi.feature +260 -0
- data/features/s3-store-and-thumbnail.feature +1 -1
- data/features/step_definitions/httpimagestore_steps.rb +24 -0
- data/features/storage.feature +199 -0
- data/features/support/test-large.jpg +0 -0
- data/features/support/test.png +0 -0
- data/httpimagestore.gemspec +13 -9
- data/lib/httpimagestore/configuration/file.rb +1 -1
- data/lib/httpimagestore/configuration/handler.rb +114 -20
- data/lib/httpimagestore/configuration/identify.rb +56 -0
- data/lib/httpimagestore/configuration/path.rb +7 -28
- data/lib/httpimagestore/configuration/s3.rb +230 -12
- data/lib/httpimagestore/configuration/thumbnailer.rb +17 -8
- data/lib/httpimagestore/error_reporter.rb +1 -11
- data/load_test/load_test.jmx +11 -11
- data/load_test/thumbnail_specs.csv +11 -11
- data/spec/configuration_file_spec.rb +20 -20
- data/spec/configuration_handler_spec.rb +156 -6
- data/spec/configuration_identify_spec.rb +45 -0
- data/spec/configuration_output_spec.rb +15 -15
- data/spec/configuration_path_spec.rb +70 -23
- data/spec/configuration_s3_spec.rb +187 -28
- data/spec/configuration_thumbnailer_spec.rb +22 -22
- data/spec/spec_helper.rb +11 -2
- data/spec/support/full.cfg +4 -4
- metadata +12 -8
- data/features/facebook.feature +0 -149
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'mime/types'
|
2
|
+
require 'digest/sha2'
|
3
|
+
require 'securerandom'
|
2
4
|
|
3
5
|
module Configuration
|
4
6
|
class ImageNotLoadedError < ConfigurationError
|
@@ -13,7 +15,31 @@ module Configuration
|
|
13
15
|
end
|
14
16
|
end
|
15
17
|
|
16
|
-
class
|
18
|
+
class VariableNotDefinedError < ConfigurationError
|
19
|
+
def initialize(name)
|
20
|
+
super "variable '#{name}' not defined"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class NoRequestBodyToGenerateMetaVariableError < ConfigurationError
|
25
|
+
def initialize(meta_value)
|
26
|
+
super "need not empty request body to generate value for '#{meta_value}'"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class NoVariableToGenerateMetaVariableError < ConfigurationError
|
31
|
+
def initialize(value_name, meta_value)
|
32
|
+
super "need '#{value_name}' variable to generate value for '#{meta_value}'"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class NoImageDataForVariableError < ConfigurationError
|
37
|
+
def initialize(image_name, meta_value)
|
38
|
+
super "image '#{image_name}' does not have data for variable '#{meta_value}'"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class RequestState < Hash
|
17
43
|
include ClassLogging
|
18
44
|
|
19
45
|
class Images < Hash
|
@@ -35,24 +61,33 @@ module Configuration
|
|
35
61
|
end
|
36
62
|
|
37
63
|
def initialize(body = '', matches = {}, path = '', query_string = {}, memory_limit = MemoryLimit.new)
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
64
|
+
super() do |request_state, name|
|
65
|
+
# note that request_state may be different object when useing with_locals that creates duplicate
|
66
|
+
request_state[name] = request_state.generate_meta_variable(name) or raise VariableNotDefinedError.new(name)
|
67
|
+
end
|
68
|
+
|
69
|
+
merge! query_string
|
70
|
+
self[:path] = path
|
71
|
+
merge! matches
|
72
|
+
self[:query_string_options] = query_string.sort.map{|kv| kv.join(':')}.join(',')
|
44
73
|
|
45
|
-
|
74
|
+
log.debug "processing request with body length: #{body.bytesize} bytes and variables: #{self} "
|
46
75
|
|
76
|
+
@body = body
|
47
77
|
@images = Images.new(memory_limit)
|
48
78
|
@memory_limit = memory_limit
|
49
79
|
@output_callback = nil
|
50
80
|
end
|
51
81
|
|
82
|
+
attr_reader :body
|
52
83
|
attr_reader :images
|
53
|
-
attr_reader :locals
|
54
84
|
attr_reader :memory_limit
|
55
85
|
|
86
|
+
def with_locals(locals)
|
87
|
+
log.debug "using additional local variables: #{locals}"
|
88
|
+
self.dup.merge!(locals)
|
89
|
+
end
|
90
|
+
|
56
91
|
def output(&callback)
|
57
92
|
@output_callback = callback
|
58
93
|
end
|
@@ -60,6 +95,62 @@ module Configuration
|
|
60
95
|
def output_callback
|
61
96
|
@output_callback or fail 'no output callback'
|
62
97
|
end
|
98
|
+
|
99
|
+
def fetch_base_variable(name, base_name)
|
100
|
+
fetch(base_name, nil) or generate_meta_variable(base_name) or raise NoVariableToGenerateMetaVariableError.new(base_name, name)
|
101
|
+
end
|
102
|
+
|
103
|
+
def generate_meta_variable(name)
|
104
|
+
log.debug "generating meta variable: #{name}"
|
105
|
+
val = case name
|
106
|
+
when :basename
|
107
|
+
path = Pathname.new(fetch_base_variable(name, :path))
|
108
|
+
path.basename(path.extname).to_s
|
109
|
+
when :dirname
|
110
|
+
Pathname.new(fetch_base_variable(name, :path)).dirname.to_s
|
111
|
+
when :extension
|
112
|
+
Pathname.new(fetch_base_variable(name, :path)).extname.delete('.')
|
113
|
+
when :digest # deprecated
|
114
|
+
@body.empty? and raise NoRequestBodyToGenerateMetaVariableError.new(name)
|
115
|
+
Digest::SHA2.new.update(@body).to_s[0,16]
|
116
|
+
when :input_digest
|
117
|
+
@body.empty? and raise NoRequestBodyToGenerateMetaVariableError.new(name)
|
118
|
+
Digest::SHA2.new.update(@body).to_s[0,16]
|
119
|
+
when :input_sha256
|
120
|
+
@body.empty? and raise NoRequestBodyToGenerateMetaVariableError.new(name)
|
121
|
+
Digest::SHA2.new.update(@body).to_s
|
122
|
+
when :input_image_width
|
123
|
+
@images['input'].width or raise NoImageDataForVariableError.new('input', name)
|
124
|
+
when :input_image_height
|
125
|
+
@images['input'].height or raise NoImageDataForVariableError.new('input', name)
|
126
|
+
when :input_image_mime_extension
|
127
|
+
@images['input'].mime_extension or raise NoImageDataForVariableError.new('input', name)
|
128
|
+
when :image_digest
|
129
|
+
Digest::SHA2.new.update(@images[fetch_base_variable(name, :image_name)].data).to_s[0,16]
|
130
|
+
when :image_sha256
|
131
|
+
Digest::SHA2.new.update(@images[fetch_base_variable(name, :image_name)].data).to_s
|
132
|
+
when :mimeextension # deprecated
|
133
|
+
image_name = fetch_base_variable(name, :image_name)
|
134
|
+
@images[image_name].mime_extension or raise NoImageDataForVariableError.new(image_name, name)
|
135
|
+
when :image_mime_extension
|
136
|
+
image_name = fetch_base_variable(name, :image_name)
|
137
|
+
@images[image_name].mime_extension or raise NoImageDataForVariableError.new(image_name, name)
|
138
|
+
when :image_width
|
139
|
+
image_name = fetch_base_variable(name, :image_name)
|
140
|
+
@images[image_name].width or raise NoImageDataForVariableError.new(image_name, name)
|
141
|
+
when :image_height
|
142
|
+
image_name = fetch_base_variable(name, :image_name)
|
143
|
+
@images[image_name].height or raise NoImageDataForVariableError.new(image_name, name)
|
144
|
+
when :uuid
|
145
|
+
SecureRandom.uuid
|
146
|
+
end
|
147
|
+
if val
|
148
|
+
log.debug "generated meta variable '#{name}': #{val}"
|
149
|
+
else
|
150
|
+
log.debug "could not generated meta variable '#{name}'"
|
151
|
+
end
|
152
|
+
val
|
153
|
+
end
|
63
154
|
end
|
64
155
|
|
65
156
|
module ImageMetaData
|
@@ -75,14 +166,14 @@ module Configuration
|
|
75
166
|
end
|
76
167
|
end
|
77
168
|
|
78
|
-
class Image < Struct.new(:data, :mime_type)
|
169
|
+
class Image < Struct.new(:data, :mime_type, :width, :height)
|
79
170
|
include ImageMetaData
|
80
171
|
end
|
81
172
|
|
82
173
|
class InputSource
|
83
174
|
def realize(request_state)
|
84
|
-
request_state.
|
85
|
-
request_state.images['input'] = Image.new(request_state.
|
175
|
+
request_state.body.empty? and raise ZeroBodyLengthError
|
176
|
+
request_state.images['input'] = Image.new(request_state.body)
|
86
177
|
end
|
87
178
|
end
|
88
179
|
|
@@ -102,7 +193,7 @@ module Configuration
|
|
102
193
|
|
103
194
|
def included?(request_state)
|
104
195
|
return true if not @template
|
105
|
-
@template.render(request_state
|
196
|
+
@template.render(request_state).split(',').include? @value
|
106
197
|
end
|
107
198
|
end
|
108
199
|
|
@@ -129,8 +220,11 @@ module Configuration
|
|
129
220
|
def initialize(global, image_name, matcher)
|
130
221
|
@global = global
|
131
222
|
@image_name = image_name
|
132
|
-
@locals = {
|
223
|
+
@locals = {}
|
224
|
+
|
133
225
|
inclusion_matcher matcher
|
226
|
+
local :imagename, @image_name # deprecated
|
227
|
+
local :image_name, @image_name
|
134
228
|
end
|
135
229
|
|
136
230
|
private
|
@@ -143,7 +237,7 @@ module Configuration
|
|
143
237
|
|
144
238
|
def rendered_path(request_state)
|
145
239
|
path = @global.paths[@path_spec]
|
146
|
-
Pathname.new(path.render(
|
240
|
+
Pathname.new(path.render(request_state.with_locals(@locals))).cleanpath.to_s
|
147
241
|
end
|
148
242
|
|
149
243
|
def put_sourced_named_image(request_state)
|
@@ -157,8 +251,6 @@ module Configuration
|
|
157
251
|
|
158
252
|
def get_named_image_for_storage(request_state)
|
159
253
|
image = request_state.images[@image_name]
|
160
|
-
local :mimeextension, image.mime_extension
|
161
|
-
|
162
254
|
rendered_path = rendered_path(request_state)
|
163
255
|
image.store_path = rendered_path
|
164
256
|
|
@@ -193,7 +285,8 @@ module Configuration
|
|
193
285
|
:global,
|
194
286
|
:http_method,
|
195
287
|
:uri_matchers,
|
196
|
-
:
|
288
|
+
:sources,
|
289
|
+
:processors,
|
197
290
|
:stores,
|
198
291
|
:output
|
199
292
|
).new
|
@@ -245,14 +338,15 @@ module Configuration
|
|
245
338
|
end
|
246
339
|
end
|
247
340
|
end
|
248
|
-
handler_configuration.
|
341
|
+
handler_configuration.sources = []
|
342
|
+
handler_configuration.processors = []
|
249
343
|
handler_configuration.stores = []
|
250
344
|
handler_configuration.output = nil
|
251
345
|
|
252
346
|
node.grab_attributes
|
253
347
|
|
254
348
|
if handler_configuration.http_method != 'get'
|
255
|
-
handler_configuration.
|
349
|
+
handler_configuration.sources << InputSource.new
|
256
350
|
end
|
257
351
|
|
258
352
|
configuration.handlers << handler_configuration
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'httpthumbnailer-client'
|
2
|
+
require 'httpimagestore/ruby_string_template'
|
3
|
+
require 'httpimagestore/configuration/handler'
|
4
|
+
|
5
|
+
module Configuration
|
6
|
+
class Identify
|
7
|
+
include ClassLogging
|
8
|
+
|
9
|
+
extend Stats
|
10
|
+
def_stats(
|
11
|
+
:total_identify_requests,
|
12
|
+
:total_identify_requests_bytes
|
13
|
+
)
|
14
|
+
|
15
|
+
include ConditionalInclusion
|
16
|
+
|
17
|
+
def self.match(node)
|
18
|
+
node.name == 'identify'
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.parse(configuration, node)
|
22
|
+
image_name = node.grab_values('image name').first
|
23
|
+
if_image_name_on = node.grab_attributes('if-image-name-on').first
|
24
|
+
|
25
|
+
matcher = InclusionMatcher.new(image_name, if_image_name_on) if if_image_name_on
|
26
|
+
|
27
|
+
configuration.processors << self.new(configuration.global, image_name, matcher)
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(global, image_name, matcher = nil)
|
31
|
+
@global = global
|
32
|
+
@image_name = image_name
|
33
|
+
inclusion_matcher matcher if matcher
|
34
|
+
end
|
35
|
+
|
36
|
+
def realize(request_state)
|
37
|
+
client = @global.thumbnailer or fail 'thumbnailer configuration'
|
38
|
+
image = request_state.images[@image_name]
|
39
|
+
|
40
|
+
log.info "identifying '#{@image_name}'"
|
41
|
+
|
42
|
+
Identify.stats.incr_total_identify_requests
|
43
|
+
Identify.stats.incr_total_identify_requests_bytes image.data.bytesize
|
44
|
+
|
45
|
+
id = client.identify(image.data)
|
46
|
+
|
47
|
+
image.mime_type = id.mime_type if id.mime_type
|
48
|
+
image.width = id.width if id.width
|
49
|
+
image.height = id.height if id.height
|
50
|
+
log.info "image '#{@image_name}' identified as '#{id.mime_type}' #{image.width}x#{image.height}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
Handler::register_node_parser Identify
|
54
|
+
StatsReporter << Identify.stats
|
55
|
+
end
|
56
|
+
|
@@ -15,14 +15,8 @@ module Configuration
|
|
15
15
|
end
|
16
16
|
|
17
17
|
class NoValueForPathTemplatePlaceholerError < PathRenderingError
|
18
|
-
def initialize(path_name, template,
|
19
|
-
super path_name, template, "no value for '\#{#{
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
class NoMetaValueForPathTemplatePlaceholerError < PathRenderingError
|
24
|
-
def initialize(path_name, template, value_name, meta_value)
|
25
|
-
super path_name, template, "need '#{value_name}' to generate value for '\#{#{meta_value}}'"
|
18
|
+
def initialize(path_name, template, placeholder)
|
19
|
+
super path_name, template, "no value for '\#{#{placeholder}}'"
|
26
20
|
end
|
27
21
|
end
|
28
22
|
|
@@ -49,26 +43,11 @@ module Configuration
|
|
49
43
|
|
50
44
|
def initialize(path_name, template)
|
51
45
|
super(template) do |locals, name|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
when :dirname
|
58
|
-
path = locals[:path] or raise NoMetaValueForPathTemplatePlaceholerError.new(path_name, template, :path, name)
|
59
|
-
Pathname.new(path).dirname.to_s
|
60
|
-
when :extension
|
61
|
-
path = locals[:path] or raise NoMetaValueForPathTemplatePlaceholerError.new(path_name, template, :path, name)
|
62
|
-
Pathname.new(path).extname.delete('.')
|
63
|
-
when :digest
|
64
|
-
return locals[:_digest] if locals.include? :_digest
|
65
|
-
data = locals[:_body] or raise NoMetaValueForPathTemplatePlaceholerError.new(path_name, template, :body, name)
|
66
|
-
digest = Digest::SHA2.new.update(data).to_s[0,16]
|
67
|
-
# cache digest in request locals
|
68
|
-
locals[:_digest] = digest
|
69
|
-
else
|
70
|
-
locals[name] or raise NoValueForPathTemplatePlaceholerError.new(path_name, template, name)
|
71
|
-
end
|
46
|
+
begin
|
47
|
+
locals[name]
|
48
|
+
rescue ConfigurationError => error
|
49
|
+
raise PathRenderingError.new(path_name, template, error.message)
|
50
|
+
end or raise NoValueForPathTemplatePlaceholerError.new(path_name, template, name)
|
72
51
|
end
|
73
52
|
end
|
74
53
|
end
|
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'aws-sdk'
|
2
|
+
require 'digest/sha2'
|
3
|
+
require 'msgpack'
|
2
4
|
require 'httpimagestore/aws_sdk_regions_hack'
|
3
5
|
require 'httpimagestore/configuration/path'
|
4
6
|
require 'httpimagestore/configuration/handler'
|
@@ -61,6 +63,190 @@ module Configuration
|
|
61
63
|
class S3SourceStoreBase < SourceStoreBase
|
62
64
|
include ClassLogging
|
63
65
|
|
66
|
+
class CacheRoot
|
67
|
+
CacheRootError = Class.new ArgumentError
|
68
|
+
class CacheRootNotDirError < CacheRootError
|
69
|
+
def initialize(root_dir)
|
70
|
+
super "S3 object cache directory '#{root_dir}' does not exist or not a directory"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class CacheRootNotWritableError < CacheRootError
|
75
|
+
def initialize(root_dir)
|
76
|
+
super "S3 object cache directory '#{root_dir}' is not writable"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class CacheRootNotAccessibleError < CacheRootError
|
81
|
+
def initialize(root_dir)
|
82
|
+
super "S3 object cache directory '#{root_dir}' is not readable"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def initialize(root_dir)
|
87
|
+
@root = Pathname.new(root_dir)
|
88
|
+
@root.directory? or raise CacheRootNotDirError.new(root_dir)
|
89
|
+
@root.executable? or raise CacheRootNotAccessibleError.new(root_dir)
|
90
|
+
@root.writable? or raise CacheRootNotWritableError.new(root_dir)
|
91
|
+
end
|
92
|
+
|
93
|
+
def cache_file(bucket, key)
|
94
|
+
File.join(Digest::SHA2.new.update("#{bucket}/#{key}").to_s[0,32].match(/(..)(..)(.*)/).captures)
|
95
|
+
end
|
96
|
+
|
97
|
+
def open(bucket, key)
|
98
|
+
# TODO: locking
|
99
|
+
file = @root + cache_file(bucket, key)
|
100
|
+
|
101
|
+
file.dirname.directory? or file.dirname.mkpath
|
102
|
+
if file.exist?
|
103
|
+
file.open('r+') do |io|
|
104
|
+
yield io
|
105
|
+
end
|
106
|
+
else
|
107
|
+
file.open('w+') do |io|
|
108
|
+
yield io
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class S3Object
|
115
|
+
def initialize(client, bucket, key)
|
116
|
+
@client = client
|
117
|
+
@bucket = bucket
|
118
|
+
@key = key
|
119
|
+
end
|
120
|
+
|
121
|
+
def s3_object
|
122
|
+
return @s3_object if @s3_object
|
123
|
+
@s3_object = @client.buckets[@bucket].objects[@key]
|
124
|
+
end
|
125
|
+
|
126
|
+
def read(max_bytes = nil)
|
127
|
+
options = {}
|
128
|
+
options[:range] = 0..max_bytes if max_bytes
|
129
|
+
s3_object.read(options)
|
130
|
+
end
|
131
|
+
|
132
|
+
def write(data, options = {})
|
133
|
+
s3_object.write(data, options)
|
134
|
+
end
|
135
|
+
|
136
|
+
def private_url
|
137
|
+
s3_object.url_for(:read, expires: 60 * 60 * 24 * 365 * 20).to_s # expire in 20 years
|
138
|
+
end
|
139
|
+
|
140
|
+
def public_url
|
141
|
+
s3_object.public_url.to_s
|
142
|
+
end
|
143
|
+
|
144
|
+
def content_type
|
145
|
+
s3_object.head[:content_type]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class CacheObject < S3Object
|
150
|
+
include ClassLogging
|
151
|
+
|
152
|
+
def initialize(io, client, bucket, key)
|
153
|
+
@io = io
|
154
|
+
super(client, bucket, key)
|
155
|
+
|
156
|
+
@header = {}
|
157
|
+
@have_cache = false
|
158
|
+
@dirty = false
|
159
|
+
|
160
|
+
begin
|
161
|
+
head_length = @io.read(4)
|
162
|
+
|
163
|
+
if head_length and head_length.length == 4
|
164
|
+
head_length = head_length.unpack('L').first
|
165
|
+
@header = MessagePack.unpack(@io.read(head_length))
|
166
|
+
@have_cache = true
|
167
|
+
|
168
|
+
log.debug{"S3 object cache hit; bucket: '#{@bucket}' key: '#{@key}' [#{@io.path}]: header: #{@header}"}
|
169
|
+
else
|
170
|
+
log.debug{"S3 object cache miss; bucket: '#{@bucket}' key: '#{@key}' [#{@io.path}]"}
|
171
|
+
end
|
172
|
+
rescue => error
|
173
|
+
log.warn "cannot use cached S3 object; bucket: '#{@bucket}' key: '#{@key}' [#{@io.path}]: #{error}"
|
174
|
+
# not usable
|
175
|
+
io.seek 0
|
176
|
+
io.truncate 0
|
177
|
+
end
|
178
|
+
|
179
|
+
yield self
|
180
|
+
|
181
|
+
# save object as was used if no error happened and there were changes
|
182
|
+
write_cache if dirty?
|
183
|
+
end
|
184
|
+
|
185
|
+
def read(max_bytes = nil)
|
186
|
+
if @have_cache
|
187
|
+
data_location = @io.seek(0, IO::SEEK_CUR)
|
188
|
+
begin
|
189
|
+
return @data = @io.read(max_bytes)
|
190
|
+
ensure
|
191
|
+
@io.seek(data_location, IO::SEEK_SET)
|
192
|
+
end
|
193
|
+
else
|
194
|
+
dirty! :read
|
195
|
+
return @data = super
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def write(data, options = {})
|
200
|
+
out = super
|
201
|
+
@data = data
|
202
|
+
dirty! :write
|
203
|
+
out
|
204
|
+
end
|
205
|
+
|
206
|
+
def private_url
|
207
|
+
@header['private_url'] ||= (dirty! :private_url; super)
|
208
|
+
end
|
209
|
+
|
210
|
+
def public_url
|
211
|
+
@header['public_url'] ||= (dirty! :public_url; super)
|
212
|
+
end
|
213
|
+
|
214
|
+
def content_type
|
215
|
+
@header['content_type'] ||= (dirty! :content_type; super)
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
def write_cache
|
221
|
+
begin
|
222
|
+
log.debug{"S3 object is dirty, wirting cache file; bucket: '#{@bucket}' key: '#{@key}' [#{@io.path}]; header: #{@header}"}
|
223
|
+
|
224
|
+
raise 'nil data!' unless @data
|
225
|
+
# rewrite
|
226
|
+
@io.seek(0, IO::SEEK_SET)
|
227
|
+
@io.truncate 0
|
228
|
+
|
229
|
+
header = MessagePack.pack(@header)
|
230
|
+
@io.write [header.length].pack('L') # header length
|
231
|
+
@io.write header
|
232
|
+
@io.write @data
|
233
|
+
rescue => error
|
234
|
+
log.warn "cannot store S3 object in cache: bucket: '#{@bucket}' key: '#{@key}' [#{@io.path}]: #{error}"
|
235
|
+
ensure
|
236
|
+
@dirty = false
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def dirty!(reason = :unknown)
|
241
|
+
log.debug{"marking cache dirty for reason: #{reason}"}
|
242
|
+
@dirty = true
|
243
|
+
end
|
244
|
+
|
245
|
+
def dirty?
|
246
|
+
@dirty
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
64
250
|
extend Stats
|
65
251
|
def_stats(
|
66
252
|
:total_s3_store,
|
@@ -75,8 +261,8 @@ module Configuration
|
|
75
261
|
node.required_attributes('bucket', 'path')
|
76
262
|
node.valid_attribute_values('public_access', true, false, nil)
|
77
263
|
|
78
|
-
bucket, path_spec, public_access, cache_control, prefix, if_image_name_on =
|
79
|
-
*node.grab_attributes('bucket', 'path', 'public', 'cache-control', 'prefix', 'if-image-name-on')
|
264
|
+
bucket, path_spec, public_access, cache_control, prefix, cache_root, if_image_name_on =
|
265
|
+
*node.grab_attributes('bucket', 'path', 'public', 'cache-control', 'prefix', 'cache-root', 'if-image-name-on')
|
80
266
|
public_access = false if public_access.nil?
|
81
267
|
prefix = '' if prefix.nil?
|
82
268
|
|
@@ -88,17 +274,31 @@ module Configuration
|
|
88
274
|
path_spec,
|
89
275
|
public_access,
|
90
276
|
cache_control,
|
91
|
-
prefix
|
277
|
+
prefix,
|
278
|
+
cache_root
|
92
279
|
)
|
93
280
|
end
|
94
281
|
|
95
|
-
def initialize(global, image_name, matcher, bucket, path_spec, public_access, cache_control, prefix)
|
282
|
+
def initialize(global, image_name, matcher, bucket, path_spec, public_access, cache_control, prefix, cache_root)
|
96
283
|
super global, image_name, matcher
|
97
284
|
@bucket = bucket
|
98
285
|
@path_spec = path_spec
|
99
286
|
@public_access = public_access
|
100
287
|
@cache_control = cache_control
|
101
288
|
@prefix = prefix
|
289
|
+
|
290
|
+
@cache_root = nil
|
291
|
+
begin
|
292
|
+
if cache_root
|
293
|
+
@cache_root = CacheRoot.new(cache_root)
|
294
|
+
log.info "using S3 object cache directory '#{cache_root}' for image '#{image_name}'"
|
295
|
+
else
|
296
|
+
log.info "S3 object cache not configured (no cache-root) for image '#{image_name}'"
|
297
|
+
end
|
298
|
+
rescue CacheRoot::CacheRootNotDirError => error
|
299
|
+
log.warn "not using S3 object cache for image '#{image_name}': #{error}"
|
300
|
+
end
|
301
|
+
|
102
302
|
local :bucket, @bucket
|
103
303
|
end
|
104
304
|
|
@@ -108,16 +308,31 @@ module Configuration
|
|
108
308
|
|
109
309
|
def url(object)
|
110
310
|
if @public_access
|
111
|
-
object.public_url
|
311
|
+
object.public_url
|
112
312
|
else
|
113
|
-
object.
|
313
|
+
object.private_url
|
114
314
|
end
|
115
315
|
end
|
116
316
|
|
117
317
|
def object(path)
|
118
318
|
begin
|
119
|
-
|
120
|
-
|
319
|
+
key = @prefix + path
|
320
|
+
image = nil
|
321
|
+
|
322
|
+
if @cache_root
|
323
|
+
begin
|
324
|
+
@cache_root.open(@bucket, key) do |cahce_file_io|
|
325
|
+
CacheObject.new(cahce_file_io, client, @bucket, key) do |obj|
|
326
|
+
image = yield obj
|
327
|
+
end
|
328
|
+
end
|
329
|
+
rescue IOError => error
|
330
|
+
log.warn "cannot use S3 object cache '#{@cache_root.cache_file(@bucket, key)}': #{error}"
|
331
|
+
image = yield obj
|
332
|
+
end
|
333
|
+
else
|
334
|
+
image = yield S3Object.new(client, @bucket, key)
|
335
|
+
end
|
121
336
|
rescue AWS::S3::Errors::AccessDenied
|
122
337
|
raise S3AccessDenied.new(@bucket, path)
|
123
338
|
rescue AWS::S3::Errors::NoSuchBucket
|
@@ -125,7 +340,11 @@ module Configuration
|
|
125
340
|
rescue AWS::S3::Errors::NoSuchKey
|
126
341
|
raise S3NoSuchKeyError.new(@bucket, path)
|
127
342
|
end
|
343
|
+
image
|
128
344
|
end
|
345
|
+
|
346
|
+
S3SourceStoreBase.logger = Handler.logger_for(S3SourceStoreBase)
|
347
|
+
CacheObject.logger = S3SourceStoreBase.logger_for(CacheObject)
|
129
348
|
end
|
130
349
|
|
131
350
|
class S3Source < S3SourceStoreBase
|
@@ -134,7 +353,7 @@ module Configuration
|
|
134
353
|
end
|
135
354
|
|
136
355
|
def self.parse(configuration, node)
|
137
|
-
configuration.
|
356
|
+
configuration.sources << super
|
138
357
|
end
|
139
358
|
|
140
359
|
def realize(request_state)
|
@@ -143,12 +362,12 @@ module Configuration
|
|
143
362
|
|
144
363
|
object(rendered_path) do |object|
|
145
364
|
data = request_state.memory_limit.get do |limit|
|
146
|
-
object.read
|
365
|
+
object.read(limit + 1)
|
147
366
|
end
|
148
367
|
S3SourceStoreBase.stats.incr_total_s3_source
|
149
368
|
S3SourceStoreBase.stats.incr_total_s3_source_bytes(data.bytesize)
|
150
369
|
|
151
|
-
image = Image.new(data, object.
|
370
|
+
image = Image.new(data, object.content_type)
|
152
371
|
image.source_url = url(object)
|
153
372
|
image
|
154
373
|
end
|
@@ -194,4 +413,3 @@ module Configuration
|
|
194
413
|
StatsReporter << S3SourceStoreBase.stats
|
195
414
|
end
|
196
415
|
|
197
|
-
|