mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Allow serving etagged dynamic content that is only generated on demand
This commit is contained in:
parent
e5180db446
commit
e3c8ab98b1
@ -230,6 +230,15 @@ class RequestData(object): # {{{
|
||||
tuple(map(lambda x:etag.update(string(x)), etag_parts))
|
||||
return ETaggedFile(output, etag.hexdigest())
|
||||
|
||||
def etagged_dynamic_response(self, etag, func, content_type='text/html; charset=UTF-8'):
|
||||
' A response that is generated only if the etag does not match '
|
||||
ct = self.outheaders.get('Content-Type')
|
||||
if not ct:
|
||||
self.outheaders.set('Content-Type', content_type, replace_all=True)
|
||||
if not etag.endswith('"'):
|
||||
etag = '"%s"' % etag
|
||||
return ETaggedDynamicOutput(func, etag)
|
||||
|
||||
def read(self, size=-1):
|
||||
return self.request_body_file.read(size)
|
||||
|
||||
@ -286,7 +295,7 @@ def filesystem_file_output(output, outheaders, stat_result):
|
||||
self.use_sendfile = True
|
||||
return self
|
||||
|
||||
def dynamic_output(output, outheaders):
|
||||
def dynamic_output(output, outheaders, etag=None):
|
||||
if isinstance(output, bytes):
|
||||
data = output
|
||||
else:
|
||||
@ -294,10 +303,18 @@ def dynamic_output(output, outheaders):
|
||||
ct = outheaders.get('Content-Type')
|
||||
if not ct:
|
||||
outheaders.set('Content-Type', 'text/plain; charset=UTF-8', replace_all=True)
|
||||
ans = ReadableOutput(ReadOnlyFileBuffer(data))
|
||||
ans = ReadableOutput(ReadOnlyFileBuffer(data), etag=etag)
|
||||
ans.accept_ranges = False
|
||||
return ans
|
||||
|
||||
class ETaggedDynamicOutput(object):
|
||||
|
||||
def __init__(self, func, etag):
|
||||
self.func, self.etag = func, etag
|
||||
|
||||
def __call__(self):
|
||||
return self.func()
|
||||
|
||||
class GeneratedOutput(object):
|
||||
|
||||
def __init__(self, output, etag=None):
|
||||
@ -550,6 +567,16 @@ class HTTPConnection(HTTPRequest):
|
||||
self.simple_response(httplib.INTERNAL_SERVER_ERROR)
|
||||
|
||||
def finalize_output(self, output, request, is_http1):
|
||||
none_match = parse_if_none_match(request.inheaders.get('If-None-Match', ''))
|
||||
if isinstance(output, ETaggedDynamicOutput):
|
||||
matched = '*' in none_match or (output.etag and output.etag in none_match)
|
||||
if matched:
|
||||
if self.method in ('GET', 'HEAD'):
|
||||
self.send_not_modified(output.etag)
|
||||
else:
|
||||
self.simple_response(httplib.PRECONDITION_FAILED)
|
||||
return
|
||||
|
||||
opts = self.opts
|
||||
outheaders = request.outheaders
|
||||
stat_result = file_metadata(output)
|
||||
@ -567,6 +594,8 @@ class HTTPConnection(HTTPRequest):
|
||||
output = ReadableOutput(output)
|
||||
elif isinstance(output, StaticOutput):
|
||||
output = ReadableOutput(ReadOnlyFileBuffer(output.data), etag=output.etag, content_length=output.content_length)
|
||||
elif isinstance(output, ETaggedDynamicOutput):
|
||||
output = dynamic_output(output(), outheaders, etag=output.etag)
|
||||
else:
|
||||
output = GeneratedOutput(output)
|
||||
ct = outheaders.get('Content-Type', '').partition(';')[0]
|
||||
@ -587,7 +616,6 @@ class HTTPConnection(HTTPRequest):
|
||||
for header in ('Accept-Ranges', 'Content-Encoding', 'Transfer-Encoding', 'ETag', 'Content-Length'):
|
||||
outheaders.pop('header', all=True)
|
||||
|
||||
none_match = parse_if_none_match(request.inheaders.get('If-None-Match', ''))
|
||||
matched = '*' in none_match or (output.etag and output.etag in none_match)
|
||||
if matched:
|
||||
if self.method in ('GET', 'HEAD'):
|
||||
|
@ -316,6 +316,25 @@ class TestHTTP(BaseTest):
|
||||
self.ae(str(len(raw)), r.getheader('Calibre-Uncompressed-Length'))
|
||||
self.ae(r.status, httplib.OK), self.ae(zlib.decompress(r.read(), 16+zlib.MAX_WBITS), raw)
|
||||
|
||||
# Test dynamic etagged content
|
||||
num_calls = [0]
|
||||
def edfunc():
|
||||
num_calls[0] += 1
|
||||
return b'data'
|
||||
server.change_handler(lambda conn:conn.etagged_dynamic_response("xxx", edfunc))
|
||||
conn = server.connect()
|
||||
conn.request('GET', '/an_etagged_path')
|
||||
r = conn.getresponse()
|
||||
self.ae(r.status, httplib.OK), self.ae(r.read(), b'data')
|
||||
etag = r.getheader('ETag')
|
||||
self.ae(etag, b'"xxx"')
|
||||
self.ae(r.getheader('Content-Length'), '4')
|
||||
conn.request('GET', '/an_etagged_path', headers={'If-None-Match':etag})
|
||||
r = conn.getresponse()
|
||||
self.ae(r.status, httplib.NOT_MODIFIED)
|
||||
self.ae(r.read(), b'')
|
||||
self.ae(num_calls[0], 1)
|
||||
|
||||
# Test getting a filesystem file
|
||||
for use_sendfile in (True, False):
|
||||
server.change_handler(lambda conn: f)
|
||||
|
Loading…
x
Reference in New Issue
Block a user