summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--bazarr/app/app.py3
-rw-r--r--libs/flask_compress/__init__.py9
-rw-r--r--libs/flask_compress/flask_compress.py239
-rw-r--r--libs/version.txt1
4 files changed, 252 insertions, 0 deletions
diff --git a/bazarr/app/app.py b/bazarr/app/app.py
index 8445df0e0..2fa01231c 100644
--- a/bazarr/app/app.py
+++ b/bazarr/app/app.py
@@ -2,6 +2,7 @@
from flask import Flask, redirect
+from flask_compress import Compress
from flask_cors import CORS
from flask_socketio import SocketIO
@@ -15,6 +16,8 @@ socketio = SocketIO()
def create_app():
# Flask Setup
app = Flask(__name__)
+ app.config['COMPRESS_ALGORITHM'] = 'gzip'
+ Compress(app)
app.wsgi_app = ReverseProxied(app.wsgi_app)
app.config["SECRET_KEY"] = settings.general.flask_secret_key
diff --git a/libs/flask_compress/__init__.py b/libs/flask_compress/__init__.py
new file mode 100644
index 000000000..0a643ec4e
--- /dev/null
+++ b/libs/flask_compress/__init__.py
@@ -0,0 +1,9 @@
+from .flask_compress import Compress
+
+# _version.py is generated by setuptools_scm when building the package, it is not versioned.
+# If missing, this means that the imported code was most likely the git repository, that was
+# installed without the "editable" mode.
+try:
+ from ._version import __version__
+except ImportError:
+ __version__ = "0"
diff --git a/libs/flask_compress/flask_compress.py b/libs/flask_compress/flask_compress.py
new file mode 100644
index 000000000..a990a7fb2
--- /dev/null
+++ b/libs/flask_compress/flask_compress.py
@@ -0,0 +1,239 @@
+
+# Authors: William Fagan
+# Copyright (c) 2013-2017 William Fagan
+# License: The MIT License (MIT)
+
+import sys
+import functools
+from gzip import GzipFile
+import zlib
+from io import BytesIO
+
+from collections import defaultdict
+
+from flask import request, after_this_request, current_app
+
+
+if sys.version_info[:2] == (2, 6):
+ class GzipFile(GzipFile):
+ """ Backport of context manager support for python 2.6"""
+ def __enter__(self):
+ if self.fileobj is None:
+ raise ValueError("I/O operation on closed GzipFile object")
+ return self
+
+ def __exit__(self, *args):
+ self.close()
+
+
+class DictCache(object):
+
+ def __init__(self):
+ self.data = {}
+
+ def get(self, key):
+ return self.data.get(key)
+
+ def set(self, key, value):
+ self.data[key] = value
+
+
+class Compress(object):
+ """
+ The Compress object allows your application to use Flask-Compress.
+
+ When initialising a Compress object you may optionally provide your
+ :class:`flask.Flask` application object if it is ready. Otherwise,
+ you may provide it later by using the :meth:`init_app` method.
+
+ :param app: optional :class:`flask.Flask` application object
+ :type app: :class:`flask.Flask` or None
+ """
+ def __init__(self, app=None):
+ """
+ An alternative way to pass your :class:`flask.Flask` application
+ object to Flask-Compress. :meth:`init_app` also takes care of some
+ default `settings`_.
+
+ :param app: the :class:`flask.Flask` application object.
+ """
+ self.app = app
+ if app is not None:
+ self.init_app(app)
+
+ def init_app(self, app):
+ defaults = [
+ ('COMPRESS_MIMETYPES', ['text/html', 'text/css', 'text/xml',
+ 'application/json',
+ 'application/javascript']),
+ ('COMPRESS_LEVEL', 6),
+ ('COMPRESS_BR_LEVEL', 4),
+ ('COMPRESS_BR_MODE', 0),
+ ('COMPRESS_BR_WINDOW', 22),
+ ('COMPRESS_BR_BLOCK', 0),
+ ('COMPRESS_DEFLATE_LEVEL', -1),
+ ('COMPRESS_MIN_SIZE', 500),
+ ('COMPRESS_CACHE_KEY', None),
+ ('COMPRESS_CACHE_BACKEND', None),
+ ('COMPRESS_REGISTER', True),
+ ('COMPRESS_STREAMS', True),
+ ('COMPRESS_ALGORITHM', ['br', 'gzip', 'deflate']),
+ ]
+
+ for k, v in defaults:
+ app.config.setdefault(k, v)
+
+ backend = app.config['COMPRESS_CACHE_BACKEND']
+ self.cache = backend() if backend else None
+ self.cache_key = app.config['COMPRESS_CACHE_KEY']
+
+ algo = app.config['COMPRESS_ALGORITHM']
+ if isinstance(algo, str):
+ self.enabled_algorithms = [i.strip() for i in algo.split(',')]
+ else:
+ self.enabled_algorithms = list(algo)
+
+ if (app.config['COMPRESS_REGISTER'] and
+ app.config['COMPRESS_MIMETYPES']):
+ app.after_request(self.after_request)
+
+ def _choose_compress_algorithm(self, accept_encoding_header):
+ """
+ Determine which compression algorithm we're going to use based on the
+ client request. The `Accept-Encoding` header may list one or more desired
+ algorithms, together with a "quality factor" for each one (higher quality
+ means the client prefers that algorithm more).
+
+ :param accept_encoding_header: Content of the `Accept-Encoding` header
+ :return: name of a compression algorithm (`gzip`, `deflate`, `br`) or `None` if
+ the client and server don't agree on any.
+ """
+ # A flag denoting that client requested using any (`*`) algorithm,
+ # in case a specific one is not supported by the server
+ fallback_to_any = False
+
+ # Map quality factors to requested algorithm names.
+ algos_by_quality = defaultdict(set)
+
+ # Set of supported algorithms
+ server_algos_set = set(self.enabled_algorithms)
+
+ for part in accept_encoding_header.lower().split(','):
+ part = part.strip()
+ if ';q=' in part:
+ # If the client associated a quality factor with an algorithm,
+ # try to parse it. We could do the matching using a regex, but
+ # the format is so simple that it would be overkill.
+ algo = part.split(';')[0].strip()
+ try:
+ quality = float(part.split('=')[1].strip())
+ except ValueError:
+ quality = 1.0
+ else:
+ # Otherwise, use the default quality
+ algo = part
+ quality = 1.0
+
+ if algo == '*':
+ if quality > 0:
+ fallback_to_any = True
+ elif algo == 'identity': # identity means 'no compression asked'
+ algos_by_quality[quality].add(None)
+ elif algo in server_algos_set:
+ algos_by_quality[quality].add(algo)
+
+ # Choose the algorithm with the highest quality factor that the server supports.
+ #
+ # If there are multiple equally good options, choose the first supported algorithm
+ # from server configuration.
+ #
+ # If the server doesn't support any algorithm that the client requested but
+ # there's a special wildcard algorithm request (`*`), choose the first supported
+ # algorithm.
+ for _, viable_algos in sorted(algos_by_quality.items(), reverse=True):
+ if len(viable_algos) == 1:
+ return viable_algos.pop()
+ elif len(viable_algos) > 1:
+ for server_algo in self.enabled_algorithms:
+ if server_algo in viable_algos:
+ return server_algo
+
+ if fallback_to_any:
+ return self.enabled_algorithms[0]
+ return None
+
+ def after_request(self, response):
+ app = self.app or current_app
+
+ vary = response.headers.get('Vary')
+ if not vary:
+ response.headers['Vary'] = 'Accept-Encoding'
+ elif 'accept-encoding' not in vary.lower():
+ response.headers['Vary'] = '{}, Accept-Encoding'.format(vary)
+
+ accept_encoding = request.headers.get('Accept-Encoding', '')
+ chosen_algorithm = self._choose_compress_algorithm(accept_encoding)
+
+ if (chosen_algorithm is None or
+ response.mimetype not in app.config["COMPRESS_MIMETYPES"] or
+ response.status_code < 200 or
+ response.status_code >= 300 or
+ (response.is_streamed and app.config["COMPRESS_STREAMS"] is False)or
+ "Content-Encoding" in response.headers or
+ (response.content_length is not None and
+ response.content_length < app.config["COMPRESS_MIN_SIZE"])):
+ return response
+
+ response.direct_passthrough = False
+
+ if self.cache is not None:
+ key = self.cache_key(request)
+ compressed_content = self.cache.get(key)
+ if compressed_content is None:
+ compressed_content = self.compress(app, response, chosen_algorithm)
+ self.cache.set(key, compressed_content)
+ else:
+ compressed_content = self.compress(app, response, chosen_algorithm)
+
+ response.set_data(compressed_content)
+
+ response.headers['Content-Encoding'] = chosen_algorithm
+ response.headers['Content-Length'] = response.content_length
+
+ # "123456789" => "123456789:gzip" - A strong ETag validator
+ # W/"123456789" => W/"123456789:gzip" - A weak ETag validator
+ etag = response.headers.get('ETag')
+ if etag:
+ response.headers['ETag'] = '{0}:{1}"'.format(etag[:-1], chosen_algorithm)
+
+ return response
+
+ def compressed(self):
+ def decorator(f):
+ @functools.wraps(f)
+ def decorated_function(*args, **kwargs):
+ @after_this_request
+ def compressor(response):
+ return self.after_request(response)
+ return f(*args, **kwargs)
+ return decorated_function
+ return decorator
+
+ def compress(self, app, response, algorithm):
+ if algorithm == 'gzip':
+ gzip_buffer = BytesIO()
+ with GzipFile(mode='wb',
+ compresslevel=app.config['COMPRESS_LEVEL'],
+ fileobj=gzip_buffer) as gzip_file:
+ gzip_file.write(response.get_data())
+ return gzip_buffer.getvalue()
+ elif algorithm == 'deflate':
+ return zlib.compress(response.get_data(),
+ app.config['COMPRESS_DEFLATE_LEVEL'])
+ elif algorithm == 'br':
+ import brotli
+ return brotli.compress(response.get_data(),
+ mode=app.config['COMPRESS_BR_MODE'],
+ quality=app.config['COMPRESS_BR_LEVEL'],
+ lgwin=app.config['COMPRESS_BR_WINDOW'],
+ lgblock=app.config['COMPRESS_BR_BLOCK'])
diff --git a/libs/version.txt b/libs/version.txt
index 88552b535..1280cb4ab 100644
--- a/libs/version.txt
+++ b/libs/version.txt
@@ -9,6 +9,7 @@ deep-translator==1.9.1
dogpile.cache==1.1.8
fese==0.1.2
ffsubsync==0.4.20
+flask-compress==1.1.3
flask-cors==3.0.10
flask-restx==1.0.3
Flask-SocketIO==5.3.1