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'