From 60e7078e13e91ad08e564eef2d290d35cdb33c03 Mon Sep 17 00:00:00 2001 From: Fluffy-Bean Date: Fri, 21 Apr 2023 17:34:25 +0000 Subject: [PATCH] Move APIs to their own folder --- onlylegs/__init__.py | 23 +-- onlylegs/api.py | 268 ------------------------------- onlylegs/api/__init__.py | 0 onlylegs/api/account.py | 92 +++++++++++ onlylegs/api/group.py | 73 +++++++++ onlylegs/api/media.py | 128 +++++++++++++++ onlylegs/templates/group.html | 8 +- onlylegs/templates/image.html | 10 +- onlylegs/templates/index.html | 2 +- onlylegs/templates/layout.html | 2 +- onlylegs/templates/list.html | 4 +- onlylegs/templates/profile.html | 6 +- onlylegs/templates/settings.html | 4 +- 13 files changed, 325 insertions(+), 295 deletions(-) delete mode 100644 onlylegs/api.py create mode 100644 onlylegs/api/__init__.py create mode 100644 onlylegs/api/account.py create mode 100644 onlylegs/api/group.py create mode 100644 onlylegs/api/media.py diff --git a/onlylegs/__init__.py b/onlylegs/__init__.py index 39fbfb2..ac5a72a 100644 --- a/onlylegs/__init__.py +++ b/onlylegs/__init__.py @@ -15,8 +15,9 @@ from werkzeug.security import generate_password_hash from onlylegs.extensions import db, migrate, login_manager, assets, compress, cache from onlylegs.config import INSTANCE_DIR, MIGRATIONS_DIR from onlylegs.models import User -from onlylegs.views import index, image, group, settings, profile -from onlylegs import api, auth +from onlylegs.views import index as view_index, image as view_image, group as view_group, settings as view_settings, profile as view_profile +from onlylegs.api import media as api_media, group as api_group, account as api_account +from onlylegs import auth as view_auth from onlylegs import gwagwa @@ -107,13 +108,17 @@ def create_app(): # pylint: disable=R0914 assets.register("styles", styles) # BLUEPRINTS - app.register_blueprint(auth.blueprint) - app.register_blueprint(api.blueprint) - app.register_blueprint(index.blueprint) - app.register_blueprint(image.blueprint) - app.register_blueprint(group.blueprint) - app.register_blueprint(profile.blueprint) - app.register_blueprint(settings.blueprint) + app.register_blueprint(view_auth.blueprint) + app.register_blueprint(view_index.blueprint) + app.register_blueprint(view_image.blueprint) + app.register_blueprint(view_group.blueprint) + app.register_blueprint(view_profile.blueprint) + app.register_blueprint(view_settings.blueprint) + + # APIS + app.register_blueprint(api_media.blueprint) + app.register_blueprint(api_group.blueprint) + app.register_blueprint(api_account.blueprint) # CACHE AND COMPRESS cache.init_app(app) diff --git a/onlylegs/api.py b/onlylegs/api.py deleted file mode 100644 index b4b88aa..0000000 --- a/onlylegs/api.py +++ /dev/null @@ -1,268 +0,0 @@ -""" -Onlylegs - API endpoints -""" -from uuid import uuid4 -import os -import pathlib -import re -import logging - -from flask import Blueprint, send_from_directory, abort, flash, request, current_app -from flask_login import login_required, current_user - -from colorthief import ColorThief - -from onlylegs.extensions import db -from onlylegs.models import Post, Group, GroupJunction, User -from onlylegs.utils import metadata as mt -from onlylegs.utils.generate_image import generate_thumbnail - - -blueprint = Blueprint("api", __name__, url_prefix="/api") - - -@blueprint.route("/media/", methods=["GET"]) -def media(path): - """ - Returns a file from the uploads folder - r for resolution, thumb for thumbnail etc - e for extension, jpg, png etc - """ - res = request.args.get("r", default=None, type=str) - ext = request.args.get("e", default=None, type=str) - # path = secure_filename(path) - - # if no args are passed, return the raw file - if not res and not ext: - if not os.path.exists(os.path.join(current_app.config["MEDIA_FOLDER"], path)): - abort(404) - return send_from_directory(current_app.config["MEDIA_FOLDER"], path) - - thumb = generate_thumbnail(path, res, ext) - if not thumb: - abort(404) - - return send_from_directory(os.path.dirname(thumb), os.path.basename(thumb)) - - -@blueprint.route("/upload", methods=["POST"]) -@login_required -def upload(): - """ - Uploads an image to the server and saves it to the database - """ - form_file = request.files["file"] - form = request.form - - # If no image is uploaded, return 404 error - if not form_file: - return abort(404) - - # Get file extension, generate random name and set file path - img_ext = pathlib.Path(form_file.filename).suffix.replace(".", "").lower() - img_name = "GWAGWA_" + str(uuid4()) - img_path = os.path.join( - current_app.config["UPLOAD_FOLDER"], img_name + "." + img_ext - ) - - # Check if file extension is allowed - if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys(): - logging.info("File extension not allowed: %s", img_ext) - abort(403) - - # Save file - try: - form_file.save(img_path) - except OSError as err: - logging.info("Error saving file %s because of %s", img_path, err) - abort(500) - - img_exif = mt.Metadata(img_path).yoink() # Get EXIF data - img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette - - # Save to database - query = Post( - author_id=current_user.id, - filename=img_name + "." + img_ext, - mimetype=img_ext, - exif=img_exif, - colours=img_colors, - description=form["description"], - alt=form["alt"], - ) - - db.session.add(query) - db.session.commit() - - return "Gwa Gwa" # Return something so the browser doesn't show an error - - -@blueprint.route("/delete/", methods=["POST"]) -@login_required -def delete_image(image_id): - """ - Deletes an image from the server and database - """ - post = db.get_or_404(Post, image_id) - - # Check if image exists and if user is allowed to delete it (author) - if post.author_id != current_user.id: - abort(403) - - # Delete file - try: - os.remove(os.path.join(current_app.config["UPLOAD_FOLDER"], post.filename)) - except FileNotFoundError: - logging.warning( - "File not found: %s, already deleted or never existed", post.filename - ) - - # Delete cached files - cache_name = post.filename.rsplit(".")[0] - for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(cache_name + "*"): - os.remove(cache_file) - - GroupJunction.query.filter_by(post_id=image_id).delete() - db.session.delete(post) - db.session.commit() - - logging.info("Removed image (%s) %s", image_id, post.filename) - flash(["Image was all in Le Head!", "1"]) - return "Gwa Gwa" - - -@blueprint.route("/group/create", methods=["POST"]) -@login_required -def create_group(): - """ - Creates a group - """ - new_group = Group( - name=request.form["name"], - description=request.form["description"], - author_id=current_user.id, - ) - - db.session.add(new_group) - db.session.commit() - - return ":3" - - -@blueprint.route("/group/modify", methods=["POST"]) -@login_required -def modify_group(): - """ - Changes the images in a group - """ - group_id = request.form["group"] - image_id = request.form["image"] - action = request.form["action"] - - group = db.get_or_404(Group, group_id) - db.get_or_404(Post, image_id) # Check if image exists - - if group.author_id != current_user.id: - abort(403) - - if ( - action == "add" - and not GroupJunction.query.filter_by( - group_id=group_id, post_id=image_id - ).first() - ): - db.session.add(GroupJunction(group_id=group_id, post_id=image_id)) - elif request.form["action"] == "remove": - GroupJunction.query.filter_by(group_id=group_id, post_id=image_id).delete() - - db.session.commit() - return ":3" - - -@blueprint.route("/group/delete", methods=["POST"]) -def delete_group(): - """ - Deletes a group - """ - group_id = request.form["group"] - group = db.get_or_404(Group, group_id) - - if group.author_id != current_user.id: - abort(403) - - GroupJunction.query.filter_by(group_id=group_id).delete() - db.session.delete(group) - db.session.commit() - - flash(["Group yeeted!", "1"]) - return ":3" - - -@blueprint.route("/user/picture/", methods=["POST"]) -def user_picture(user_id): - """ - Returns the profile of a user - """ - user = db.get_or_404(User, user_id) - file = request.files["file"] - - # If no image is uploaded, return 404 error - if not file: - return abort(404) - elif user.id != current_user.id: - return abort(403) - - # Get file extension, generate random name and set file path - img_ext = pathlib.Path(file.filename).suffix.replace(".", "").lower() - img_name = str(user.id) - img_path = os.path.join(current_app.config["PFP_FOLDER"], img_name + "." + img_ext) - - # Check if file extension is allowed - if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys(): - logging.info("File extension not allowed: %s", img_ext) - abort(403) - - if user.picture: - os.remove(os.path.join(current_app.config["PFP_FOLDER"], user.picture)) - # Delete cached files - cache_name = user.picture.rsplit(".")[0] - for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(cache_name + "*"): - os.remove(cache_file) - - # Save file - try: - file.save(img_path) - except OSError as err: - logging.info("Error saving file %s because of %s", img_path, err) - abort(500) - - img_colors = ColorThief(img_path).get_color() # Get color palette - - # Save to database - user.colour = img_colors - user.picture = str(img_name + "." + img_ext) - db.session.commit() - - return "Gwa Gwa" # Return something so the browser doesn't show an error - -@blueprint.route("/user/username/", methods=["POST"]) -def user_username(user_id): - """ - Returns the profile of a user - """ - user = db.get_or_404(User, user_id) - new_name = request.form["name"] - - username_regex = re.compile(r"\b[A-Za-z0-9._-]+\b") - - # Validate the form - if not new_name or not username_regex.match(new_name): - abort(400) - elif user.id != current_user.id: - return abort(403) - - # Save to database - user.username = new_name - db.session.commit() - - return "Gwa Gwa" # Return something so the browser doesn't show an error \ No newline at end of file diff --git a/onlylegs/api/__init__.py b/onlylegs/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/onlylegs/api/account.py b/onlylegs/api/account.py new file mode 100644 index 0000000..03754b2 --- /dev/null +++ b/onlylegs/api/account.py @@ -0,0 +1,92 @@ +""" +Onlylegs - API endpoints +""" +from uuid import uuid4 +import os +import pathlib +import re +import logging + +from flask import Blueprint, jsonify, request, current_app +from flask_login import login_required, current_user + +from colorthief import ColorThief + +from onlylegs.extensions import db +from onlylegs.models import User + + +blueprint = Blueprint("account_api", __name__, url_prefix="/api/account") + + +@blueprint.route("/picture/", methods=["POST"]) +@login_required +def account_picture(user_id): + """ + Returns the profile of a user + """ + user = db.get_or_404(User, user_id) + file = request.files["file"] + + # If no image is uploaded, return 404 error + if not file: + return jsonify({"error": "No file uploaded"}), 400 + elif user.id != current_user.id: + return jsonify({"error": "You are not allowed to do this, go away"}), 403 + + # Get file extension, generate random name and set file path + img_ext = pathlib.Path(file.filename).suffix.replace(".", "").lower() + img_name = str(user.id) + img_path = os.path.join(current_app.config["PFP_FOLDER"], img_name + "." + img_ext) + + # Check if file extension is allowed + if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys(): + logging.info("File extension not allowed: %s", img_ext) + return jsonify({"error": "File extension not allowed"}), 403 + + if user.picture: + # Delete cached files and old image + os.remove(os.path.join(current_app.config["PFP_FOLDER"], user.picture)) + cache_name = user.picture.rsplit(".")[0] + for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(cache_name + "*"): + os.remove(cache_file) + + # Save file + try: + file.save(img_path) + except OSError as err: + logging.info("Error saving file %s because of %s", img_path, err) + return jsonify({"error": "Error saving file"}), 500 + + img_colors = ColorThief(img_path).get_color() + + # Save to database + user.colour = img_colors + user.picture = str(img_name + "." + img_ext) + db.session.commit() + + return jsonify({"message": "File uploaded"}), 200 + + +@blueprint.route("/username/", methods=["POST"]) +@login_required +def account_username(user_id): + """ + Returns the profile of a user + """ + user = db.get_or_404(User, user_id) + new_name = request.form["name"] + + username_regex = re.compile(r"\b[A-Za-z0-9._-]+\b") + + # Validate the form + if not new_name or not username_regex.match(new_name): + return jsonify({"error": "Username is invalid"}), 400 + elif user.id != current_user.id: + return jsonify({"error": "You are not allowed to do this, go away"}), 403 + + # Save to database + user.username = new_name + db.session.commit() + + return jsonify({"message": "Username changed"}), 200 diff --git a/onlylegs/api/group.py b/onlylegs/api/group.py new file mode 100644 index 0000000..24ee23e --- /dev/null +++ b/onlylegs/api/group.py @@ -0,0 +1,73 @@ +""" +Onlylegs - API endpoints +""" +from flask import Blueprint, flash, jsonify, request +from flask_login import login_required, current_user + +from onlylegs.extensions import db +from onlylegs.models import Post, Group, GroupJunction + + +blueprint = Blueprint("group_api", __name__, url_prefix="/api/group") + + +@blueprint.route("/create", methods=["POST"]) +@login_required +def create_group(): + """ + Creates a group + """ + new_group = Group( + name=request.form["name"], + description=request.form["description"], + author_id=current_user.id, + ) + + db.session.add(new_group) + db.session.commit() + + return jsonify({"message": "Group created", "id": new_group.id}) + + +@blueprint.route("/modify", methods=["POST"]) +@login_required +def modify_group(): + """ + Changes the images in a group + """ + group_id = request.form["group"] + image_id = request.form["image"] + action = request.form["action"] + + group = db.get_or_404(Group, group_id) + db.get_or_404(Post, image_id) # Check if image exists + + if group.author_id != current_user.id: + return jsonify({"message": "You are not the owner of this group"}), 403 + + if (action == "add" and not GroupJunction.query.filter_by(group_id=group_id, post_id=image_id).first()): + db.session.add(GroupJunction(group_id=group_id, post_id=image_id)) + elif request.form["action"] == "remove": + GroupJunction.query.filter_by(group_id=group_id, post_id=image_id).delete() + + db.session.commit() + return jsonify({"message": "Group modified"}) + + +@blueprint.route("/delete", methods=["POST"]) +def delete_group(): + """ + Deletes a group + """ + group_id = request.form["group"] + group = db.get_or_404(Group, group_id) + + if group.author_id != current_user.id: + return jsonify({"message": "You are not the owner of this group"}), 403 + + GroupJunction.query.filter_by(group_id=group_id).delete() + db.session.delete(group) + db.session.commit() + + flash(["Group yeeted!", "1"]) + return jsonify({"message": "Group deleted"}) diff --git a/onlylegs/api/media.py b/onlylegs/api/media.py new file mode 100644 index 0000000..a933caa --- /dev/null +++ b/onlylegs/api/media.py @@ -0,0 +1,128 @@ +""" +Onlylegs - API endpoints +Media upload and retrieval +""" +import os +from uuid import uuid4 +import os +import pathlib +import logging + +from flask import Blueprint, flash, abort, send_from_directory, jsonify, request, current_app +from flask_login import login_required, current_user + +from colorthief import ColorThief + +from onlylegs.extensions import db +from onlylegs.models import Post, GroupJunction +from onlylegs.utils import metadata as mt +from onlylegs.utils.generate_image import generate_thumbnail + + +blueprint = Blueprint("media_api", __name__, url_prefix="/api/media") + + +@blueprint.route("/", methods=["GET"]) +def media(path): + """ + Returns a file from the uploads folder + r for resolution, thumb for thumbnail etc + e for extension, jpg, png etc + """ + res = request.args.get("r", default=None, type=str) + ext = request.args.get("e", default=None, type=str) + # path = secure_filename(path) + + # if no args are passed, return the raw file + if not res and not ext: + if not os.path.exists(os.path.join(current_app.config["MEDIA_FOLDER"], path)): + abort(404) + return send_from_directory(current_app.config["MEDIA_FOLDER"], path) + + thumb = generate_thumbnail(path, res, ext) + if not thumb: + abort(500) + + return send_from_directory(os.path.dirname(thumb), os.path.basename(thumb)) + + +@blueprint.route("/upload", methods=["POST"]) +@login_required +def upload(): + """ + Uploads an image to the server and saves it to the database + """ + form_file = request.files["file"] + form = request.form + + if not form_file: + return jsonify({"message": "No file"}), 400 + + # Get file extension, generate random name and set file path + img_ext = pathlib.Path(form_file.filename).suffix.replace(".", "").lower() + img_name = "GWAGWA_" + str(uuid4()) + img_path = os.path.join(current_app.config["UPLOAD_FOLDER"], img_name + "." + img_ext) + + # Check if file extension is allowed + if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys(): + logging.info("File extension not allowed: %s", img_ext) + return jsonify({"message": "File extension not allowed"}), 403 + + # Save file + try: + form_file.save(img_path) + except OSError as err: + logging.info("Error saving file %s because of %s", img_path, err) + return jsonify({"message": "Error saving file"}), 500 + + img_exif = mt.Metadata(img_path).yoink() # Get EXIF data + img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette + + # Save to database + query = Post( + author_id=current_user.id, + filename=img_name + "." + img_ext, + mimetype=img_ext, + exif=img_exif, + colours=img_colors, + description=form["description"], + alt=form["alt"], + ) + + db.session.add(query) + db.session.commit() + + return jsonify({"message": "File uploaded"}), 200 + + +@blueprint.route("/delete/", methods=["POST"]) +@login_required +def delete_image(image_id): + """ + Deletes an image from the server and database + """ + post = db.get_or_404(Post, image_id) + + # Check if image exists and if user is allowed to delete it (author) + if post.author_id != current_user.id: + logging.info("User %s tried to delete image %s", current_user.id, image_id) + return jsonify({"message": "You are not allowed to delete this image, heck off"}), 403 + + # Delete file + try: + os.remove(os.path.join(current_app.config["UPLOAD_FOLDER"], post.filename)) + except FileNotFoundError: + logging.warning("File not found: %s, already deleted or never existed", post.filename) + + # Delete cached files + cache_name = post.filename.rsplit(".")[0] + for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(cache_name + "*"): + os.remove(cache_file) + + GroupJunction.query.filter_by(post_id=image_id).delete() + db.session.delete(post) + db.session.commit() + + logging.info("Removed image (%s) %s", image_id, post.filename) + flash(["Image was all in Le Head!", "1"]) + return jsonify({"message": "Image deleted"}), 200 diff --git a/onlylegs/templates/group.html b/onlylegs/templates/group.html index 6ca0c2f..bebe8ec 100644 --- a/onlylegs/templates/group.html +++ b/onlylegs/templates/group.html @@ -51,7 +51,7 @@ const formData = new FormData(); formData.append("group", formID); - fetch('{{ url_for('api.delete_group') }}', { + fetch('{{ url_for('group_api.delete_group') }}', { method: 'POST', body: formData }).then(response => { @@ -147,7 +147,7 @@ formData.append("image", formImage); formData.append("action", formAction); - fetch('{{ url_for('api.modify_group') }}', { + fetch('{{ url_for('group_api.modify_group') }}', { method: 'POST', body: formData }).then(response => { @@ -221,7 +221,7 @@ {% block content %} {% if images %}