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:
Claudio
2026-05-11 11:34:58 +02:00
parent 9e9b1ba224
commit 576b0c236d
8 changed files with 378 additions and 16 deletions

View File

@@ -0,0 +1,38 @@
"""Add photos.nextcloud_fileid for NC preview proxying
Revision ID: 0018_photos_nextcloud_fileid
Revises: 0017_photos_list_index
Create Date: 2026-05-11
The thumbnail endpoint will proxy Nextcloud's /index.php/core/preview
instead of generating and serving its own WebP cache under /data/thumbs.
That requires storing each photo's Nextcloud numeric fileid alongside
the row. NULL is allowed because legacy / non-NC photos still exist
and the handler keeps the on-disk fallback for them.
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "0018_photos_nextcloud_fileid"
down_revision: Union[str, None] = "0017_photos_list_index"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column(
"photos",
sa.Column("nextcloud_fileid", sa.Integer(), nullable=True),
)
op.create_index(
"ix_photos_nextcloud_fileid",
"photos",
["nextcloud_fileid"],
)
def downgrade() -> None:
op.drop_index("ix_photos_nextcloud_fileid", table_name="photos")
op.drop_column("photos", "nextcloud_fileid")