[controller] Add controller for handling uploading images
This commit is contained in:
		
							parent
							
								
									42e304cf5f
								
							
						
					
					
						commit
						a6fe921920
					
				| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
							
								
								
									
										4
									
								
								setup.py
								
								
								
								
							
							
						
						
									
										4
									
								
								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={
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue