feat(downloads): per-gallery download resolutions (#858) (#1022)

Clients who need smaller files no longer make the photographer re-export. Two capabilities, both off by default.

STANDARD RESOLUTION — the size a gallery hands out for every ordinary download (single, selected, download-all). Global default in Settings, overridable per gallery with the NULL=inherit tri-state. The pre-built download-all zip is built AT the standard resolution, so changing it invalidates those archives, including a fan-out to inheriting galleries.

RESOLUTION PICKER — opt-in modal letting guests choose a different size. Custom archives are built as a DB-backed job the client polls, never cached. The picker never offers a size above the standard, and Original reappears only when the admin explicitly allows it.

Resize is fit:'inside' + withoutEnlargement — aspect preserved, never upscaled — applied before the watermark, since the mark is sized relative to its input.

Three rounds of external review hardened this: job archives are bound to the requester's visibility scope and re-validated at delivery, the streamed download-all path applies the cap, queue admission is bounded, and rejected resolutions no longer inflate download stats.

Closes #858.
This commit is contained in:
Paul Nothaft
2026-08-11 09:46:46 +02:00
committed by GitHub
parent 02deac9f10
commit 8e3573788b
29 changed files with 2716 additions and 82 deletions
+34 -2
View File
@@ -153,7 +153,9 @@
"showAll": "Alle anzeigen",
"confirm": "Bestätigen",
"show": "Einblenden",
"poweredBy": "Bereitgestellt von"
"poweredBy": "Bereitgestellt von",
"on": "an",
"off": "aus"
},
"upload": {
"photoCategory": "Fotokategorie",
@@ -1044,7 +1046,17 @@
"socials": "Soziale Netzwerke"
},
"photosCount_one": "{{count}} Foto",
"photosCount_other": "{{count}} Fotos"
"photosCount_other": "{{count}} Fotos",
"chooseResolution": "Downloadgröße wählen",
"resolutionUpTo": "bis zu {{width}} × {{height}} px",
"prepareDownload": "Download vorbereiten",
"preparingDownload": "Download wird vorbereitet…",
"preparingHint": "Fotos werden verkleinert — bei großen Galerien kann das einen Moment dauern.",
"preparingProgress": "{{count}} Fotos verpackt",
"downloadReady": "Ihr Download ist bereit",
"downloadNow": "Herunterladen",
"downloadPrepFailed": "Vorbereitung fehlgeschlagen",
"downloadPrepTimeout": "Das dauert länger als erwartet. Bitte erneut versuchen."
},
"categories": {
"title": "Fotokategorien",
@@ -2284,6 +2296,26 @@
"logoutFromIdpHint": "Die Abmeldung von PicPeak beendet auch die IdP-Sitzung (RP-initiated Logout). Gilt nur für Sitzungen, die per SSO angemeldet wurden; ohne diese Option bleibt die IdP-Sitzung bestehen und der nächste SSO-Klick meldet direkt wieder an.",
"postLogoutRedirectUri": "Post-Logout-Redirect-URI (beim IdP-Client registrieren, z. B. Keycloak „Valid post logout redirect URIs“)"
}
},
"downloads": {
"title": "Download-Auflösungen",
"intro": "Die Standardgröße gibt jede Galerie standardmäßig heraus. Einzelne Galerien können das überschreiben.",
"standard": "Standardauflösung",
"original": "Original (volle Größe)",
"picker": "Gäste die Downloadgröße wählen lassen",
"pickerHint": "Fügt Sammel-Downloads eine Größenauswahl hinzu. Eigene Größen werden bei Bedarf erzeugt und sind nie größer als der Standard.",
"allowOriginal": "„Original“ in der Auswahl anbieten",
"allowOriginalHint": "Standardmäßig aus: Wer die Standardgröße reduziert, möchte in der Regel keine Dateien in voller Auflösung herausgeben.",
"presets": "Verfügbare Größen",
"presetsHint": "Größen sind eine Obergrenze — das Seitenverhältnis bleibt erhalten und Fotos werden nie vergrößert.",
"label": "Bezeichnung",
"width": "Breite",
"height": "Höhe",
"eventTitle": "Download-Auflösung",
"eventIntro": "Überschreibt die globalen Download-Einstellungen nur für diese Galerie. „Erben“ folgt Einstellungen → Download-Auflösungen.",
"inheritWith": "Erben ({{value}})",
"effective": "Gibt derzeit heraus: {{standard}}",
"pickerOn": "Gäste dürfen eine andere Größe wählen"
}
},
"branding": {
+34 -2
View File
@@ -153,7 +153,9 @@
"showAll": "Show all",
"confirm": "Confirm",
"show": "Show",
"poweredBy": "Powered by"
"poweredBy": "Powered by",
"on": "on",
"off": "off"
},
"upload": {
"photoCategory": "Photo Category",
@@ -589,7 +591,17 @@
"photosSelected_one": "{{count}} photo selected",
"photosSelected_other": "{{count}} photos selected",
"downloadSelected_one": "Download {{count}} photo",
"downloadSelected_other": "Download {{count}} photos"
"downloadSelected_other": "Download {{count}} photos",
"chooseResolution": "Choose a download size",
"resolutionUpTo": "up to {{width}} × {{height}} px",
"prepareDownload": "Prepare download",
"preparingDownload": "Preparing your download…",
"preparingHint": "Resizing photos — this can take a moment for large galleries.",
"preparingProgress": "{{count}} photos packaged",
"downloadReady": "Your download is ready",
"downloadNow": "Download",
"downloadPrepFailed": "Preparation failed",
"downloadPrepTimeout": "This is taking longer than expected. Please try again."
},
"categories": {
"title": "Photo Categories",
@@ -1829,6 +1841,26 @@
"logoutFromIdpHint": "Logging out of PicPeak also ends the IdP session (RP-initiated logout). Only applies to sessions that signed in via SSO; without this, logging out of PicPeak leaves the IdP session alive and the next SSO click signs straight back in.",
"postLogoutRedirectUri": "Post-logout redirect URI (register this on your IdP client, e.g. Keycloak \"Valid post logout redirect URIs\")"
}
},
"downloads": {
"title": "Download resolutions",
"intro": "The standard size is what every gallery hands out by default. Individual galleries can override this.",
"standard": "Standard resolution",
"original": "Original (full size)",
"picker": "Let guests choose a download size",
"pickerHint": "Adds a size picker to bulk downloads. Custom sizes are prepared on demand and are never larger than the standard.",
"allowOriginal": "Offer \"Original\" in the picker",
"allowOriginalHint": "Off by default: lowering the standard size normally means full-resolution files should not be handed out.",
"presets": "Available sizes",
"presetsHint": "Sizes are an upper bound — the aspect ratio is kept and photos are never enlarged.",
"label": "Label",
"width": "Width",
"height": "Height",
"eventTitle": "Download resolution",
"eventIntro": "Override the site-wide download settings for this gallery only. \"Inherit\" follows Settings → Download resolutions.",
"inheritWith": "Inherit ({{value}})",
"effective": "Currently hands out: {{standard}}",
"pickerOn": "guests may choose another size"
}
},
"analytics": {