diff --git a/src/calibre/srv/http_response.py b/src/calibre/srv/http_response.py index 9628be6e1f..4e886c9f4f 100644 --- a/src/calibre/srv/http_response.py +++ b/src/calibre/srv/http_response.py @@ -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'): diff --git a/src/calibre/srv/tests/http.py b/src/calibre/srv/tests/http.py index 6f8ca02c6c..8a3cd3b344 100644 --- a/src/calibre/srv/tests/http.py +++ b/src/calibre/srv/tests/http.py @@ -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)