Skip to content

Commit

Permalink
Add thumbnail routes basic implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Boomaa23 committed Sep 5, 2024
1 parent e01cd15 commit ca16256
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,7 @@ venv/
.env
.DS_Store
**/.DS_Store
server/**/*.db-journal
server/docs/build
server/thumbnails/*
!server/thumbnails/.gitkeep
1 change: 1 addition & 0 deletions server/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
doc8
flask==3.0.3
flask-cors==5.0.0
pillow~=10.4.0
pre-commit~=3.5.0
pytest==8.3.2
python-dotenv==1.0.0
Expand Down
97 changes: 97 additions & 0 deletions server/src/api_thumbnail_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import glob
import os
from sqlite3.dbapi2 import Cursor

import auth
import common
import flask
import models
import werkzeug.utils
from PIL import Image


THUMBNAIL_FOLDER = os.path.abspath('../thumbnails')
ALLOWED_EXTENSIONS = {'jpg'}
# ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # 10MB

api_thumbnail_blueprint = flask.Blueprint('api_thumbnail', __name__)


@api_thumbnail_blueprint.route('/api/thumbnail/get', methods=['GET', 'POST'])
def api_thumbnail_get():
# TODO documentation
form = common.FlaskPOSTForm(flask.request.form if flask.request.method == 'POST' else flask.request.args)
item_id = form.get(models.Item.id_name)
thumbnail_size = form.get('size', int)
thumbnail_name = f'{item_id}_{thumbnail_size}.jpg'
thumbnail_path = os.path.join(THUMBNAIL_FOLDER, thumbnail_name)

if not os.path.exists(thumbnail_path):
thumbnail_source_path = os.path.join(THUMBNAIL_FOLDER, f'{item_id}.jpg')
image = Image.open(thumbnail_source_path)
image.thumbnail((thumbnail_size, thumbnail_size))
image.save(thumbnail_path)
return image

return flask.send_file(thumbnail_path, mimetype='image/jpg')


@api_thumbnail_blueprint.route('/api/thumbnail/upload', methods=['POST'])
@auth.route_requires_auth(auth.Scope.THUMBNAIL_UPLOAD)
def api_thumbnail_upload():
# TODO documentation
form = common.FlaskPOSTForm(flask.request.form)
conn, cursor = common.get_db_connection()

if 'image' not in flask.request.files:
flask.abort(400, 'No image sent')

image = flask.request.files['image']
# If the user does not select a file, the browser submits an empty file without a filename.
if image.filename == '':
flask.abort(400, 'No image data sent')
if not image:
flask.abort(400, 'No image was specified')

input_filename = werkzeug.utils.secure_filename(image.filename)
input_filename_ext = input_filename.rsplit('.', 1)[1].lower() if ('.' in input_filename) else None
if input_filename_ext not in ALLOWED_EXTENSIONS:
flask.abort(415, f'Unsupported file type \'{input_filename_ext}\'. Expected one of {ALLOWED_EXTENSIONS}')

# Do not overwrite existing files
item_id = form.get(models.Item.id_name)
output_filepath = os.path.join(THUMBNAIL_FOLDER, f'{item_id}.jpg')
if os.path.exists(output_filepath):
flask.abort(400, f'A thumbnail already exists for {models.Item.id_name} {item_id}. Delete this first.')

_verify_item_exists(cursor, item_id)

image.save(output_filepath)
return common.create_response(200, [])


@api_thumbnail_blueprint.route('/api/thumbnail/delete', methods=['POST'])
@auth.route_requires_auth(auth.Scope.THUMBNAIL_DELETE)
def api_thumbnail_delete():
# TODO documentation
form = common.FlaskPOSTForm(flask.request.form)
conn, cursor = common.get_db_connection()

item_id = form.get(models.Item.id_name)
_verify_item_exists(cursor, item_id)

thumbnail_paths = glob.glob(os.path.join(THUMBNAIL_FOLDER, f'{item_id}_*.jpg'))
thumbnail_paths.append(os.path.join(THUMBNAIL_FOLDER, f'{item_id}.jpg'))

for path in thumbnail_paths:
os.remove(path)

return common.create_response(200, [])


def _verify_item_exists(cursor: Cursor, item_id: str) -> None:
item_res = cursor.execute(f'SELECT * FROM {models.Item.table_name} WHERE {models.Item.id_name}=?', (item_id,))
db_item = item_res.fetchone()
if db_item is None or len(db_item) == 0:
flask.abort(404, f'{models.Item.__name__} does not exist')
3 changes: 3 additions & 0 deletions server/src/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ class Scope(enum.IntFlag):
BOX_UPDATE = enum.auto()
BOX_DELETE = enum.auto()
BOXES_LIST = enum.auto()
THUMBNAIL_GET = enum.auto()
THUMBNAIL_UPLOAD = enum.auto()
THUMBNAIL_DELETE = enum.auto()


def route_requires_auth(scope):
Expand Down
2 changes: 1 addition & 1 deletion server/src/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def create_response(code: int, body: Union[List[dict], dict]) -> Dict[str, Any]:
{
'code': <number>,
'body': [
<one or more of models.Model>
<zero or more of models.Model>
...
]
}
Expand Down
2 changes: 1 addition & 1 deletion server/src/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def to_dict(self) -> Dict[str, Any]:

def to_response(self) -> Dict[str, Any]:
"""Convert this model to a JSON API response."""
return common.create_response(200, self.to_dict())
return common.create_response(200, [self.to_dict()])

def to_insert_str(self) -> str:
"""Convert this model to a string which can be INSERTed into an SQL database."""
Expand Down
2 changes: 2 additions & 0 deletions server/src/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from api_box_routes import api_box_blueprint
from api_item_routes import api_item_blueprint
from api_reservation_routes import api_reservation_blueprint
from api_thumbnail_routes import api_thumbnail_blueprint
from api_user_routes import api_user_blueprint
from flask_cors import CORS
from werkzeug.exceptions import HTTPException
Expand Down Expand Up @@ -39,6 +40,7 @@ def _create_tables():
app.register_blueprint(api_user_blueprint)
app.register_blueprint(api_reservation_blueprint)
app.register_blueprint(api_box_blueprint)
app.register_blueprint(api_thumbnail_blueprint)


@app.teardown_appcontext
Expand Down
Empty file added server/thumbnails/.gitkeep
Empty file.

0 comments on commit ca16256

Please sign in to comment.