fix(uploads): apply RAW extraction in the actual async ingest path (codex review of #833)
The RAW/DNG extraction was only wired into processUploadedPhotos() (the synchronous path), but real uploads queue to 'pending' and are handled by the background worker → processPhoto(), which generated the thumbnail + dimensions directly from the DNG (both fail) and then marked the photo 'complete' — success with no thumbnail. Wire withProcessableImage() into processPhoto() (the live path) and into photoReplacementService.replacePhoto() (replace-by-name), so all three ingest paths extract the embedded JPEG preview for RAW. Updates the processPhoto test's imageProcessor mock with the new withProcessableImage dependency (pass-through for ordinary images).
This commit is contained in:
@@ -75,6 +75,13 @@ jest.mock('../../src/services/imageProcessor', () => {
|
|||||||
withLocalCopy: jest.fn(async (key, fn) =>
|
withLocalCopy: jest.fn(async (key, fn) =>
|
||||||
fn(`/tmp/local-copy-${require('path').basename(key)}`)
|
fn(`/tmp/local-copy-${require('path').basename(key)}`)
|
||||||
),
|
),
|
||||||
|
// Pass-through for ordinary (non-RAW) images: returns the path unchanged
|
||||||
|
// with a no-op cleanup, matching the real helper's behaviour for jpg/png.
|
||||||
|
withProcessableImage: jest.fn(async (localPath) => ({
|
||||||
|
path: localPath,
|
||||||
|
outputBasename: undefined,
|
||||||
|
cleanup: () => {},
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -465,15 +465,22 @@ async function processPhoto(photoId) {
|
|||||||
if (result.metadata.height) updateData.height = result.metadata.height;
|
if (result.metadata.height) updateData.height = result.metadata.height;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// RAW/DNG can't be sharp-decoded directly — extract the embedded JPEG
|
||||||
|
// preview and thumbnail/measure that. Pass-through for ordinary images.
|
||||||
|
// This is the ASYNC worker path (backgroundProcessor → processPhoto), the
|
||||||
|
// one real uploads actually take; the synchronous processUploadedPhotos()
|
||||||
|
// has the same handling.
|
||||||
|
const proc = await withProcessableImage(localPath, photo.filename);
|
||||||
try {
|
try {
|
||||||
const thumbnailPath = await generateThumbnail(localPath);
|
try {
|
||||||
|
const thumbnailPath = await generateThumbnail(proc.path, { outputBasename: proc.outputBasename });
|
||||||
if (thumbnailPath) updateData.thumbnail_path = thumbnailPath;
|
if (thumbnailPath) updateData.thumbnail_path = thumbnailPath;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn(`processPhoto: thumbnail generation failed for ${photoId}`, { error: e.message });
|
logger.warn(`processPhoto: thumbnail generation failed for ${photoId}`, { error: e.message });
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
const metadata = await sharp(localPath).metadata();
|
const metadata = await sharp(proc.path).metadata();
|
||||||
if (metadata.width && metadata.height) {
|
if (metadata.width && metadata.height) {
|
||||||
updateData.width = metadata.width;
|
updateData.width = metadata.width;
|
||||||
updateData.height = metadata.height;
|
updateData.height = metadata.height;
|
||||||
@@ -481,6 +488,9 @@ async function processPhoto(photoId) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn(`processPhoto: dimensions extraction failed for ${photoId}`, { error: e.message });
|
logger.warn(`processPhoto: dimensions extraction failed for ${photoId}`, { error: e.message });
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
await proc.cleanup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const path = require('path');
|
|||||||
const fsp = require('fs/promises');
|
const fsp = require('fs/promises');
|
||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
const { db } = require('../database/db');
|
const { db } = require('../database/db');
|
||||||
const { generateThumbnail, extractCaptureDate } = require('./imageProcessor');
|
const { generateThumbnail, extractCaptureDate, withProcessableImage } = require('./imageProcessor');
|
||||||
const { generatePhotoFilename } = require('../utils/filenameSanitizer');
|
const { generatePhotoFilename } = require('../utils/filenameSanitizer');
|
||||||
const watermarkGeneratorService = require('./watermarkGeneratorService');
|
const watermarkGeneratorService = require('./watermarkGeneratorService');
|
||||||
const { getStorage } = require('./storage');
|
const { getStorage } = require('./storage');
|
||||||
@@ -61,25 +61,31 @@ async function replacePhoto(existingPhoto, newFileTempPath, { originalFilename,
|
|||||||
// No EXIF — keep null
|
// No EXIF — keep null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stats = await fsp.stat(newFileTempPath);
|
||||||
|
|
||||||
|
// RAW/DNG isn't sharp-decodable — extract the embedded JPEG preview first
|
||||||
|
// (pass-through for ordinary images), then measure + thumbnail that. Mirrors
|
||||||
|
// the ingest paths (processPhoto / processUploadedPhotos).
|
||||||
let width = null;
|
let width = null;
|
||||||
let height = null;
|
let height = null;
|
||||||
|
let thumbnailPath = null;
|
||||||
|
const proc = await withProcessableImage(newFileTempPath, originalFilename);
|
||||||
try {
|
try {
|
||||||
const metadata = await sharp(newFileTempPath).metadata();
|
try {
|
||||||
|
const metadata = await sharp(proc.path).metadata();
|
||||||
width = metadata.width || null;
|
width = metadata.width || null;
|
||||||
height = metadata.height || null;
|
height = metadata.height || null;
|
||||||
} catch {
|
} catch {
|
||||||
// Non-image or corrupt
|
// Non-image or corrupt
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = await fsp.stat(newFileTempPath);
|
|
||||||
|
|
||||||
// Generate new thumbnail FROM the local temp before uploading the original.
|
|
||||||
let thumbnailPath = null;
|
|
||||||
try {
|
try {
|
||||||
thumbnailPath = await generateThumbnail(newFileTempPath);
|
thumbnailPath = await generateThumbnail(proc.path, { outputBasename: proc.outputBasename });
|
||||||
} catch {
|
} catch {
|
||||||
logger.warn('Failed to generate thumbnail for replaced photo', { photoId: existingPhoto.id });
|
logger.warn('Failed to generate thumbnail for replaced photo', { photoId: existingPhoto.id });
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
await proc.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
// Delete old assets BEFORE uploading the new key — if they share the path
|
// Delete old assets BEFORE uploading the new key — if they share the path
|
||||||
// (rare but possible if filename collision), we want the new content.
|
// (rare but possible if filename collision), we want the new content.
|
||||||
|
|||||||
Reference in New Issue
Block a user