refactor: defer recipe image upload until form submission
Changed recipe image upload behavior to only process images when the form is submitted, rather than immediately on file selection. This prevents orphaned image files when users abandon the form. Changes: - CardAdd.svelte: Preview only, store File object instead of uploading - Created imageProcessing.ts: Shared utility for image processing - Add/edit page clients: Use selected_image_file instead of filename - Add/edit page servers: Process and save images during form submission - Images are validated, hashed, and saved in multiple formats on submit Benefits: - No orphaned files from abandoned forms - Faster initial file selection experience - Server-side image processing ensures security validation - Cleaner architecture with shared processing logic
This commit is contained in:
79
src/utils/imageProcessing.ts
Normal file
79
src/utils/imageProcessing.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import path from 'path';
|
||||
import sharp from 'sharp';
|
||||
import { generateImageHashFromBuffer, getHashedFilename } from '$utils/imageHash';
|
||||
import { validateImageFile } from '$utils/imageValidation';
|
||||
|
||||
/**
|
||||
* Process and save recipe image with multiple versions (full, thumb, placeholder)
|
||||
* @param file - The image File object
|
||||
* @param name - The base name for the image (usually recipe short_name)
|
||||
* @param imageDir - The base directory where images are stored
|
||||
* @returns Object with hashedFilename and unhashedFilename
|
||||
*/
|
||||
export async function processAndSaveRecipeImage(
|
||||
file: File,
|
||||
name: string,
|
||||
imageDir: string
|
||||
): Promise<{ filename: string; unhashedFilename: string }> {
|
||||
// Comprehensive security validation
|
||||
const validationResult = await validateImageFile(file);
|
||||
if (!validationResult.valid) {
|
||||
throw new Error(validationResult.error || 'Invalid image file');
|
||||
}
|
||||
|
||||
// Convert File to Buffer for processing
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
// Generate content hash for cache busting
|
||||
const imageHash = generateImageHashFromBuffer(buffer);
|
||||
const hashedFilename = getHashedFilename(name, imageHash);
|
||||
const unhashedFilename = name + '.webp';
|
||||
|
||||
// Process image with Sharp - convert to WebP format
|
||||
// Save full size - both hashed and unhashed versions
|
||||
const fullBuffer = await sharp(buffer)
|
||||
.toFormat('webp')
|
||||
.webp({ quality: 90 }) // High quality for full size
|
||||
.toBuffer();
|
||||
|
||||
await sharp(fullBuffer).toFile(
|
||||
path.join(imageDir, 'rezepte', 'full', hashedFilename)
|
||||
);
|
||||
await sharp(fullBuffer).toFile(
|
||||
path.join(imageDir, 'rezepte', 'full', unhashedFilename)
|
||||
);
|
||||
|
||||
// Save thumbnail (800px width) - both hashed and unhashed versions
|
||||
const thumbBuffer = await sharp(buffer)
|
||||
.resize({ width: 800 })
|
||||
.toFormat('webp')
|
||||
.webp({ quality: 85 })
|
||||
.toBuffer();
|
||||
|
||||
await sharp(thumbBuffer).toFile(
|
||||
path.join(imageDir, 'rezepte', 'thumb', hashedFilename)
|
||||
);
|
||||
await sharp(thumbBuffer).toFile(
|
||||
path.join(imageDir, 'rezepte', 'thumb', unhashedFilename)
|
||||
);
|
||||
|
||||
// Save placeholder (20px width) - both hashed and unhashed versions
|
||||
const placeholderBuffer = await sharp(buffer)
|
||||
.resize({ width: 20 })
|
||||
.toFormat('webp')
|
||||
.webp({ quality: 60 })
|
||||
.toBuffer();
|
||||
|
||||
await sharp(placeholderBuffer).toFile(
|
||||
path.join(imageDir, 'rezepte', 'placeholder', hashedFilename)
|
||||
);
|
||||
await sharp(placeholderBuffer).toFile(
|
||||
path.join(imageDir, 'rezepte', 'placeholder', unhashedFilename)
|
||||
);
|
||||
|
||||
return {
|
||||
filename: hashedFilename,
|
||||
unhashedFilename: unhashedFilename
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user