fix(uploads): RAW derivative key collision, watermark skip, dev exiftool (codex review of #833 round 2)

- Derivative key collision: processUploadedPhotos/replacePhoto passed the
  client-supplied original filename as the RAW output basename, but thumbnails/
  heroes/previews are global keys — two galleries uploading IMG_0001.dng would
  overwrite each other's derivative. Use the unique stored newFilename instead.
  (processPhoto already used the unique photo.filename.)
- Watermark: the watermark path opens the original with sharp, which can't decode
  RAW, so it fell back to the original bytes and recorded the copy as watermarked.
  Skip RAW in generateForPhoto (like videos) so the watermark state stays honest
  until RAW watermarking is properly supported.
- exiftool added to Dockerfile.dev so dev/native runtimes don't accept a DNG then
  fail it with ENOENT.
This commit is contained in:
Paul Nothaft
2026-07-17 22:50:24 +02:00
parent b743ea0398
commit d0ccadbc99
4 changed files with 19 additions and 4 deletions
+4 -1
View File
@@ -8,7 +8,10 @@ RUN apk upgrade --no-cache
# Install dumb-init for proper signal handling and ffmpeg for video uploads.
# Alpine's ffmpeg ships both ffmpeg + ffprobe built natively against musl;
# the npm-bundled binary doesn't run reliably on Alpine. Match production.
RUN apk add --no-cache dumb-init ffmpeg
# exiftool: extract embedded JPEG previews from RAW/DNG uploads (#821) — kept in
# sync with the production Dockerfile so dev/native runtimes don't accept a DNG
# and then fail it with ENOENT.
RUN apk add --no-cache dumb-init ffmpeg exiftool
# Copy package files
COPY package*.json ./
+3 -1
View File
@@ -148,7 +148,9 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ
// RAW/DNG can't be fed to sharp directly (no raw loader), so extract the
// embedded JPEG preview first and thumbnail/measure THAT. Pass-through
// for ordinary images. The stored original stays the RAW (download).
const proc = await withProcessableImage(tempPath, file.originalname);
// Use the unique stored filename (not the client-supplied original) so
// the RAW-derived thumbnail's global key can't collide across galleries.
const proc = await withProcessableImage(tempPath, newFilename);
try {
thumbnailPath = await generateThumbnail(proc.path, { outputBasename: proc.outputBasename });
try {
@@ -69,7 +69,9 @@ async function replacePhoto(existingPhoto, newFileTempPath, { originalFilename,
let width = null;
let height = null;
let thumbnailPath = null;
const proc = await withProcessableImage(newFileTempPath, originalFilename);
// Detect/name by the unique stored filename (newFilename), not the
// client-supplied original, so RAW derivative keys can't collide.
const proc = await withProcessableImage(newFileTempPath, newFilename);
try {
try {
const metadata = await sharp(proc.path).metadata();
@@ -11,7 +11,7 @@
const { db } = require('../database/db');
const watermarkService = require('./watermarkService');
const { resolvePhotoStorageKey, resolvePhotoFilePath } = require('./photoResolver');
const { withLocalCopy } = require('./imageProcessor');
const { withLocalCopy, isRawFilename } = require('./imageProcessor');
const logger = require('../utils/logger');
class WatermarkGeneratorService {
@@ -52,6 +52,14 @@ class WatermarkGeneratorService {
return { success: false, error: 'Videos do not support watermarks' };
}
// Skip RAW/DNG (experimental, #821). The watermark path opens the original
// with sharp, which can't decode RAW — proceeding would fall back to the
// original bytes and falsely record the copy as watermarked. Skipping keeps
// the watermark state honest until RAW watermarking is properly supported.
if (isRawFilename(photo.filename)) {
return { success: false, error: 'RAW/DNG files are not watermarked yet' };
}
// Get watermark settings
const settings = await watermarkService.getWatermarkSettings();
if (!settings || !settings.enabled) {