feat(screenshots): serve thumbnails while keeping original for preview and copy

This commit is contained in:
2026-02-07 11:02:16 +08:00
parent 2d5be0feb2
commit 21c537da10
17 changed files with 130 additions and 54 deletions

View File

@@ -11,12 +11,21 @@ from app_config import get_config
from app_security import is_safe_path
from flask import Blueprint, jsonify, send_from_directory
from flask_login import current_user, login_required
from PIL import Image, ImageOps
from services.client_log import log_to_client
from services.time_utils import BEIJING_TZ
config = get_config()
SCREENSHOTS_DIR = config.SCREENSHOTS_DIR
_IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg")
_THUMBNAIL_DIR = os.path.join(SCREENSHOTS_DIR, ".thumbs")
_THUMBNAIL_MAX_SIZE = (480, 270)
_THUMBNAIL_QUALITY = 80
try:
_RESAMPLE_FILTER = Image.Resampling.LANCZOS
except AttributeError: # Pillow<9 fallback
_RESAMPLE_FILTER = Image.LANCZOS
api_screenshots_bp = Blueprint("api_screenshots", __name__)
@@ -49,6 +58,48 @@ def _build_display_name(filename: str) -> str:
return filename
def _thumbnail_name(filename: str) -> str:
stem, _ = os.path.splitext(filename)
return f"{stem}.thumb.jpg"
def _thumbnail_path(filename: str) -> str:
return os.path.join(_THUMBNAIL_DIR, _thumbnail_name(filename))
def _ensure_thumbnail(source_path: str, thumb_path: str) -> bool:
if not os.path.exists(source_path):
return False
source_mtime = os.path.getmtime(source_path)
if os.path.exists(thumb_path) and os.path.getmtime(thumb_path) >= source_mtime:
return True
os.makedirs(_THUMBNAIL_DIR, exist_ok=True)
with Image.open(source_path) as image:
image = ImageOps.exif_transpose(image)
if image.mode != "RGB":
image = image.convert("RGB")
image.thumbnail(_THUMBNAIL_MAX_SIZE, _RESAMPLE_FILTER)
image.save(
thumb_path,
format="JPEG",
quality=_THUMBNAIL_QUALITY,
optimize=True,
progressive=True,
)
os.utime(thumb_path, (source_mtime, source_mtime))
return True
def _remove_thumbnail(filename: str) -> None:
thumb_path = _thumbnail_path(filename)
if os.path.exists(thumb_path):
os.remove(thumb_path)
@api_screenshots_bp.route("/api/screenshots", methods=["GET"])
@login_required
def get_screenshots():
@@ -85,7 +136,7 @@ def get_screenshots():
@api_screenshots_bp.route("/screenshots/<filename>")
@login_required
def serve_screenshot(filename):
"""提供图文件访问"""
"""提供图文件访问"""
user_id = current_user.id
username_prefix = _get_user_prefix(user_id)
@@ -98,6 +149,34 @@ def serve_screenshot(filename):
return send_from_directory(SCREENSHOTS_DIR, filename)
@api_screenshots_bp.route("/screenshots/thumb/<filename>")
@login_required
def serve_screenshot_thumbnail(filename):
"""提供缩略图访问(失败时自动回退原图)"""
user_id = current_user.id
username_prefix = _get_user_prefix(user_id)
if not _is_user_screenshot(filename, username_prefix):
return jsonify({"error": "无权访问"}), 403
if not is_safe_path(SCREENSHOTS_DIR, filename):
return jsonify({"error": "非法路径"}), 403
source_path = os.path.join(SCREENSHOTS_DIR, filename)
if not os.path.exists(source_path):
return jsonify({"error": "文件不存在"}), 404
thumb_path = _thumbnail_path(filename)
try:
if _ensure_thumbnail(source_path, thumb_path) and os.path.exists(thumb_path):
return send_from_directory(_THUMBNAIL_DIR, os.path.basename(thumb_path), max_age=86400, conditional=True)
except Exception:
pass
return send_from_directory(SCREENSHOTS_DIR, filename, max_age=3600, conditional=True)
@api_screenshots_bp.route("/api/screenshots/<filename>", methods=["DELETE"])
@login_required
def delete_screenshot(filename):
@@ -115,6 +194,7 @@ def delete_screenshot(filename):
filepath = os.path.join(SCREENSHOTS_DIR, filename)
if os.path.exists(filepath):
os.remove(filepath)
_remove_thumbnail(filename)
log_to_client(f"删除截图: {filename}", user_id)
return jsonify({"success": True})
return jsonify({"error": "文件不存在"}), 404
@@ -133,6 +213,7 @@ def clear_all_screenshots():
deleted_count = 0
for entry in _iter_user_screenshot_entries(username_prefix):
os.remove(entry.path)
_remove_thumbnail(entry.name)
deleted_count += 1
log_to_client(f"清理了 {deleted_count} 个截图文件", user_id)