Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 36x 36x 36x 2x 2x 36x 2x 2x 2x 36x 36x 34x 34x 34x 2x 2x 2x 2x 2x 2x 2x 2x 36x 109x 2x 28x 7x 10x 10x 2x 2x 2x 9x 2x 8x 2x 2x 2x 2x 2x 23x 3x 3x 8x 3x 3x 3x 58x 44x 44x 8x 46x 46x 46x 36x 59x 55x 8x 8x 4x 4x 4x 4x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 36x 109x 1x 109x 102x 109x 104x 109x 2x 8x 8x 36x 202x 2x 8x 8x 8x 199x | // src/middleware/multer.middleware.ts
import multer from 'multer';
import path from 'path';
import fs from 'node:fs/promises';
import { Request, Response, NextFunction } from 'express';
import { UserProfile } from '../types';
import { sanitizeFilename } from '../utils/stringUtils';
import { ValidationError } from '../services/db/errors.db';
import { logger } from '../services/logger.server';
export const flyerStoragePath =
process.env.STORAGE_PATH || '/var/www/flyer-crawler.projectium.com/flyer-images';
export const avatarStoragePath = path.join(process.cwd(), 'public', 'uploads', 'avatars');
export const receiptStoragePath = path.join(
process.env.STORAGE_PATH || '/var/www/flyer-crawler.projectium.com',
'receipts',
);
// Ensure directories exist at startup
(async () => {
try {
await fs.mkdir(flyerStoragePath, { recursive: true });
await fs.mkdir(avatarStoragePath, { recursive: true });
await fs.mkdir(receiptStoragePath, { recursive: true });
logger.info('Ensured multer storage directories exist.');
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
logger.error({ error: err }, 'Failed to create multer storage directories on startup.');
}
})();
type StorageType = 'flyer' | 'avatar' | 'receipt';
const getStorageConfig = (type: StorageType) => {
switch (type) {
case 'avatar':
return multer.diskStorage({
destination: (req, file, cb) => cb(null, avatarStoragePath),
filename: (req, file, cb) => {
const user = req.user as UserProfile | undefined;
if (!user) {
// This should ideally not happen if auth middleware runs first.
return cb(new Error('User not authenticated for avatar upload'), '');
}
if (process.env.NODE_ENV === 'test') {
// Use a predictable filename for test avatars for easy cleanup.
return cb(null, `test-avatar${path.extname(file.originalname) || '.png'}`);
}
const uniqueSuffix = `${user.user.user_id}-${Date.now()}${path.extname(
file.originalname,
)}`;
cb(null, uniqueSuffix);
},
});
case 'receipt':
return multer.diskStorage({
destination: (req, file, cb) => cb(null, receiptStoragePath),
filename: (req, file, cb) => {
const user = req.user as UserProfile | undefined;
const userId = user?.user.user_id || 'anonymous';
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
const sanitizedOriginalName = sanitizeFilename(file.originalname);
cb(null, `receipt-${userId}-${uniqueSuffix}-${sanitizedOriginalName}`);
},
});
case 'flyer':
default:
return multer.diskStorage({
destination: (req, file, cb) => {
console.error('[MULTER DEBUG] Flyer storage destination:', flyerStoragePath);
cb(null, flyerStoragePath);
},
filename: (req, file, cb) => {
// Use unique filenames in ALL environments to prevent race conditions
// between concurrent test runs or uploads.
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
const sanitizedOriginalName = sanitizeFilename(file.originalname);
cb(null, `${file.fieldname}-${uniqueSuffix}-${sanitizedOriginalName}`);
},
});
}
};
const imageFileFilter = (
req: Request,
file: Express.Multer.File,
cb: multer.FileFilterCallback,
) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
// Reject the file with a specific error that can be caught by a middleware.
const validationIssue = {
path: ['file', file.fieldname],
message: 'Only image files are allowed!',
};
const err = new ValidationError([validationIssue], 'Only image files are allowed!');
cb(err as Error); // Cast to Error to satisfy multer's type, though ValidationError extends Error.
}
};
interface MulterOptions {
storageType: StorageType;
fileSize?: number;
fileFilter?: 'image';
}
/**
* Creates a configured multer instance for file uploads.
* @param options - Configuration for storage type, file size, and file filter.
* @returns A multer instance.
*/
export const createUploadMiddleware = (options: MulterOptions) => {
const multerOptions: multer.Options = {
storage: getStorageConfig(options.storageType),
};
if (options.fileSize) {
multerOptions.limits = { fileSize: options.fileSize };
}
if (options.fileFilter === 'image') {
multerOptions.fileFilter = imageFileFilter;
}
return multer(multerOptions);
};
/**
* A general error handler for multer. Place this after all routes using multer in your router file.
* It catches errors from `fileFilter` and other multer issues (e.g., file size limits).
*/
export const handleMulterError = (err: Error, req: Request, res: Response, next: NextFunction) => {
if (err instanceof multer.MulterError) {
// A Multer error occurred when uploading (e.g., file too large).
return res.status(400).json({ message: `File upload error: ${err.message}` });
}
// If it's not a multer error, pass it on.
next(err);
};
|