feat(thumbs): proxy Nextcloud previews instead of duplicating the cache
mule-image was generating and storing three WebP sizes per photo in /data/thumbs while Nextcloud already keeps its own previews for the same source files. Frontend thumbnail requests now proxy NC's /index.php/core/preview keyed by the photo's Nextcloud fileid, authenticated with the owner's encrypted app password. - new column photos.nextcloud_fileid (alembic 0018) plus an index - get_preview_async + fetch_fileid helpers in nextcloud_dav.py - thumb route proxies NC primary, falls back to /data/thumbs (legacy rows / NC unreachable) so a single-file revert restores the old path - extract_metadata caches the fileid on first run for new photos - generate_thumbnails now writes only medium since the vision worker still loads it from disk; small + large drop out of the worker path - backend/scripts/backfill_nextcloud_fileid.py for one-shot population of existing rows: docker exec mulita-backend python -m scripts.backfill_nextcloud_fileid X-Mule-Thumb-Source response header marks each request 'nextcloud' or 'disk' for observability while the rollout settles.
This commit is contained in:
@@ -204,11 +204,39 @@ async def _extract_metadata_async(photo_id: str):
|
||||
select(Photo).where(Photo.id == photo_id)
|
||||
)
|
||||
photo = result.scalar_one_or_none()
|
||||
|
||||
|
||||
if not photo:
|
||||
logger.error(f"Photo not found: {photo_id}")
|
||||
return {'status': 'error', 'message': 'Photo not found'}
|
||||
|
||||
|
||||
# Cache Nextcloud's numeric fileid on the row so the thumbnail
|
||||
# handler can proxy /index.php/core/preview without doing a
|
||||
# PROPFIND per request. PROPFIND blocks for ~50ms; tolerable
|
||||
# because extract_metadata already does seconds of ExifTool
|
||||
# work. Failures are silent — the thumb handler falls back
|
||||
# to its on-disk path when the column is NULL.
|
||||
if photo.nextcloud_fileid is None and photo.user_id:
|
||||
from app.models.user import User
|
||||
from app.services.nextcloud_dav import (
|
||||
fetch_fileid, is_nextcloud_path,
|
||||
)
|
||||
if photo.filepath and is_nextcloud_path(photo.filepath):
|
||||
owner = (
|
||||
await session.execute(
|
||||
select(User).where(User.id == photo.user_id)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if owner is not None and owner.nextcloud_app_password_enc:
|
||||
try:
|
||||
fid = fetch_fileid(owner, photo.filepath)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"fileid lookup failed for %s: %s", photo_id, e
|
||||
)
|
||||
fid = None
|
||||
if fid is not None:
|
||||
photo.nextcloud_fileid = fid
|
||||
|
||||
# Check if file exists
|
||||
if not Path(photo.filepath).exists():
|
||||
logger.error(f"File not found: {photo.filepath}")
|
||||
|
||||
Reference in New Issue
Block a user