feat(downloads): preserve original camera filenames on download (opt-in) (#493)

New Settings → General toggle `Use original filenames on download` (off by
default). When on, single-photo downloads, bulk/selection zips, and per-event
archive zips surface `photos.original_filename` instead of the sanitized
storage filename. Storage paths are unchanged.

- Content-Disposition uses RFC 5987 (`filename=` ASCII + `filename*=UTF-8''…`)
  so unicode camera filenames survive while header-injection bytes are stripped.
- Zip entries are deduplicated with a deterministic `_1` / `_2` suffix on
  collision (folder structure preserved in archive zips).
- Pre-generated download-all zips and the in-memory setting cache are
  invalidated when the toggle flips so the next download rebuilds with the
  new names.
- Falls back to the storage filename whenever `original_filename` is null
  (legacy uploads predating migration 062).
This commit is contained in:
Paul Nothaft
2026-05-14 23:11:00 +02:00
parent 61f1d13210
commit 7eeef2ba98
13 changed files with 460 additions and 19 deletions
@@ -20,6 +20,7 @@ export interface GeneralSettings {
enable_registration: boolean;
maintenance_mode: boolean;
short_gallery_urls: boolean;
use_original_filenames_for_downloads: boolean;
default_language: string;
date_format: { format: string; locale: string };
}
@@ -94,6 +95,7 @@ export function useSettingsState() {
enable_registration: false,
maintenance_mode: false,
short_gallery_urls: false,
use_original_filenames_for_downloads: false,
default_language: 'en',
date_format: { format: 'dd/MM/yyyy', locale: 'en-GB' }
});
@@ -179,6 +181,10 @@ export function useSettingsState() {
enable_registration: toBoolean(settings.general_enable_registration, false),
maintenance_mode: toBoolean(settings.general_maintenance_mode, false),
short_gallery_urls: toBoolean(settings.general_short_gallery_urls, false),
use_original_filenames_for_downloads: toBoolean(
settings.general_use_original_filenames_for_downloads,
false
),
default_language: settings.general_default_language || 'en',
date_format: settings.general_date_format
? (typeof settings.general_date_format === 'string'
@@ -229,6 +229,21 @@ export const GeneralTab: React.FC<GeneralTabProps> = ({
{t('settings.general.enableShortGalleryUrlsHelp')}
</p>
</div>
<div>
<label className="flex items-center">
<input
type="checkbox"
checked={generalSettings.use_original_filenames_for_downloads}
onChange={(e) => setGeneralSettings(prev => ({ ...prev, use_original_filenames_for_downloads: e.target.checked }))}
className="w-4 h-4 text-primary-600 rounded focus:ring-primary-500"
/>
<span className="ml-2 text-sm text-neutral-700 dark:text-neutral-300">{t('settings.general.useOriginalFilenames')}</span>
</label>
<p className="text-xs text-neutral-500 dark:text-neutral-400 ml-6 mt-1">
{t('settings.general.useOriginalFilenamesHelp')}
</p>
</div>
</div>
</Card>
+2
View File
@@ -1038,6 +1038,8 @@
"enableRegistration": "Selbstregistrierung für Admins erlauben",
"enableShortGalleryUrls": "Kurze Galerie-Links verwenden",
"enableShortGalleryUrlsHelp": "Entfernt den Veranstaltungs-Slug aus neuen Freigabelinks und lässt bestehende Links weiterhin funktionieren.",
"useOriginalFilenames": "Originale Dateinamen beim Download verwenden",
"useOriginalFilenamesHelp": "Wenn aktiviert, verwenden Einzel- und ZIP-Downloads den Original-Dateinamen der Kamera (z. B. DSC_1234.jpg) statt des umbenannten Namens. Der Speicher bleibt unverändert; Duplikate innerhalb einer Veranstaltung erhalten ein numerisches Suffix.",
"maintenanceMode": "Wartungsmodus aktivieren",
"language": "Sprache",
"defaultLanguageHelp": "Sprache, die Gästen vor der Anmeldung angezeigt wird",
+2
View File
@@ -677,6 +677,8 @@
"enableRegistration": "Allow self-registration for admins",
"enableShortGalleryUrls": "Use short gallery URLs",
"enableShortGalleryUrlsHelp": "Removes the event slug from new share links while keeping existing links working.",
"useOriginalFilenames": "Use original filenames on download",
"useOriginalFilenamesHelp": "When on, single-photo and ZIP downloads use the original camera filename (e.g. DSC_1234.jpg) instead of the sanitized name. Storage is unchanged; duplicates in the same event get a numeric suffix.",
"maintenanceMode": "Enable maintenance mode",
"language": "Language",
"defaultLanguageHelp": "Language shown to guests before login",