fix(images): probe and clean up preview tiers under the extension the encoder actually wrote (#1355)

generatePreviewImage rewrites the output extension to match the encoding
it chose, .jpg or .webp for alpha and multi-frame sources. The tier
lookup in ensurePreviewImageAtWidth and the cleanup list in
previewTierKeys kept the SOURCE extension instead, so for anything but a
lowercase .jpg source the stat never matched: every tier request for a
.png, .JPG, .jpeg, .heic or RAW photo re-ran Sharp, and cleanup never
found the files it left behind, which accumulated for the life of the
install.

Both now derive every key the tier can live under: the .jpg and .webp
candidates, plus the source-extension key last so tiers written before
the rewrite are still found by lookup and by cleanup.

Follow-up to issue 1020, where the mismatch was identified during review.

Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
Paul Nothaft
2026-09-08 08:41:53 +02:00
committed by GitHub
co-authored by Paul Nothaft
parent c97341e454
commit acb25a9a1c
3 changed files with 208 additions and 20 deletions
@@ -129,8 +129,15 @@ describe('preview tiers (#1095)', () => {
it('derives every non-default tier key for cleanup', () => {
// Tiers live outside preview_path, so delete/archive/regenerate have no
// other way to find them. 1920 is excluded because that IS preview_path.
// Two candidates per width — the encoder picks `.jpg` or `.webp` and the
// cleanup list cannot know which without probing the source.
const keys = imageProcessor.previewTierKeys({ id: 5, path: 'e/a.jpg', source_origin: 'managed' });
expect(keys).toHaveLength(imageProcessor.PREVIEW_WIDTHS.length - 1);
const widths = imageProcessor.PREVIEW_WIDTHS.filter((w) => w !== 1920);
expect(keys).toHaveLength(widths.length * 2);
for (const w of widths) {
expect(keys).toContain(`previews/preview_w${w}_p5_a.jpg`);
expect(keys).toContain(`previews/preview_w${w}_p5_a.webp`);
}
expect(keys.some((k) => k.includes('w1920'))).toBe(false);
expect(keys.every((k) => k.includes('p5_'))).toBe(true);
});