diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index 71d62c7f..a2ef40fc 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -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 ./ diff --git a/backend/src/services/photoProcessor.js b/backend/src/services/photoProcessor.js index 986df36a..5820cb1f 100644 --- a/backend/src/services/photoProcessor.js +++ b/backend/src/services/photoProcessor.js @@ -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 { diff --git a/backend/src/services/photoReplacementService.js b/backend/src/services/photoReplacementService.js index 8799825c..1944aeae 100644 --- a/backend/src/services/photoReplacementService.js +++ b/backend/src/services/photoReplacementService.js @@ -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(); diff --git a/backend/src/services/watermarkGeneratorService.js b/backend/src/services/watermarkGeneratorService.js index 1424de37..746e55aa 100644 --- a/backend/src/services/watermarkGeneratorService.js +++ b/backend/src/services/watermarkGeneratorService.js @@ -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) {