diff --git a/flaschengeist/controller/imageController.py b/flaschengeist/controller/imageController.py new file mode 100644 index 0000000..5139d56 --- /dev/null +++ b/flaschengeist/controller/imageController.py @@ -0,0 +1,65 @@ +from datetime import date +from flask import send_file +from pathlib import Path +from PIL import Image as PImage + +from werkzeug.exceptions import NotFound, UnprocessableEntity +from werkzeug.datastructures import FileStorage +from werkzeug.utils import secure_filename + +from flaschengeist.models.image import Image +from flaschengeist.database import db +from flaschengeist.config import config + + +def check_mimetype(mime: str): + return mime in config["FILES"].get('allowed_mimetypes', []) + + +def send_image(id: int = None, image: Image = None): + if image is None: + image = Image.query.get(id) + if not image: + raise NotFound + return send_file(image.path_, mimetype=image.mimetype_, download_name=image.filename_) + + +def send_thumbnail(id: int = None, image: Image = None): + if image is None: + image = Image.query.get(id) + if not image: + raise NotFound + if not image.thumbnail_: + with PImage.open(image.open()) as im: + im.thumbnail(tuple(config["FILES"].get("thumbnail_size"))) + s = image.path_.split('.') + s.insert(len(s)-1, 'thumbnail') + im.save('.'.join(s)) + image.thumbnail_ = '.'.join(s) + db.session.commit() + return send_file(image.thumbnail_, mimetype=image.mimetype_, download_name=image.filename_) + + +def upload_image(file: FileStorage): + if not check_mimetype(file.mimetype): + raise UnprocessableEntity + + path = Path(config["FILES"].get("data_path")) / str(date.today().year) + path.mkdir(mode=int('0700', 8), parents=True, exist_ok=True) + + if file.filename.count('.') < 1: + name = secure_filename(file.filename + '.' + file.mimetype.split('/')[-1]) + else: + name = secure_filename(file.filename) + img = Image(mimetype_=file.mimetype, filename_=name) + db.session.add(img) + db.session.flush() + try: + img.path_ = str((path / f"{img.id}.{img.filename_.split('.')[-1]}").resolve()) + file.save(img.path_) + except: + db.session.delete(img) + raise + finally: + db.session.commit() + return img diff --git a/flaschengeist/flaschengeist.toml b/flaschengeist/flaschengeist.toml index a920eb6..0898e2c 100644 --- a/flaschengeist/flaschengeist.toml +++ b/flaschengeist/flaschengeist.toml @@ -7,7 +7,7 @@ auth = "auth_plain" # Enable if you run flaschengeist behind a proxy, e.g. nginx + gunicorn #proxy = false # Set root path, prefixes all routes -#root = /api +root = "/api" # Set secret key secret_key = "V3ryS3cr3t" # Domain used by frontend @@ -21,10 +21,19 @@ level = "WARNING" [DATABASE] # engine = "mysql" (default) -# user = "user" -# host = "127.0.0.1" -# password = "password" -# database = "database" + +[FILES] +# Path for file / image uploads +data_path = "./data" +# Thumbnail size +thumbnail_size = [192, 192] +# Accepted mimetypes +allowed_mimetypes = [ + "image/avif", + "image/jpeg", + "image/png", + "image/webp" +] [auth_plain] enabled = true diff --git a/flaschengeist/models/image.py b/flaschengeist/models/image.py new file mode 100644 index 0000000..1242c2e --- /dev/null +++ b/flaschengeist/models/image.py @@ -0,0 +1,30 @@ +from __future__ import annotations # TODO: Remove if python requirement is >= 3.10 + +from sqlalchemy import event +from pathlib import Path + +from . import ModelSerializeMixin, Serial +from ..database import db + +class Image(db.Model, ModelSerializeMixin): + __tablename__ = "image" + id: int = db.Column("id", Serial, primary_key=True) + filename_: str = db.Column(db.String(30), nullable=False) + mimetype_: str = db.Column(db.String(30), nullable=False) + thumbnail_: str = db.Column(db.String(127)) + path_: str = db.Column(db.String(127)) + + def open(self): + return open(self.path_, "rb") + + +@event.listens_for(Image, 'before_delete') +def clear_file(mapper, connection, target: Image): + if target.path_: + p = Path(target.path_) + if p.exists(): + p.unlink() + if target.thumbnail_: + p = Path(target.thumbnail_) + if p.exists(): + p.unlink() diff --git a/setup.py b/setup.py index 7860856..adef712 100644 --- a/setup.py +++ b/setup.py @@ -38,13 +38,13 @@ setup( "sqlalchemy>=1.4", "flask_sqlalchemy>=2.5", "flask_cors", + "Pillow>=8.4.0", "werkzeug", mysql_driver, ], extras_require={ "ldap": ["flask_ldapconn", "ldap3"], "argon": ["argon2-cffi"], - "pricelist": ["pillow"], "test": ["pytest", "coverage"], }, entry_points={ @@ -59,7 +59,7 @@ setup( "balance = flaschengeist.plugins.balance:BalancePlugin", "events = flaschengeist.plugins.events:EventPlugin", "mail = flaschengeist.plugins.message_mail:MailMessagePlugin", - "pricelist = flaschengeist.plugins.pricelist:PriceListPlugin [pricelist]", + "pricelist = flaschengeist.plugins.pricelist:PriceListPlugin", ], }, cmdclass={