2017-08-11 21:05:11 +02:00
|
|
|
import brotli as brotli_
|
2017-08-11 19:44:09 +02:00
|
|
|
from flask import request, make_response
|
2017-08-11 19:13:37 +02:00
|
|
|
from threading import Thread
|
|
|
|
from hashlib import sha256
|
|
|
|
import redis
|
2017-08-11 19:44:09 +02:00
|
|
|
import os.path
|
|
|
|
import mimetypes
|
2017-08-11 19:13:37 +02:00
|
|
|
|
|
|
|
class BrotliCache(object):
|
2017-08-11 20:58:32 +02:00
|
|
|
def __init__(self, redis_kwargs={}, max_wait=0.3, expire=60*60*12):
|
2017-08-11 19:13:37 +02:00
|
|
|
self.redis = redis.StrictRedis(**redis_kwargs)
|
2017-08-11 20:58:32 +02:00
|
|
|
self.max_wait = max_wait
|
|
|
|
self.expire = expire
|
2017-08-11 19:13:37 +02:00
|
|
|
|
2017-08-11 21:05:11 +02:00
|
|
|
def compress(self, cache_key, lock_key, body, mode=brotli_.MODE_GENERIC):
|
|
|
|
encbody = brotli_.compress(body, mode=mode)
|
2017-08-11 20:58:32 +02:00
|
|
|
self.redis.set(cache_key, encbody, ex=self.expire)
|
2017-08-11 19:13:37 +02:00
|
|
|
self.redis.delete(lock_key)
|
|
|
|
|
|
|
|
def wrap_response(self, response):
|
|
|
|
if 'br' not in request.accept_encodings or response.is_streamed:
|
|
|
|
return response
|
|
|
|
|
|
|
|
body = response.get_data()
|
|
|
|
digest = sha256(body).hexdigest()
|
|
|
|
cache_key = 'brotlicache:{}'.format(digest)
|
|
|
|
|
|
|
|
encbody = self.redis.get(cache_key)
|
2017-08-12 22:01:42 +02:00
|
|
|
response.headers.set('x-brotli-cache', 'HIT')
|
2017-08-11 20:58:32 +02:00
|
|
|
if not encbody:
|
2017-08-12 22:01:42 +02:00
|
|
|
response.headers.set('x-brotli-cache', 'MISS')
|
2017-08-11 20:58:32 +02:00
|
|
|
lock_key = 'brotlicache:lock:{}'.format(digest)
|
|
|
|
if self.redis.set(lock_key, 1, nx=True, ex=10):
|
2017-08-11 21:05:11 +02:00
|
|
|
mode = brotli_.MODE_TEXT if response.content_type.startswith('text/') else brotli_.MODE_GENERIC
|
|
|
|
t = Thread(target=self.compress, args=(cache_key, lock_key, body, mode))
|
2017-08-11 20:58:32 +02:00
|
|
|
t.start()
|
|
|
|
if self.max_wait > 0:
|
|
|
|
t.join(self.max_wait)
|
|
|
|
encbody = self.redis.get(cache_key)
|
2017-08-11 19:13:37 +02:00
|
|
|
if encbody:
|
|
|
|
response.headers.set('content-encoding', 'br')
|
2017-08-11 19:44:09 +02:00
|
|
|
response.headers.set('vary', 'accept-encoding')
|
2017-08-11 19:13:37 +02:00
|
|
|
response.set_data(encbody)
|
|
|
|
return response
|
2017-08-11 17:57:32 +02:00
|
|
|
|
|
|
|
return response
|
2017-08-11 19:44:09 +02:00
|
|
|
|
|
|
|
def brotli(app, static = True, dynamic = True):
|
|
|
|
original_static = app.view_functions['static']
|
|
|
|
def static_maybe_gzip_brotli(filename=None):
|
|
|
|
path = os.path.join(app.static_folder, filename)
|
|
|
|
for encoding, extension in (('br', '.br'), ('gzip', '.gz')):
|
|
|
|
if encoding not in request.accept_encodings:
|
|
|
|
continue
|
|
|
|
encpath = path + extension
|
|
|
|
if os.path.isfile(encpath):
|
|
|
|
resp = make_response(original_static(filename=filename + extension))
|
|
|
|
resp.headers.set('content-encoding', encoding)
|
|
|
|
resp.headers.set('vary', 'accept-encoding')
|
|
|
|
mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
|
|
|
resp.headers.set('content-type', mimetype)
|
|
|
|
return resp
|
|
|
|
return original_static(filename=filename)
|
|
|
|
if static:
|
|
|
|
app.view_functions['static'] = static_maybe_gzip_brotli
|
|
|
|
if dynamic:
|
|
|
|
cache = BrotliCache()
|
|
|
|
app.after_request(cache.wrap_response)
|