From 8260cf5ae35f0c9a06c41c93a5a5a50d28053392 Mon Sep 17 00:00:00 2001 From: lelo Date: Mon, 21 Apr 2025 13:09:16 +0200 Subject: [PATCH] add thumbnails in backend --- app.py | 147 ++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 94 insertions(+), 53 deletions(-) diff --git a/app.py b/app.py index 17304b2..6a26442 100755 --- a/app.py +++ b/app.py @@ -1,6 +1,6 @@ from flask import Flask, render_template, send_file, url_for, jsonify, request, session, send_from_directory, abort import os -from PIL import Image +from PIL import Image, ImageOps import io from functools import wraps import mimetypes @@ -249,10 +249,9 @@ def serve_file(subpath): # 2) Prep request info mime, _ = mimetypes.guess_type(full_path) mime = mime or 'application/octet-stream' - is_cache_request = request.headers.get('X-Cache-Request') == 'true' - ip_address = request.remote_addr - user_agent = request.headers.get('User-Agent') + ip_address = request.remote_addr + user_agent = request.headers.get('User-Agent') # skip logging on cache hits or on audio GETs (per your rules) do_log = not is_cache_request @@ -260,77 +259,119 @@ def serve_file(subpath): do_log = False # 3) Pick cache - if mime.startswith('audio/'): cache = cache_audio - elif mime.startswith('image/'): cache = cache_image - elif mime.startswith('video/'): cache = cache_video - else: cache = cache_other + if mime.startswith('audio/'): + cache = cache_audio + elif mime.startswith('image/'): + cache = cache_image + elif mime.startswith('video/'): + cache = cache_video + else: + cache = cache_other - # 4) Ensure cached on‑disk file and get its path + # 4) Image and thumbnail handling first + if mime.startswith('image/'): + small = request.args.get('thumbnail') == 'true' + name, ext = os.path.splitext(subpath) + orig_key = subpath + small_key = f"{name}_small{ext}" + cache_key = small_key if small else orig_key + + try: + # Try to read the requested variant + with cache.read(cache_key) as reader: + file_path = reader.name + except KeyError: + # On miss: generate both full-size and small thumb, then cache + with Image.open(full_path) as orig: + exif_bytes = orig.info.get('exif') + img = ImageOps.exif_transpose(orig) + variants = { + orig_key: (1920, 1920), + small_key: ( 192, 192), + } + for key, size in variants.items(): + thumb = img.copy() + thumb.thumbnail(size, Image.LANCZOS) + if thumb.mode in ("RGBA", "P"): + thumb = thumb.convert("RGB") + bio = io.BytesIO() + save_kwargs = {'format': 'JPEG', 'quality': 85} + if exif_bytes: + save_kwargs['exif'] = exif_bytes + thumb.save(bio, **save_kwargs) + bio.seek(0) + cache.set(key, bio, read=True) + # Read back the variant we need + with cache.read(cache_key) as reader: + file_path = reader.name + + # Serve the image variant + response = send_file( + file_path, + mimetype=mime, + conditional=True, + as_attachment=(request.args.get('download') == 'true'), + download_name=os.path.basename(orig_key) + ) + response.headers['Content-Disposition'] = 'inline' + response.headers['Cache-Control'] = 'public, max-age=86400' + + if do_log: + a.log_file_access( + cache_key, + os.path.getsize(file_path), + mime, + ip_address, + user_agent, + session['device_id'], + True + ) + return response + + # 5) Non-image branch: ensure original is cached try: with cache.read(subpath) as reader: file_path = reader.name except KeyError: - # cache miss - if mime.startswith('image/'): - # ─── 4a) Image branch: thumbnail & cache the JPEG ─── - try: - with Image.open(full_path) as img: - img.thumbnail((1920, 1920)) - if img.mode in ("RGBA", "P"): - img = img.convert("RGB") - thumb_io = io.BytesIO() - img.save(thumb_io, format='JPEG', quality=85) - thumb_io.seek(0) - # write thumbnail into diskcache as a real file - cache.set(subpath, thumb_io, read=True) - # now re-open from cache to get the on-disk path - with cache.read(subpath) as reader: - file_path = reader.name - except Exception as e: - app.logger.error(f"Image processing failed for {subpath}: {e}") - abort(500) - else: - # ─── 4b) Non-image branch: cache original file ─── - try: - # store the real file on diskcache - cache.set(subpath, open(full_path, 'rb'), read=True) - # read back to get its path - with cache.read(subpath) as reader: - file_path = reader.name - except Exception as e: - app.logger.error(f"Failed to cache file {subpath}: {e}") - abort(500) + try: + cache.set(subpath, open(full_path, 'rb'), read=True) + with cache.read(subpath) as reader: + file_path = reader.name + except Exception as e: + app.logger.error(f"Failed to cache file {subpath}: {e}") + abort(500) + # 6) Build response for non-image filesize = os.path.getsize(file_path) - cached_hit = True # or False if you want to pass that into your logger + cached_hit = True - # 5) Figure out download flag and filename + # Figure out download flag and filename ask_download = request.args.get('download') == 'true' filename = os.path.basename(full_path) - # 6) Single send_file call with proper attachment handling + # Single send_file call with proper attachment handling response = send_file( file_path, - mimetype = mime, - conditional = True, - as_attachment = ask_download, - download_name = filename if ask_download else None + mimetype=mime, + conditional=True, + as_attachment=ask_download, + download_name=filename if ask_download else None ) - - # Explicitly force inline if not downloading if not ask_download: response.headers['Content-Disposition'] = 'inline' - response.headers['Cache-Control'] = 'public, max-age=86400' # 7) Logging if do_log: a.log_file_access( - subpath, filesize, mime, - ip_address, user_agent, - session['device_id'], cached_hit + subpath, + filesize, + mime, + ip_address, + user_agent, + session['device_id'], + cached_hit ) - return response