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))
|
tuple(map(lambda x:etag.update(string(x)), etag_parts))
|
||||||
return ETaggedFile(output, etag.hexdigest())
|
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):
|
def read(self, size=-1):
|
||||||
return self.request_body_file.read(size)
|
return self.request_body_file.read(size)
|
||||||
|
|
||||||
@ -286,7 +295,7 @@ def filesystem_file_output(output, outheaders, stat_result):
|
|||||||
self.use_sendfile = True
|
self.use_sendfile = True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def dynamic_output(output, outheaders):
|
def dynamic_output(output, outheaders, etag=None):
|
||||||
if isinstance(output, bytes):
|
if isinstance(output, bytes):
|
||||||
data = output
|
data = output
|
||||||
else:
|
else:
|
||||||
@ -294,10 +303,18 @@ def dynamic_output(output, outheaders):
|
|||||||
ct = outheaders.get('Content-Type')
|
ct = outheaders.get('Content-Type')
|
||||||
if not ct:
|
if not ct:
|
||||||
outheaders.set('Content-Type', 'text/plain; charset=UTF-8', replace_all=True)
|
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
|
ans.accept_ranges = False
|
||||||
return ans
|
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):
|
class GeneratedOutput(object):
|
||||||
|
|
||||||
def __init__(self, output, etag=None):
|
def __init__(self, output, etag=None):
|
||||||
@ -550,6 +567,16 @@ class HTTPConnection(HTTPRequest):
|
|||||||
self.simple_response(httplib.INTERNAL_SERVER_ERROR)
|
self.simple_response(httplib.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
def finalize_output(self, output, request, is_http1):
|
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
|
opts = self.opts
|
||||||
outheaders = request.outheaders
|
outheaders = request.outheaders
|
||||||
stat_result = file_metadata(output)
|
stat_result = file_metadata(output)
|
||||||
@ -567,6 +594,8 @@ class HTTPConnection(HTTPRequest):
|
|||||||
output = ReadableOutput(output)
|
output = ReadableOutput(output)
|
||||||
elif isinstance(output, StaticOutput):
|
elif isinstance(output, StaticOutput):
|
||||||
output = ReadableOutput(ReadOnlyFileBuffer(output.data), etag=output.etag, content_length=output.content_length)
|
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:
|
else:
|
||||||
output = GeneratedOutput(output)
|
output = GeneratedOutput(output)
|
||||||
ct = outheaders.get('Content-Type', '').partition(';')[0]
|
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'):
|
for header in ('Accept-Ranges', 'Content-Encoding', 'Transfer-Encoding', 'ETag', 'Content-Length'):
|
||||||
outheaders.pop('header', all=True)
|
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)
|
matched = '*' in none_match or (output.etag and output.etag in none_match)
|
||||||
if matched:
|
if matched:
|
||||||
if self.method in ('GET', 'HEAD'):
|
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(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)
|
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
|
# Test getting a filesystem file
|
||||||
for use_sendfile in (True, False):
|
for use_sendfile in (True, False):
|
||||||
server.change_handler(lambda conn: f)
|
server.change_handler(lambda conn: f)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user