diff --git a/frontends/etiquette_flask/backend/decorators.py b/frontends/etiquette_flask/backend/decorators.py index 3bc4589..0407ba4 100644 --- a/frontends/etiquette_flask/backend/decorators.py +++ b/frontends/etiquette_flask/backend/decorators.py @@ -1,93 +1,11 @@ import flask; from flask import request import functools -import time import werkzeug.datastructures -from voussoirkit import dotdict from voussoirkit import flasktools -from voussoirkit import passwordy -from voussoirkit import sentinel import etiquette -NOT_CACHED = sentinel.Sentinel('not cached', truthyness=False) - -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). - - With max_age=0, the function will be run every time to check if the value - has changed, but if it hasn't changed then we can still send a 304 response, - saving bandwidth. - - An example use case would be large-sized data dumps that don't need to be - precisely up to date every time. - ''' - if max_age < 0: - raise ValueError(f'max_age should be positive, not {max_age}.') - - state = dotdict.DotDict({ - 'max_age': max_age, - 'stored_value': NOT_CACHED, - 'stored_etag': None, - 'headers': {'ETag': None, 'Cache-Control': f'max-age={max_age}'}, - 'last_run': 0, - }) - - def wrapper(function): - def get_value(*args, **kwargs): - can_bail = ( - state.stored_value is not NOT_CACHED and - state.max_age != 0 and - (time.time() - state.last_run) < state.max_age - ) - if can_bail: - return state.stored_value - - 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() - return value - - @functools.wraps(function) - def wrapped(*args, **kwargs): - value = get_value(*args, **kwargs) - - 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 @@ -128,32 +46,3 @@ def give_theme_cookie(function): return response return wrapped - -def required_fields(fields, forbid_whitespace=False): - ''' - Declare that the endpoint requires certain POST body fields. Without them, - we respond with 400 and a message. - - forbid_whitespace: - If True, then providing the field is not good enough. It must also - contain at least some non-whitespace characters. - ''' - def wrapper(function): - @functools.wraps(function) - def wrapped(*args, **kwargs): - for requirement in fields: - missing = ( - requirement not in request.form or - (forbid_whitespace and request.form[requirement].strip() == '') - ) - if missing: - response = { - 'error_type': 'MISSING_FIELDS', - 'error_message': 'Required fields: %s' % ', '.join(fields), - } - response = flasktools.make_json_response(response, status=400) - return response - - return function(*args, **kwargs) - return wrapped - return wrapper diff --git a/frontends/etiquette_flask/backend/endpoints/album_endpoints.py b/frontends/etiquette_flask/backend/endpoints/album_endpoints.py index 002b1d0..ee25bb1 100644 --- a/frontends/etiquette_flask/backend/endpoints/album_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/album_endpoints.py @@ -9,7 +9,6 @@ from voussoirkit import stringtools import etiquette from .. import common -from .. import decorators site = common.site session_manager = common.session_manager @@ -57,7 +56,7 @@ def get_album_zip(album_id): return flask.Response(streamed_zip, headers=outgoing_headers) @site.route('/album//add_child', methods=['POST']) -@decorators.required_fields(['child_id'], forbid_whitespace=True) +@flasktools.required_fields(['child_id'], forbid_whitespace=True) def post_album_add_child(album_id): album = common.P_album(album_id, response_type='json') @@ -69,7 +68,7 @@ def post_album_add_child(album_id): return flasktools.make_json_response(response) @site.route('/album//remove_child', methods=['POST']) -@decorators.required_fields(['child_id'], forbid_whitespace=True) +@flasktools.required_fields(['child_id'], forbid_whitespace=True) def post_album_remove_child(album_id): album = common.P_album(album_id, response_type='json') @@ -98,7 +97,7 @@ def post_album_refresh_directories(album_id): return flasktools.make_json_response({}) @site.route('/album//set_thumbnail_photo', methods=['POST']) -@decorators.required_fields(['photo_id'], forbid_whitespace=True) +@flasktools.required_fields(['photo_id'], forbid_whitespace=True) def post_album_set_thumbnail_photo(album_id): album = common.P_album(album_id, response_type='json') photo = common.P_photo(request.form['photo_id'], response_type='json') @@ -109,7 +108,7 @@ def post_album_set_thumbnail_photo(album_id): # Album photo operations ########################################################################### @site.route('/album//add_photo', methods=['POST']) -@decorators.required_fields(['photo_id'], forbid_whitespace=True) +@flasktools.required_fields(['photo_id'], forbid_whitespace=True) def post_album_add_photo(album_id): ''' Add a photo or photos to this album. @@ -123,7 +122,7 @@ def post_album_add_photo(album_id): return flasktools.make_json_response(response) @site.route('/album//remove_photo', methods=['POST']) -@decorators.required_fields(['photo_id'], forbid_whitespace=True) +@flasktools.required_fields(['photo_id'], forbid_whitespace=True) def post_album_remove_photo(album_id): ''' Remove a photo or photos from this album. @@ -195,7 +194,7 @@ def post_album_show_in_folder(album_id): # Album listings ################################################################################### @site.route('/all_albums.json') -@decorators.cached_endpoint(max_age=15) +@flasktools.cached_endpoint(max_age=15) def get_all_album_names(): all_albums = {album.id: album.display_name for album in common.P.get_albums()} response = {'albums': all_albums} diff --git a/frontends/etiquette_flask/backend/endpoints/bookmark_endpoints.py b/frontends/etiquette_flask/backend/endpoints/bookmark_endpoints.py index 92010c3..f6894f7 100644 --- a/frontends/etiquette_flask/backend/endpoints/bookmark_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/bookmark_endpoints.py @@ -5,7 +5,6 @@ from voussoirkit import flasktools import etiquette from .. import common -from .. import decorators site = common.site session_manager = common.session_manager @@ -45,7 +44,7 @@ def get_bookmarks_json(): # Bookmark create and delete ####################################################################### @site.route('/bookmarks/create_bookmark', methods=['POST']) -@decorators.required_fields(['url'], forbid_whitespace=True) +@flasktools.required_fields(['url'], forbid_whitespace=True) def post_bookmark_create(): url = request.form['url'] title = request.form.get('title', None) diff --git a/frontends/etiquette_flask/backend/endpoints/photo_endpoints.py b/frontends/etiquette_flask/backend/endpoints/photo_endpoints.py index 1b6b978..eb06046 100644 --- a/frontends/etiquette_flask/backend/endpoints/photo_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/photo_endpoints.py @@ -10,7 +10,6 @@ from voussoirkit import stringtools import etiquette from .. import common -from .. import decorators from .. import helpers site = common.site @@ -98,7 +97,7 @@ def post_photo_add_remove_tag_core(photo_ids, tagname, add_or_remove): return flasktools.make_json_response(response) @site.route('/photo//add_tag', methods=['POST']) -@decorators.required_fields(['tagname'], forbid_whitespace=True) +@flasktools.required_fields(['tagname'], forbid_whitespace=True) def post_photo_add_tag(photo_id): ''' Add a tag to this photo. @@ -111,7 +110,7 @@ def post_photo_add_tag(photo_id): return response @site.route('/photo//copy_tags', methods=['POST']) -@decorators.required_fields(['other_photo'], forbid_whitespace=True) +@flasktools.required_fields(['other_photo'], forbid_whitespace=True) def post_photo_copy_tags(photo_id): ''' Copy the tags from another photo. @@ -123,7 +122,7 @@ def post_photo_copy_tags(photo_id): return flasktools.make_json_response([tag.jsonify(minimal=True) for tag in photo.get_tags()]) @site.route('/photo//remove_tag', methods=['POST']) -@decorators.required_fields(['tagname'], forbid_whitespace=True) +@flasktools.required_fields(['tagname'], forbid_whitespace=True) def post_photo_remove_tag(photo_id): ''' Remove a tag from this photo. @@ -136,7 +135,7 @@ def post_photo_remove_tag(photo_id): return response @site.route('/batch/photos/add_tag', methods=['POST']) -@decorators.required_fields(['photo_ids', 'tagname'], forbid_whitespace=True) +@flasktools.required_fields(['photo_ids', 'tagname'], forbid_whitespace=True) def post_batch_photos_add_tag(): response = post_photo_add_remove_tag_core( photo_ids=request.form['photo_ids'], @@ -146,7 +145,7 @@ def post_batch_photos_add_tag(): return response @site.route('/batch/photos/remove_tag', methods=['POST']) -@decorators.required_fields(['photo_ids', 'tagname'], forbid_whitespace=True) +@flasktools.required_fields(['photo_ids', 'tagname'], forbid_whitespace=True) def post_batch_photos_remove_tag(): response = post_photo_add_remove_tag_core( photo_ids=request.form['photo_ids'], @@ -194,7 +193,7 @@ def post_photo_refresh_metadata(photo_id): return response @site.route('/batch/photos/refresh_metadata', methods=['POST']) -@decorators.required_fields(['photo_ids'], forbid_whitespace=True) +@flasktools.required_fields(['photo_ids'], forbid_whitespace=True) def post_batch_photos_refresh_metadata(): response = post_photo_refresh_metadata_core(photo_ids=request.form['photo_ids']) return response @@ -238,14 +237,14 @@ def post_photo_show_in_folder(photo_id): flask.abort(501) @site.route('/batch/photos/set_searchhidden', methods=['POST']) -@decorators.required_fields(['photo_ids'], forbid_whitespace=True) +@flasktools.required_fields(['photo_ids'], forbid_whitespace=True) def post_batch_photos_set_searchhidden(): photo_ids = request.form['photo_ids'] response = post_batch_photos_searchhidden_core(photo_ids=photo_ids, searchhidden=True) return response @site.route('/batch/photos/unset_searchhidden', methods=['POST']) -@decorators.required_fields(['photo_ids'], forbid_whitespace=True) +@flasktools.required_fields(['photo_ids'], forbid_whitespace=True) def post_batch_photos_unset_searchhidden(): photo_ids = request.form['photo_ids'] response = post_batch_photos_searchhidden_core(photo_ids=photo_ids, searchhidden=False) @@ -258,7 +257,7 @@ def get_clipboard_page(): return common.render_template(request, 'clipboard.html') @site.route('/batch/photos', methods=['POST']) -@decorators.required_fields(['photo_ids'], forbid_whitespace=True) +@flasktools.required_fields(['photo_ids'], forbid_whitespace=True) def post_batch_photos(): ''' Return a list of photo.jsonify() for each requested photo id. @@ -273,7 +272,7 @@ def post_batch_photos(): return response @site.route('/batch/photos/photo_card', methods=['POST']) -@decorators.required_fields(['photo_ids'], forbid_whitespace=True) +@flasktools.required_fields(['photo_ids'], forbid_whitespace=True) def post_batch_photos_photo_cards(): photo_ids = request.form['photo_ids'] @@ -328,7 +327,7 @@ def get_batch_photos_download_zip(zip_token): return flask.Response(streamed_zip, headers=outgoing_headers) @site.route('/batch/photos/download_zip', methods=['POST']) -@decorators.required_fields(['photo_ids'], forbid_whitespace=True) +@flasktools.required_fields(['photo_ids'], forbid_whitespace=True) def post_batch_photos_download_zip(): ''' Initiating file downloads via POST requests is a bit clunky and unreliable, diff --git a/frontends/etiquette_flask/backend/endpoints/tag_endpoints.py b/frontends/etiquette_flask/backend/endpoints/tag_endpoints.py index 65e8361..b3a75ed 100644 --- a/frontends/etiquette_flask/backend/endpoints/tag_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/tag_endpoints.py @@ -5,7 +5,6 @@ from voussoirkit import flasktools import etiquette from .. import common -from .. import decorators site = common.site session_manager = common.session_manager @@ -57,7 +56,7 @@ def post_tag_edit(tagname): return response @site.route('/tag//add_child', methods=['POST']) -@decorators.required_fields(['child_name'], forbid_whitespace=True) +@flasktools.required_fields(['child_name'], forbid_whitespace=True) def post_tag_add_child(tagname): parent = common.P_tag(tagname, response_type='json') child = common.P_tag(request.form['child_name'], response_type='json') @@ -66,7 +65,7 @@ def post_tag_add_child(tagname): return flasktools.make_json_response(response) @site.route('/tag//add_synonym', methods=['POST']) -@decorators.required_fields(['syn_name'], forbid_whitespace=True) +@flasktools.required_fields(['syn_name'], forbid_whitespace=True) def post_tag_add_synonym(tagname): syn_name = request.form['syn_name'] @@ -77,7 +76,7 @@ def post_tag_add_synonym(tagname): return flasktools.make_json_response(response) @site.route('/tag//remove_child', methods=['POST']) -@decorators.required_fields(['child_name'], forbid_whitespace=True) +@flasktools.required_fields(['child_name'], forbid_whitespace=True) def post_tag_remove_child(tagname): parent = common.P_tag(tagname, response_type='json') child = common.P_tag(request.form['child_name'], response_type='json') @@ -86,7 +85,7 @@ def post_tag_remove_child(tagname): return flasktools.make_json_response(response) @site.route('/tag//remove_synonym', methods=['POST']) -@decorators.required_fields(['syn_name'], forbid_whitespace=True) +@flasktools.required_fields(['syn_name'], forbid_whitespace=True) def post_tag_remove_synonym(tagname): syn_name = request.form['syn_name'] @@ -99,7 +98,7 @@ def post_tag_remove_synonym(tagname): # Tag listings ##################################################################################### @site.route('/all_tags.json') -@decorators.cached_endpoint(max_age=15) +@flasktools.cached_endpoint(max_age=15) def get_all_tag_names(): all_tags = list(common.P.get_all_tag_names()) all_synonyms = common.P.get_all_synonyms() @@ -158,7 +157,7 @@ def get_tags_json(): # Tag create and delete ############################################################################ @site.route('/tags/create_tag', methods=['POST']) -@decorators.required_fields(['name'], forbid_whitespace=True) +@flasktools.required_fields(['name'], forbid_whitespace=True) def post_tag_create(): name = request.form['name'] description = request.form.get('description', None) @@ -168,7 +167,7 @@ def post_tag_create(): return flasktools.make_json_response(response) @site.route('/tags/easybake', methods=['POST']) -@decorators.required_fields(['easybake_string'], forbid_whitespace=True) +@flasktools.required_fields(['easybake_string'], forbid_whitespace=True) def post_tag_easybake(): easybake_string = request.form['easybake_string'] diff --git a/frontends/etiquette_flask/backend/endpoints/user_endpoints.py b/frontends/etiquette_flask/backend/endpoints/user_endpoints.py index eb09c81..dc337e2 100644 --- a/frontends/etiquette_flask/backend/endpoints/user_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/user_endpoints.py @@ -5,7 +5,6 @@ from voussoirkit import flasktools import etiquette from .. import common -from .. import decorators from .. import sessions site = common.site @@ -67,7 +66,7 @@ def get_login(): return response @site.route('/login', methods=['POST']) -@decorators.required_fields(['username', 'password']) +@flasktools.required_fields(['username', 'password']) def post_login(): session = session_manager.get(request) if session.user: @@ -107,7 +106,7 @@ def get_register(): return flask.redirect('/login') @site.route('/register', methods=['POST']) -@decorators.required_fields(['username', 'password_1', 'password_2']) +@flasktools.required_fields(['username', 'password_1', 'password_2']) def post_register(): session = session_manager.get(request) if session.user: