feat: use checksummed filenames for recipe images and clean up old files
All checks were successful
CI / update (push) Successful in 1m13s

Updated recipe image handling to use checksummed filenames for proper
cache busting. When uploading a new image during recipe edit, old image
files (both hashed and unhashed versions) are now properly deleted from
all directories (full, thumb, placeholder).

Changes:
- CardAdd.svelte: Use checksummed filename from upload response
- Edit page server: Add deleteRecipeImage() helper to remove old images
- Edit page server: Delete old images when new image is uploaded
This commit is contained in:
2026-01-15 13:57:03 +01:00
parent ae32db7dfd
commit 5fae01c06d
2 changed files with 44 additions and 3 deletions

View File

@@ -113,7 +113,7 @@ export async function show_local_image(){
} }
const result = await response.json(); const result = await response.json();
uploaded_image_filename = result.unhashedFilename; uploaded_image_filename = result.filename;
upload_error = ""; upload_error = "";
} catch (error: any) { } catch (error: any) {

View File

@@ -4,7 +4,7 @@ import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db'; import { dbConnect } from '$utils/db';
import { invalidateRecipeCaches } from '$lib/server/cache'; import { invalidateRecipeCaches } from '$lib/server/cache';
import { IMAGE_DIR } from '$env/static/private'; import { IMAGE_DIR } from '$env/static/private';
import { rename, access } from 'fs/promises'; import { rename, access, unlink } from 'fs/promises';
import { join } from 'path'; import { join } from 'path';
import { constants } from 'fs'; import { constants } from 'fs';
import { import {
@@ -14,6 +14,43 @@ import {
detectChangedFields detectChangedFields
} from '$utils/recipeFormHelpers'; } from '$utils/recipeFormHelpers';
/**
* Delete recipe image files from all directories (full, thumb, placeholder)
* Handles both hashed and unhashed versions for backward compatibility
*/
async function deleteRecipeImage(filename: string): Promise<void> {
if (!filename) return;
const imageDirectories = ['full', 'thumb', 'placeholder'];
// Extract basename to handle both hashed and unhashed versions
const basename = filename
.replace(/\.[a-f0-9]{8}\.webp$/, '') // Remove hash if present
.replace(/\.webp$/, ''); // Remove extension
const unhashedFilename = basename + '.webp';
for (const dir of imageDirectories) {
// Delete hashed version (if it's the hashed filename that was passed in)
if (filename !== unhashedFilename) {
try {
await unlink(join(IMAGE_DIR, 'rezepte', dir, filename));
console.log(`Deleted hashed image: ${dir}/${filename}`);
} catch (err) {
console.warn(`Could not delete hashed image ${dir}/${filename}:`, err);
}
}
// Delete unhashed version
try {
await unlink(join(IMAGE_DIR, 'rezepte', dir, unhashedFilename));
console.log(`Deleted unhashed image: ${dir}/${unhashedFilename}`);
} catch (err) {
console.warn(`Could not delete unhashed image ${dir}/${unhashedFilename}:`, err);
}
}
}
export const load: PageServerLoad = async ({ fetch, params, locals}) => { export const load: PageServerLoad = async ({ fetch, params, locals}) => {
// Edit is German-only - redirect to German version // Edit is German-only - redirect to German version
if (params.recipeLang === 'recipes') { if (params.recipeLang === 'recipes') {
@@ -79,7 +116,11 @@ export const actions = {
const existingImagePath = formData.get('existing_image_path')?.toString(); const existingImagePath = formData.get('existing_image_path')?.toString();
if (uploadedImage) { if (uploadedImage) {
// New image uploaded - use it // New image uploaded - delete old image files and use new one
if (existingImagePath && existingImagePath !== uploadedImage) {
await deleteRecipeImage(existingImagePath);
}
recipeData.images = [{ recipeData.images = [{
mediapath: uploadedImage, mediapath: uploadedImage,
alt: existingImagePath ? (recipeData.images?.[0]?.alt || '') : '', alt: existingImagePath ? (recipeData.images?.[0]?.alt || '') : '',