109 lines
4.2 KiB
Python
109 lines
4.2 KiB
Python
import os
|
|
|
|
from PIL import Image as PILImage, UnidentifiedImageError
|
|
from flask import Blueprint, request, jsonify, send_file
|
|
from tinydb import Query
|
|
|
|
from api.utils import get_current_user_id, sanitize_email
|
|
from config.paths import get_user_image_dir
|
|
|
|
from db.db import image_db
|
|
from models.image import Image
|
|
|
|
image_api = Blueprint('image_api', __name__)
|
|
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png'}
|
|
IMAGE_TYPE_PROFILE = 1
|
|
IMAGE_TYPE_ICON = 2
|
|
MAX_DIMENSION = 512
|
|
|
|
def allowed_file(filename):
|
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
|
|
|
@image_api.route('/image/upload', methods=['POST'])
|
|
def upload():
|
|
user_id = get_current_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'User not authenticated'}), 401
|
|
if 'file' not in request.files:
|
|
return jsonify({'error': 'No file part in the request'}), 400
|
|
file = request.files['file']
|
|
if file.filename == '':
|
|
return jsonify({'error': 'No selected file'}), 400
|
|
|
|
image_type = request.form.get('type', None)
|
|
if not image_type:
|
|
return jsonify({'error': 'Image type is required'}), 400
|
|
try:
|
|
image_type = int(image_type)
|
|
if image_type not in [IMAGE_TYPE_PROFILE, IMAGE_TYPE_ICON]:
|
|
return jsonify({'error': 'Invalid image type. Must be 1 or 2'}), 400
|
|
except ValueError:
|
|
return jsonify({'error': 'Image type must be an integer'}), 400
|
|
|
|
perm = request.form.get('permanent', "false").lower() == 'true'
|
|
|
|
if not file or not allowed_file(file.filename):
|
|
return jsonify({'error': 'Invalid file type or no file selected'}), 400
|
|
|
|
try:
|
|
pil_image = PILImage.open(file.stream)
|
|
original_format = pil_image.format # store before convert
|
|
pil_image.verify() # quick integrity check
|
|
file.stream.seek(0)
|
|
pil_image = PILImage.open(file.stream).convert('RGBA' if pil_image.mode in ('RGBA', 'LA') else 'RGB')
|
|
except UnidentifiedImageError:
|
|
return jsonify({'error': 'Uploaded file is not a valid image'}), 400
|
|
except Exception:
|
|
return jsonify({'error': 'Failed to process image'}), 400
|
|
|
|
if original_format not in ('JPEG', 'PNG'):
|
|
return jsonify({'error': 'Only JPEG and PNG images are allowed'}), 400
|
|
|
|
# Resize preserving aspect ratio (in-place thumbnail)
|
|
pil_image.thumbnail((MAX_DIMENSION, MAX_DIMENSION), PILImage.Resampling.LANCZOS)
|
|
|
|
format_extension_map = {'JPEG': '.jpg', 'PNG': '.png'}
|
|
extension = format_extension_map.get(original_format, '.png')
|
|
image_record = Image(extension=extension, permanent=perm, type=image_type, user=user_id)
|
|
filename = image_record.id + extension
|
|
filepath = os.path.abspath(os.path.join(get_user_image_dir(sanitize_email(user_id)), filename))
|
|
|
|
try:
|
|
# Save with appropriate format
|
|
save_params = {}
|
|
if pil_image.format == 'JPEG':
|
|
save_params['quality'] = 90
|
|
save_params['optimize'] = True
|
|
pil_image.save(filepath, format=pil_image.format, **save_params)
|
|
except Exception:
|
|
return jsonify({'error': 'Failed to save processed image'}), 500
|
|
|
|
image_db.insert(image_record.to_dict())
|
|
|
|
return jsonify({'message': 'Image uploaded successfully', 'filename': filename, 'id': image_record.id}), 200
|
|
|
|
@image_api.route('/image/request/<id>', methods=['GET'])
|
|
def request_image(id):
|
|
ImageQuery = Query()
|
|
image: Image = Image.from_dict(image_db.get(ImageQuery.id == id))
|
|
if not image:
|
|
return jsonify({'error': 'Image not found'}), 404
|
|
filename = f"{image.id}{image.extension}"
|
|
filepath = os.path.abspath(os.path.join(get_user_image_dir(image.user), filename))
|
|
if not os.path.exists(filepath):
|
|
return jsonify({'error': 'File not found'}), 404
|
|
return send_file(filepath)
|
|
|
|
@image_api.route('/image/list', methods=['GET'])
|
|
def list_images():
|
|
image_type = request.args.get('type', type=int)
|
|
ImageQuery = Query()
|
|
if image_type is not None:
|
|
if image_type not in [IMAGE_TYPE_PROFILE, IMAGE_TYPE_ICON]:
|
|
return jsonify({'error': 'Invalid image type'}), 400
|
|
images = image_db.search(ImageQuery.type == image_type)
|
|
else:
|
|
images = image_db.all()
|
|
image_ids = [img['id'] for img in images]
|
|
return jsonify({'ids': image_ids, 'count': len(image_ids)}), 200
|