python-gallery/gallery/routes/api.py
Michał f9aeb0955d SVG favicons
Image and colour preview for links
2023-03-15 16:19:42 +00:00

319 lines
10 KiB
Python

"""
Onlylegs - API endpoints
Used internally by the frontend and possibly by other applications
"""
from uuid import uuid4
import os
import pathlib
import io
import logging
from datetime import datetime as dt
from flask import (Blueprint, send_from_directory, send_file,
abort, flash, jsonify, request, g, current_app)
from werkzeug.utils import secure_filename
from colorthief import ColorThief
from PIL import Image, ImageOps, ImageFilter
from sqlalchemy.orm import sessionmaker
from gallery.auth import login_required
from gallery import db
from gallery.utils import metadata as mt
blueprint = Blueprint('api', __name__, url_prefix='/api')
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route('/file/<file_name>', methods=['GET'])
def get_file(file_name):
"""
Returns a file from the uploads folder
t is the type of file (thumb, etc.)
w and h are the width and height of the image for resizing
f is whether to apply filters to the image, such as blurring NSFW images
b is whether to force blur the image, even if it's not NSFW
"""
# Get args
type = request.args.get('t', default=None, type=str) # Type of file (thumb, etc)
width = request.args.get('w', default=0, type=int) # Width of image
height = request.args.get('h', default=0, type=int) # Height of image
filtered = request.args.get('f', default=False, type=bool) # Whether to apply filters
blur = request.args.get('b', default=False, type=bool) # Whether to force blur
# Idea: instead if specifying the height and width, pass in a string like "200x200" or "200x" or "x200"
# This would remove the need for the if statements below and would be possible to pass in a string
# like 'thumb' to get the thumbnail size instead of having to specify the width and height
# This would also allow for more flexibility in the future if I wanted to add more sizes
# Another idea is to pass in a list of filters to apply to the image
# such as "blur,grayscale" or "blur,grayscale,sepia". But this would require a lot more work to implement
# and would be a lot more complicated to use, would also implement the risk of the server being overloaded
# with requests to apply a lot of filters to a lot of images at once
file_name = secure_filename(file_name) # Sanitize file name
# If type is thumb(nail), return from database instead of file system
# as it's faster than generating a new thumbnail on every request
if type == 'thumb':
thumb = db_session.query(db.Thumbnails).filter_by(file_name=file_name).first()
# If no thumbnail exists, return 404
if not thumb:
abort(404)
return send_file(thumb.data, mimetype='image/' + thumb.file_ext)
# if no args are passed, return the raw file
if not request.args:
if not os.path.exists(os.path.join(current_app.config['UPLOAD_FOLDER'], file_name)):
abort(404)
return send_from_directory(current_app.config['UPLOAD_FOLDER'], file_name)
# If only width is passed, set height to width
if width and not height:
height = width
# If only height is passed, set width to height
elif not width and height:
width = height
# If neither are passed, return 400 as one is required for resizing
elif not width and not height:
abort(400)
buff = io.BytesIO() # Image Buffer
# Open image and set extension
try:
img = Image.open(os.path.join(current_app.config['UPLOAD_FOLDER'], file_name))
# FileNotFound is raised if the file doesn't exist
except FileNotFoundError:
logging.error('File not found: %s', file_name)
abort(404)
# OSError is raised if the file is broken or corrupted
except OSError as err:
logging.error('Possibly broken image %s, error: %s', file_name, err)
abort(500)
img_ext = pathlib.Path(file_name).suffix.replace('.', '').lower() # Get file extension
img_ext = current_app.config['ALLOWED_EXTENSIONS'][img_ext] # Convert to MIME type
img_icc = img.info.get("icc_profile") # Get ICC profile
img.thumbnail((width, height), Image.LANCZOS) # Resize image
img = ImageOps.exif_transpose(img) # Rotate image based on EXIF data
# If has NSFW tag, blur image, etc.
if filtered:
pass
# If forced to blur, blur image
if blur:
img = img.filter(ImageFilter.GaussianBlur(20))
try:
img.save(buff, img_ext, icc_profile=img_icc)
except OSError:
# This usually happens when saving a JPEG with an ICC profile,
# so we convert to RGB and try again
img = img.convert('RGB')
img.save(buff, img_ext, icc_profile=img_icc)
except Exception as err:
logging.error('Could not resize image %s, error: %s', file_name, err)
abort(500)
img.close() # Close image to free memory, learned the hard way
buff.seek(0) # Reset buffer to start
return send_file(buff, mimetype='image/' + img_ext)
@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 Exception as err:
logging.error('Could not save file: %s', 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
try:
query = db.Posts(author_id=g.user.id,
created_at=dt.utcnow(),
file_name=img_name+'.'+img_ext,
file_type=img_ext,
image_exif=img_exif,
image_colours=img_colors,
post_description=form['description'],
post_alt=form['alt'])
db_session.add(query)
db_session.commit()
except Exception as err:
logging.error('Could not save to database: %s', err)
abort(500)
return 'Gwa Gwa' # Return something so the browser doesn't show an error
@blueprint.route('/delete/<int:image_id>', methods=['POST'])
@login_required
def delete_image(image_id):
"""
Deletes an image from the server and database
"""
img = db_session.query(db.Posts).filter_by(id=image_id).first()
if img is None:
abort(404)
if img.author_id != g.user.id:
abort(403)
try:
os.remove(os.path.join(current_app.config['UPLOAD_FOLDER'],img.file_name))
except FileNotFoundError:
# File was already deleted or doesn't exist
logging.warning('File not found: %s, already deleted or never existed', img.file_name)
except Exception as err:
logging.error('Could not remove file: %s', err)
abort(500)
try:
db_session.query(db.Posts).filter_by(id=image_id).delete()
groups = db_session.query(db.GroupJunction).filter_by(post_id=image_id).all()
for group in groups:
db_session.delete(group)
db_session.commit()
except Exception as err:
logging.error('Could not remove from database: %s', err)
abort(500)
logging.info('Removed image (%s) %s', image_id, img.file_name)
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 = db.Groups(name=request.form['name'],
description=request.form['description'],
author_id=g.user.id,
created_at=dt.utcnow())
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']
group = db_session.query(db.Groups).filter_by(id=group_id).first()
if group is None:
abort(404)
elif group.author_id != g.user.id:
abort(403)
if request.form['action'] == 'add':
if db_session.query(db.GroupJunction).filter_by(group_id=group_id, post_id=image_id).first() is None:
db_session.add(db.GroupJunction(group_id=group_id, post_id=image_id, date_added=dt.utcnow()))
elif request.form['action'] == 'remove':
db_session.query(db.GroupJunction).filter_by(group_id=group_id, post_id=image_id).delete()
db_session.commit()
return ':3'
@blueprint.route('/metadata/<int:img_id>', methods=['GET'])
def metadata(img_id):
"""
Yoinks metadata from an image
"""
img = db_session.query(db.Posts).filter_by(id=img_id).first()
if img is None:
abort(404)
img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], img.file_name)
exif = mt.Metadata(img_path).yoink()
return jsonify(exif)
@blueprint.route('/logfile')
@login_required
def logfile():
"""
Gets the log file and returns it as a JSON object
"""
log_dict = {}
with open('only.log', encoding='utf-8') as file:
for i, line in enumerate(file):
line = line.split(' : ')
event = line[0].strip().split(' ')
event_data = {
'date': event[0],
'time': event[1],
'severity': event[2],
'owner': event[3]
}
message = line[1].strip()
try:
message_data = {
'code': int(message[1:4]),
'message': message[5:].strip()
}
except ValueError:
message_data = {'code': 0, 'message': message}
except Exception as err:
logging.error('Could not parse log file: %s', err)
abort(500)
log_dict[i] = {'event': event_data, 'message': message_data}
return jsonify(log_dict)