debug: add comprehensive logging to recipe image upload flow
All checks were successful
CI / update (push) Successful in 1m23s
All checks were successful
CI / update (push) Successful in 1m23s
Add detailed console logging throughout the image upload pipeline to help diagnose upload issues: - Log file metadata and validation steps in imageValidation.ts - Log image processing and file saving operations in imageProcessing.ts - Log form data and processing steps in recipe add page action - Log API request details and upload progress in img/add endpoint All logs are prefixed with component name ([ImageValidation], [ImageProcessing], [RecipeAdd], [API:ImgAdd]) for easy filtering and debugging.
This commit is contained in:
@@ -36,13 +36,19 @@ export const actions = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
|
console.log('[RecipeAdd] Form data received');
|
||||||
|
|
||||||
// Extract recipe data from FormData
|
// Extract recipe data from FormData
|
||||||
const recipeData = extractRecipeFromFormData(formData);
|
const recipeData = extractRecipeFromFormData(formData);
|
||||||
|
console.log('[RecipeAdd] Recipe data extracted:', {
|
||||||
|
short_name: recipeData.short_name,
|
||||||
|
title: recipeData.title
|
||||||
|
});
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
const validationErrors = validateRecipeData(recipeData);
|
const validationErrors = validateRecipeData(recipeData);
|
||||||
if (validationErrors.length > 0) {
|
if (validationErrors.length > 0) {
|
||||||
|
console.error('[RecipeAdd] Validation errors:', validationErrors);
|
||||||
return fail(400, {
|
return fail(400, {
|
||||||
error: validationErrors.join(', '),
|
error: validationErrors.join(', '),
|
||||||
errors: validationErrors,
|
errors: validationErrors,
|
||||||
@@ -52,8 +58,16 @@ export const actions = {
|
|||||||
|
|
||||||
// Handle optional image upload
|
// Handle optional image upload
|
||||||
const recipeImage = formData.get('recipe_image') as File | null;
|
const recipeImage = formData.get('recipe_image') as File | null;
|
||||||
|
console.log('[RecipeAdd] Recipe image from form:', {
|
||||||
|
hasImage: !!recipeImage,
|
||||||
|
size: recipeImage?.size,
|
||||||
|
name: recipeImage?.name,
|
||||||
|
type: recipeImage?.type
|
||||||
|
});
|
||||||
|
|
||||||
if (recipeImage && recipeImage.size > 0) {
|
if (recipeImage && recipeImage.size > 0) {
|
||||||
try {
|
try {
|
||||||
|
console.log('[RecipeAdd] Starting image processing...');
|
||||||
// Process and save the image
|
// Process and save the image
|
||||||
const { filename } = await processAndSaveRecipeImage(
|
const { filename } = await processAndSaveRecipeImage(
|
||||||
recipeImage,
|
recipeImage,
|
||||||
@@ -61,13 +75,14 @@ export const actions = {
|
|||||||
IMAGE_DIR
|
IMAGE_DIR
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log('[RecipeAdd] Image processed successfully, filename:', filename);
|
||||||
recipeData.images = [{
|
recipeData.images = [{
|
||||||
mediapath: filename,
|
mediapath: filename,
|
||||||
alt: '',
|
alt: '',
|
||||||
caption: ''
|
caption: ''
|
||||||
}];
|
}];
|
||||||
} catch (imageError: any) {
|
} catch (imageError: any) {
|
||||||
console.error('Image processing error:', imageError);
|
console.error('[RecipeAdd] Image processing error:', imageError);
|
||||||
return fail(400, {
|
return fail(400, {
|
||||||
error: `Failed to process image: ${imageError.message}`,
|
error: `Failed to process image: ${imageError.message}`,
|
||||||
errors: ['Image processing failed'],
|
errors: ['Image processing failed'],
|
||||||
@@ -75,6 +90,7 @@ export const actions = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
console.log('[RecipeAdd] No image uploaded, using placeholder');
|
||||||
// No image uploaded - use placeholder based on short_name
|
// No image uploaded - use placeholder based on short_name
|
||||||
recipeData.images = [{
|
recipeData.images = [{
|
||||||
mediapath: `${recipeData.short_name}.webp`,
|
mediapath: `${recipeData.short_name}.webp`,
|
||||||
|
|||||||
@@ -19,11 +19,15 @@ import { validateImageFile } from '$utils/imageValidation';
|
|||||||
* @route POST /api/rezepte/img/add
|
* @route POST /api/rezepte/img/add
|
||||||
*/
|
*/
|
||||||
export const POST = (async ({ request, locals }) => {
|
export const POST = (async ({ request, locals }) => {
|
||||||
|
console.log('[API:ImgAdd] Image upload request received');
|
||||||
|
|
||||||
// Check authentication
|
// Check authentication
|
||||||
const auth = await locals.auth();
|
const auth = await locals.auth();
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
|
console.error('[API:ImgAdd] Authentication required');
|
||||||
throw error(401, 'Authentication required to upload images');
|
throw error(401, 'Authentication required to upload images');
|
||||||
}
|
}
|
||||||
|
console.log('[API:ImgAdd] Authentication passed');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
@@ -32,71 +36,99 @@ export const POST = (async ({ request, locals }) => {
|
|||||||
const image = formData.get('image') as File;
|
const image = formData.get('image') as File;
|
||||||
const name = formData.get('name')?.toString().trim();
|
const name = formData.get('name')?.toString().trim();
|
||||||
|
|
||||||
|
console.log('[API:ImgAdd] Form data:', {
|
||||||
|
hasImage: !!image,
|
||||||
|
imageSize: image?.size,
|
||||||
|
imageName: image?.name,
|
||||||
|
imageType: image?.type,
|
||||||
|
recipeName: name
|
||||||
|
});
|
||||||
|
|
||||||
if (!image) {
|
if (!image) {
|
||||||
|
console.error('[API:ImgAdd] No image file provided');
|
||||||
throw error(400, 'No image file provided');
|
throw error(400, 'No image file provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
|
console.error('[API:ImgAdd] Image name is required');
|
||||||
throw error(400, 'Image name is required');
|
throw error(400, 'Image name is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comprehensive security validation
|
// Comprehensive security validation
|
||||||
|
console.log('[API:ImgAdd] Starting validation...');
|
||||||
const validationResult = await validateImageFile(image);
|
const validationResult = await validateImageFile(image);
|
||||||
if (!validationResult.valid) {
|
if (!validationResult.valid) {
|
||||||
|
console.error('[API:ImgAdd] Validation failed:', validationResult.error);
|
||||||
throw error(400, validationResult.error || 'Invalid image file');
|
throw error(400, validationResult.error || 'Invalid image file');
|
||||||
}
|
}
|
||||||
|
console.log('[API:ImgAdd] Validation passed');
|
||||||
|
|
||||||
// Convert File to Buffer for processing
|
// Convert File to Buffer for processing
|
||||||
const arrayBuffer = await image.arrayBuffer();
|
const arrayBuffer = await image.arrayBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
console.log('[API:ImgAdd] Buffer created, size:', buffer.length, 'bytes');
|
||||||
|
|
||||||
// Generate content hash for cache busting
|
// Generate content hash for cache busting
|
||||||
const imageHash = generateImageHashFromBuffer(buffer);
|
const imageHash = generateImageHashFromBuffer(buffer);
|
||||||
const hashedFilename = getHashedFilename(name, imageHash);
|
const hashedFilename = getHashedFilename(name, imageHash);
|
||||||
const unhashedFilename = name + '.webp';
|
const unhashedFilename = name + '.webp';
|
||||||
|
console.log('[API:ImgAdd] Generated filenames:', {
|
||||||
|
hashed: hashedFilename,
|
||||||
|
unhashed: unhashedFilename
|
||||||
|
});
|
||||||
|
|
||||||
// Process image with Sharp - convert to WebP format
|
// Process image with Sharp - convert to WebP format
|
||||||
// Save full size - both hashed and unhashed versions
|
// Save full size - both hashed and unhashed versions
|
||||||
|
console.log('[API:ImgAdd] Processing full size image...');
|
||||||
const fullBuffer = await sharp(buffer)
|
const fullBuffer = await sharp(buffer)
|
||||||
.toFormat('webp')
|
.toFormat('webp')
|
||||||
.webp({ quality: 90 }) // High quality for full size
|
.webp({ quality: 90 }) // High quality for full size
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
console.log('[API:ImgAdd] Full size buffer created, size:', fullBuffer.length, 'bytes');
|
||||||
|
|
||||||
await sharp(fullBuffer).toFile(
|
const fullHashedPath = path.join(IMAGE_DIR, 'rezepte', 'full', hashedFilename);
|
||||||
path.join(IMAGE_DIR, 'rezepte', 'full', hashedFilename)
|
const fullUnhashedPath = path.join(IMAGE_DIR, 'rezepte', 'full', unhashedFilename);
|
||||||
);
|
console.log('[API:ImgAdd] Saving full size to:', { fullHashedPath, fullUnhashedPath });
|
||||||
await sharp(fullBuffer).toFile(
|
|
||||||
path.join(IMAGE_DIR, 'rezepte', 'full', unhashedFilename)
|
await sharp(fullBuffer).toFile(fullHashedPath);
|
||||||
);
|
await sharp(fullBuffer).toFile(fullUnhashedPath);
|
||||||
|
console.log('[API:ImgAdd] Full size images saved ✓');
|
||||||
|
|
||||||
// Save thumbnail (800px width) - both hashed and unhashed versions
|
// Save thumbnail (800px width) - both hashed and unhashed versions
|
||||||
|
console.log('[API:ImgAdd] Processing thumbnail...');
|
||||||
const thumbBuffer = await sharp(buffer)
|
const thumbBuffer = await sharp(buffer)
|
||||||
.resize({ width: 800 })
|
.resize({ width: 800 })
|
||||||
.toFormat('webp')
|
.toFormat('webp')
|
||||||
.webp({ quality: 85 })
|
.webp({ quality: 85 })
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
console.log('[API:ImgAdd] Thumbnail buffer created, size:', thumbBuffer.length, 'bytes');
|
||||||
|
|
||||||
await sharp(thumbBuffer).toFile(
|
const thumbHashedPath = path.join(IMAGE_DIR, 'rezepte', 'thumb', hashedFilename);
|
||||||
path.join(IMAGE_DIR, 'rezepte', 'thumb', hashedFilename)
|
const thumbUnhashedPath = path.join(IMAGE_DIR, 'rezepte', 'thumb', unhashedFilename);
|
||||||
);
|
console.log('[API:ImgAdd] Saving thumbnail to:', { thumbHashedPath, thumbUnhashedPath });
|
||||||
await sharp(thumbBuffer).toFile(
|
|
||||||
path.join(IMAGE_DIR, 'rezepte', 'thumb', unhashedFilename)
|
await sharp(thumbBuffer).toFile(thumbHashedPath);
|
||||||
);
|
await sharp(thumbBuffer).toFile(thumbUnhashedPath);
|
||||||
|
console.log('[API:ImgAdd] Thumbnail images saved ✓');
|
||||||
|
|
||||||
// Save placeholder (20px width) - both hashed and unhashed versions
|
// Save placeholder (20px width) - both hashed and unhashed versions
|
||||||
|
console.log('[API:ImgAdd] Processing placeholder...');
|
||||||
const placeholderBuffer = await sharp(buffer)
|
const placeholderBuffer = await sharp(buffer)
|
||||||
.resize({ width: 20 })
|
.resize({ width: 20 })
|
||||||
.toFormat('webp')
|
.toFormat('webp')
|
||||||
.webp({ quality: 60 })
|
.webp({ quality: 60 })
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
console.log('[API:ImgAdd] Placeholder buffer created, size:', placeholderBuffer.length, 'bytes');
|
||||||
|
|
||||||
await sharp(placeholderBuffer).toFile(
|
const placeholderHashedPath = path.join(IMAGE_DIR, 'rezepte', 'placeholder', hashedFilename);
|
||||||
path.join(IMAGE_DIR, 'rezepte', 'placeholder', hashedFilename)
|
const placeholderUnhashedPath = path.join(IMAGE_DIR, 'rezepte', 'placeholder', unhashedFilename);
|
||||||
);
|
console.log('[API:ImgAdd] Saving placeholder to:', { placeholderHashedPath, placeholderUnhashedPath });
|
||||||
await sharp(placeholderBuffer).toFile(
|
|
||||||
path.join(IMAGE_DIR, 'rezepte', 'placeholder', unhashedFilename)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
await sharp(placeholderBuffer).toFile(placeholderHashedPath);
|
||||||
|
await sharp(placeholderBuffer).toFile(placeholderUnhashedPath);
|
||||||
|
console.log('[API:ImgAdd] Placeholder images saved ✓');
|
||||||
|
|
||||||
|
console.log('[API:ImgAdd] Upload completed successfully ✓');
|
||||||
return json({
|
return json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: 'Image uploaded successfully',
|
msg: 'Image uploaded successfully',
|
||||||
@@ -108,7 +140,7 @@ export const POST = (async ({ request, locals }) => {
|
|||||||
if (err.status) throw err;
|
if (err.status) throw err;
|
||||||
|
|
||||||
// Log and throw generic error for unexpected failures
|
// Log and throw generic error for unexpected failures
|
||||||
console.error('Image upload error:', err);
|
console.error('[API:ImgAdd] Upload error:', err);
|
||||||
throw error(500, `Failed to upload image: ${err.message || 'Unknown error'}`);
|
throw error(500, `Failed to upload image: ${err.message || 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
}) satisfies RequestHandler;
|
}) satisfies RequestHandler;
|
||||||
|
|||||||
@@ -15,63 +15,86 @@ export async function processAndSaveRecipeImage(
|
|||||||
name: string,
|
name: string,
|
||||||
imageDir: string
|
imageDir: string
|
||||||
): Promise<{ filename: string; unhashedFilename: string }> {
|
): Promise<{ filename: string; unhashedFilename: string }> {
|
||||||
|
console.log('[ImageProcessing] Starting image processing for:', {
|
||||||
|
fileName: file.name,
|
||||||
|
recipeName: name,
|
||||||
|
imageDir: imageDir
|
||||||
|
});
|
||||||
|
|
||||||
// Comprehensive security validation
|
// Comprehensive security validation
|
||||||
const validationResult = await validateImageFile(file);
|
const validationResult = await validateImageFile(file);
|
||||||
if (!validationResult.valid) {
|
if (!validationResult.valid) {
|
||||||
|
console.error('[ImageProcessing] Validation failed:', validationResult.error);
|
||||||
throw new Error(validationResult.error || 'Invalid image file');
|
throw new Error(validationResult.error || 'Invalid image file');
|
||||||
}
|
}
|
||||||
|
console.log('[ImageProcessing] Validation succeeded');
|
||||||
|
|
||||||
// Convert File to Buffer for processing
|
// Convert File to Buffer for processing
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
console.log('[ImageProcessing] Buffer created, size:', buffer.length, 'bytes');
|
||||||
|
|
||||||
// Generate content hash for cache busting
|
// Generate content hash for cache busting
|
||||||
const imageHash = generateImageHashFromBuffer(buffer);
|
const imageHash = generateImageHashFromBuffer(buffer);
|
||||||
const hashedFilename = getHashedFilename(name, imageHash);
|
const hashedFilename = getHashedFilename(name, imageHash);
|
||||||
const unhashedFilename = name + '.webp';
|
const unhashedFilename = name + '.webp';
|
||||||
|
console.log('[ImageProcessing] Generated filenames:', {
|
||||||
|
hashed: hashedFilename,
|
||||||
|
unhashed: unhashedFilename
|
||||||
|
});
|
||||||
|
|
||||||
// Process image with Sharp - convert to WebP format
|
// Process image with Sharp - convert to WebP format
|
||||||
// Save full size - both hashed and unhashed versions
|
// Save full size - both hashed and unhashed versions
|
||||||
|
console.log('[ImageProcessing] Converting to WebP and generating full size...');
|
||||||
const fullBuffer = await sharp(buffer)
|
const fullBuffer = await sharp(buffer)
|
||||||
.toFormat('webp')
|
.toFormat('webp')
|
||||||
.webp({ quality: 90 }) // High quality for full size
|
.webp({ quality: 90 }) // High quality for full size
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
console.log('[ImageProcessing] Full size buffer created, size:', fullBuffer.length, 'bytes');
|
||||||
|
|
||||||
await sharp(fullBuffer).toFile(
|
const fullHashedPath = path.join(imageDir, 'rezepte', 'full', hashedFilename);
|
||||||
path.join(imageDir, 'rezepte', 'full', hashedFilename)
|
const fullUnhashedPath = path.join(imageDir, 'rezepte', 'full', unhashedFilename);
|
||||||
);
|
console.log('[ImageProcessing] Saving full size to:', { fullHashedPath, fullUnhashedPath });
|
||||||
await sharp(fullBuffer).toFile(
|
|
||||||
path.join(imageDir, 'rezepte', 'full', unhashedFilename)
|
await sharp(fullBuffer).toFile(fullHashedPath);
|
||||||
);
|
await sharp(fullBuffer).toFile(fullUnhashedPath);
|
||||||
|
console.log('[ImageProcessing] Full size images saved ✓');
|
||||||
|
|
||||||
// Save thumbnail (800px width) - both hashed and unhashed versions
|
// Save thumbnail (800px width) - both hashed and unhashed versions
|
||||||
|
console.log('[ImageProcessing] Generating thumbnail (800px)...');
|
||||||
const thumbBuffer = await sharp(buffer)
|
const thumbBuffer = await sharp(buffer)
|
||||||
.resize({ width: 800 })
|
.resize({ width: 800 })
|
||||||
.toFormat('webp')
|
.toFormat('webp')
|
||||||
.webp({ quality: 85 })
|
.webp({ quality: 85 })
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
console.log('[ImageProcessing] Thumbnail buffer created, size:', thumbBuffer.length, 'bytes');
|
||||||
|
|
||||||
await sharp(thumbBuffer).toFile(
|
const thumbHashedPath = path.join(imageDir, 'rezepte', 'thumb', hashedFilename);
|
||||||
path.join(imageDir, 'rezepte', 'thumb', hashedFilename)
|
const thumbUnhashedPath = path.join(imageDir, 'rezepte', 'thumb', unhashedFilename);
|
||||||
);
|
console.log('[ImageProcessing] Saving thumbnail to:', { thumbHashedPath, thumbUnhashedPath });
|
||||||
await sharp(thumbBuffer).toFile(
|
|
||||||
path.join(imageDir, 'rezepte', 'thumb', unhashedFilename)
|
await sharp(thumbBuffer).toFile(thumbHashedPath);
|
||||||
);
|
await sharp(thumbBuffer).toFile(thumbUnhashedPath);
|
||||||
|
console.log('[ImageProcessing] Thumbnail images saved ✓');
|
||||||
|
|
||||||
// Save placeholder (20px width) - both hashed and unhashed versions
|
// Save placeholder (20px width) - both hashed and unhashed versions
|
||||||
|
console.log('[ImageProcessing] Generating placeholder (20px)...');
|
||||||
const placeholderBuffer = await sharp(buffer)
|
const placeholderBuffer = await sharp(buffer)
|
||||||
.resize({ width: 20 })
|
.resize({ width: 20 })
|
||||||
.toFormat('webp')
|
.toFormat('webp')
|
||||||
.webp({ quality: 60 })
|
.webp({ quality: 60 })
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
console.log('[ImageProcessing] Placeholder buffer created, size:', placeholderBuffer.length, 'bytes');
|
||||||
|
|
||||||
await sharp(placeholderBuffer).toFile(
|
const placeholderHashedPath = path.join(imageDir, 'rezepte', 'placeholder', hashedFilename);
|
||||||
path.join(imageDir, 'rezepte', 'placeholder', hashedFilename)
|
const placeholderUnhashedPath = path.join(imageDir, 'rezepte', 'placeholder', unhashedFilename);
|
||||||
);
|
console.log('[ImageProcessing] Saving placeholder to:', { placeholderHashedPath, placeholderUnhashedPath });
|
||||||
await sharp(placeholderBuffer).toFile(
|
|
||||||
path.join(imageDir, 'rezepte', 'placeholder', unhashedFilename)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
await sharp(placeholderBuffer).toFile(placeholderHashedPath);
|
||||||
|
await sharp(placeholderBuffer).toFile(placeholderUnhashedPath);
|
||||||
|
console.log('[ImageProcessing] Placeholder images saved ✓');
|
||||||
|
|
||||||
|
console.log('[ImageProcessing] All image versions processed and saved successfully ✓');
|
||||||
return {
|
return {
|
||||||
filename: hashedFilename,
|
filename: hashedFilename,
|
||||||
unhashedFilename: unhashedFilename
|
unhashedFilename: unhashedFilename
|
||||||
|
|||||||
@@ -34,8 +34,15 @@ const MAGIC_BYTES = {
|
|||||||
* @returns ValidationResult with valid flag and optional error message
|
* @returns ValidationResult with valid flag and optional error message
|
||||||
*/
|
*/
|
||||||
export async function validateImageFile(file: File): Promise<ValidationResult> {
|
export async function validateImageFile(file: File): Promise<ValidationResult> {
|
||||||
|
console.log('[ImageValidation] Starting validation for file:', {
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type
|
||||||
|
});
|
||||||
|
|
||||||
// Layer 1: Check file size
|
// Layer 1: Check file size
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
|
console.error('[ImageValidation] File too large:', file.size, 'bytes');
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: `File size must be less than ${MAX_FILE_SIZE / 1024 / 1024}MB. Current size: ${(file.size / 1024 / 1024).toFixed(2)}MB`
|
error: `File size must be less than ${MAX_FILE_SIZE / 1024 / 1024}MB. Current size: ${(file.size / 1024 / 1024).toFixed(2)}MB`
|
||||||
@@ -43,6 +50,7 @@ export async function validateImageFile(file: File): Promise<ValidationResult> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (file.size === 0) {
|
if (file.size === 0) {
|
||||||
|
console.error('[ImageValidation] File is empty');
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: 'File is empty'
|
error: 'File is empty'
|
||||||
@@ -51,20 +59,24 @@ export async function validateImageFile(file: File): Promise<ValidationResult> {
|
|||||||
|
|
||||||
// Layer 2: Check MIME type (client-provided)
|
// Layer 2: Check MIME type (client-provided)
|
||||||
if (!ALLOWED_MIME_TYPES.includes(file.type)) {
|
if (!ALLOWED_MIME_TYPES.includes(file.type)) {
|
||||||
|
console.error('[ImageValidation] Invalid MIME type:', file.type);
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: `Invalid file type. Allowed types: ${ALLOWED_MIME_TYPES.join(', ')}. Received: ${file.type || 'unknown'}`
|
error: `Invalid file type. Allowed types: ${ALLOWED_MIME_TYPES.join(', ')}. Received: ${file.type || 'unknown'}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
console.log('[ImageValidation] MIME type valid:', file.type);
|
||||||
|
|
||||||
// Layer 3: Check file extension
|
// Layer 3: Check file extension
|
||||||
const extension = file.name.split('.').pop()?.toLowerCase();
|
const extension = file.name.split('.').pop()?.toLowerCase();
|
||||||
if (!extension || !ALLOWED_EXTENSIONS.includes(extension)) {
|
if (!extension || !ALLOWED_EXTENSIONS.includes(extension)) {
|
||||||
|
console.error('[ImageValidation] Invalid extension:', extension);
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: `Invalid file extension. Allowed: ${ALLOWED_EXTENSIONS.join(', ')}. Received: ${extension || 'none'}`
|
error: `Invalid file extension. Allowed: ${ALLOWED_EXTENSIONS.join(', ')}. Received: ${extension || 'none'}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
console.log('[ImageValidation] Extension valid:', extension);
|
||||||
|
|
||||||
// Convert File to Buffer for magic bytes validation
|
// Convert File to Buffer for magic bytes validation
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
@@ -75,14 +87,18 @@ export async function validateImageFile(file: File): Promise<ValidationResult> {
|
|||||||
const fileType = await fileTypeFromBuffer(buffer);
|
const fileType = await fileTypeFromBuffer(buffer);
|
||||||
|
|
||||||
if (!fileType) {
|
if (!fileType) {
|
||||||
|
console.error('[ImageValidation] Unable to detect file type from headers');
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: 'Unable to detect file type from file headers. File may be corrupted or not a valid image.'
|
error: 'Unable to detect file type from file headers. File may be corrupted or not a valid image.'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[ImageValidation] Detected file type from headers:', fileType.mime);
|
||||||
|
|
||||||
// Verify detected type matches allowed types
|
// Verify detected type matches allowed types
|
||||||
if (!ALLOWED_MIME_TYPES.includes(fileType.mime)) {
|
if (!ALLOWED_MIME_TYPES.includes(fileType.mime)) {
|
||||||
|
console.error('[ImageValidation] Detected type not allowed:', fileType.mime);
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: `File headers indicate type "${fileType.mime}" which is not allowed. This file may have been renamed to bypass filters.`
|
error: `File headers indicate type "${fileType.mime}" which is not allowed. This file may have been renamed to bypass filters.`
|
||||||
@@ -91,12 +107,18 @@ export async function validateImageFile(file: File): Promise<ValidationResult> {
|
|||||||
|
|
||||||
// Verify MIME type consistency
|
// Verify MIME type consistency
|
||||||
if (fileType.mime !== file.type) {
|
if (fileType.mime !== file.type) {
|
||||||
|
console.error('[ImageValidation] MIME type mismatch:', {
|
||||||
|
claimed: file.type,
|
||||||
|
actual: fileType.mime
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: `File type mismatch: claimed to be "${file.type}" but actual type is "${fileType.mime}". Possible file spoofing attempt.`
|
error: `File type mismatch: claimed to be "${file.type}" but actual type is "${fileType.mime}". Possible file spoofing attempt.`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
console.log('[ImageValidation] Magic bytes validation passed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('[ImageValidation] Magic bytes validation error:', error);
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: `Failed to validate file headers: ${error.message}`
|
error: `Failed to validate file headers: ${error.message}`
|
||||||
@@ -107,7 +129,14 @@ export async function validateImageFile(file: File): Promise<ValidationResult> {
|
|||||||
try {
|
try {
|
||||||
const metadata = await sharp(buffer).metadata();
|
const metadata = await sharp(buffer).metadata();
|
||||||
|
|
||||||
|
console.log('[ImageValidation] Sharp metadata:', {
|
||||||
|
width: metadata.width,
|
||||||
|
height: metadata.height,
|
||||||
|
format: metadata.format
|
||||||
|
});
|
||||||
|
|
||||||
if (!metadata.width || !metadata.height) {
|
if (!metadata.width || !metadata.height) {
|
||||||
|
console.error('[ImageValidation] Unable to read image dimensions');
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: 'Invalid image: unable to read image dimensions'
|
error: 'Invalid image: unable to read image dimensions'
|
||||||
@@ -115,18 +144,25 @@ export async function validateImageFile(file: File): Promise<ValidationResult> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (metadata.width > 10000 || metadata.height > 10000) {
|
if (metadata.width > 10000 || metadata.height > 10000) {
|
||||||
|
console.error('[ImageValidation] Image dimensions too large:', {
|
||||||
|
width: metadata.width,
|
||||||
|
height: metadata.height
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: `Image dimensions too large: ${metadata.width}x${metadata.height}. Maximum: 10000x10000px`
|
error: `Image dimensions too large: ${metadata.width}x${metadata.height}. Maximum: 10000x10000px`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
console.log('[ImageValidation] Sharp validation passed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('[ImageValidation] Sharp validation error:', error);
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: `Invalid or corrupted image file: ${error.message}`
|
error: `Invalid or corrupted image file: ${error.message}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[ImageValidation] All validation layers passed ✓');
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user