diff --git a/frontends/etiquette_flask/backend/caching.py b/frontends/etiquette_flask/backend/caching.py index 63e678f..c732175 100644 --- a/frontends/etiquette_flask/backend/caching.py +++ b/frontends/etiquette_flask/backend/caching.py @@ -1,70 +1,7 @@ -import flask; from flask import request -import functools -import time - from voussoirkit import cacheclass -from voussoirkit import passwordy import etiquette -def cached_endpoint(max_age): - ''' - The cached_endpoint decorator can be used on slow endpoints that don't need - to be constantly updated or endpoints that produce large, static responses. - - WARNING: The return value of the endpoint is shared with all users. - You should never use this cache on an endpoint that provides private - or personalized data, and you should not try to pass other headers through - the response. - - When the function is run, its return value is stored and a random etag is - generated so that subsequent runs can respond with 304. This way, large - response bodies do not need to be transmitted often. - - Given a nonzero max_age, the endpoint will only be run once per max_age - seconds on a global basis (not per-user). This way, you can prevent a slow - function from being run very often. In-between requests will just receive - the previous return value (still using 200 or 304 as appropriate for the - client's provided etag). - - An example use case would be large-sized data dumps that don't need to be - precisely up to date every time. - ''' - state = { - 'max_age': max_age, - 'stored_value': None, - 'stored_etag': None, - 'headers': {'ETag': None, 'Cache-Control': f'max-age={max_age}'}, - 'last_run': 0, - } - - def wrapper(function): - @functools.wraps(function) - def wrapped(*args, **kwargs): - if (not state['max_age']) or (time.time() - state['last_run'] > state['max_age']): - value = function(*args, **kwargs) - if isinstance(value, flask.Response): - if value.headers.get('Content-Type'): - state['headers']['Content-Type'] = value.headers.get('Content-Type') - value = value.response - if value != state['stored_value']: - state['stored_value'] = value - state['stored_etag'] = passwordy.random_hex(20) - state['headers']['ETag'] = state['stored_etag'] - state['last_run'] = time.time() - else: - value = state['stored_value'] - - client_etag = request.headers.get('If-None-Match', None) - if client_etag == state['stored_etag']: - response = flask.Response(status=304, headers=state['headers']) - else: - response = flask.Response(value, status=200, headers=state['headers']) - - return response - return wrapped - return wrapper - class FileCacheManager: ''' The FileCacheManager serves ETag and Cache-Control headers for disk files. diff --git a/frontends/etiquette_flask/backend/decorators.py b/frontends/etiquette_flask/backend/decorators.py index 43fab22..44d5ad6 100644 --- a/frontends/etiquette_flask/backend/decorators.py +++ b/frontends/etiquette_flask/backend/decorators.py @@ -1,11 +1,71 @@ -import flask -from flask import request +import flask; from flask import request import functools +import time + +from voussoirkit import passwordy import etiquette from . import jsonify +def cached_endpoint(max_age): + ''' + The cached_endpoint decorator can be used on slow endpoints that don't need + to be constantly updated or endpoints that produce large, static responses. + + WARNING: The return value of the endpoint is shared with all users. + You should never use this cache on an endpoint that provides private + or personalized data, and you should not try to pass other headers through + the response. + + When the function is run, its return value is stored and a random etag is + generated so that subsequent runs can respond with 304. This way, large + response bodies do not need to be transmitted often. + + Given a nonzero max_age, the endpoint will only be run once per max_age + seconds on a global basis (not per-user). This way, you can prevent a slow + function from being run very often. In-between requests will just receive + the previous return value (still using 200 or 304 as appropriate for the + client's provided etag). + + An example use case would be large-sized data dumps that don't need to be + precisely up to date every time. + ''' + state = { + 'max_age': max_age, + 'stored_value': None, + 'stored_etag': None, + 'headers': {'ETag': None, 'Cache-Control': f'max-age={max_age}'}, + 'last_run': 0, + } + + def wrapper(function): + @functools.wraps(function) + def wrapped(*args, **kwargs): + if (not state['max_age']) or (time.time() - state['last_run'] > state['max_age']): + value = function(*args, **kwargs) + if isinstance(value, flask.Response): + if value.headers.get('Content-Type'): + state['headers']['Content-Type'] = value.headers.get('Content-Type') + value = value.response + if value != state['stored_value']: + state['stored_value'] = value + state['stored_etag'] = passwordy.random_hex(20) + state['headers']['ETag'] = state['stored_etag'] + state['last_run'] = time.time() + else: + value = state['stored_value'] + + client_etag = request.headers.get('If-None-Match', None) + if client_etag == state['stored_etag']: + response = flask.Response(status=304, headers=state['headers']) + else: + response = flask.Response(value, status=200, headers=state['headers']) + + return response + return wrapped + return wrapper + def catch_etiquette_exception(function): ''' If an EtiquetteException is raised, automatically catch it and convert it diff --git a/frontends/etiquette_flask/backend/endpoints/album_endpoints.py b/frontends/etiquette_flask/backend/endpoints/album_endpoints.py index 74e0559..53c8004 100644 --- a/frontends/etiquette_flask/backend/endpoints/album_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/album_endpoints.py @@ -6,7 +6,6 @@ from voussoirkit import stringtools import etiquette -from .. import caching from .. import common from .. import decorators from .. import jsonify @@ -159,7 +158,7 @@ def post_album_edit(album_id): # Album listings ################################################################################### @site.route('/all_albums.json') -@caching.cached_endpoint(max_age=0) +@decorators.cached_endpoint(max_age=0) def get_all_album_names(): all_albums = {album.display_name: album.id for album in common.P.get_albums()} response = {'updated': int(time.time()), 'albums': all_albums} diff --git a/frontends/etiquette_flask/backend/endpoints/tag_endpoints.py b/frontends/etiquette_flask/backend/endpoints/tag_endpoints.py index 274b69e..5a105ce 100644 --- a/frontends/etiquette_flask/backend/endpoints/tag_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/tag_endpoints.py @@ -3,7 +3,6 @@ import time import etiquette -from .. import caching from .. import common from .. import decorators from .. import jsonify @@ -65,7 +64,7 @@ def post_tag_remove_child(tagname): # Tag listings ##################################################################################### @site.route('/all_tags.json') -@caching.cached_endpoint(max_age=0) +@decorators.cached_endpoint(max_age=0) def get_all_tag_names(): all_tags = list(common.P.get_all_tag_names()) all_synonyms = common.P.get_all_synonyms()