All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 12m2s
123 lines
4.3 KiB
TypeScript
123 lines
4.3 KiB
TypeScript
// 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 { 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');
|
|
|
|
// Ensure directories exist at startup
|
|
(async () => {
|
|
try {
|
|
await fs.mkdir(flyerStoragePath, { recursive: true });
|
|
await fs.mkdir(avatarStoragePath, { 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';
|
|
|
|
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 'flyer':
|
|
default:
|
|
return multer.diskStorage({
|
|
destination: (req, file, cb) => cb(null, flyerStoragePath),
|
|
filename: (req, file, cb) => {
|
|
if (process.env.NODE_ENV === 'test') {
|
|
// Use a predictable filename for test flyers for easy cleanup.
|
|
const ext = path.extname(file.originalname);
|
|
return cb(null, `${file.fieldname}-test-flyer-image${ext || '.jpg'}`);
|
|
}
|
|
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 err = new Error('Only image files are allowed!');
|
|
cb(err);
|
|
}
|
|
};
|
|
|
|
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}` });
|
|
} else if (err && err.message === 'Only image files are allowed!') {
|
|
// A custom error from our fileFilter.
|
|
return res.status(400).json({ message: err.message });
|
|
}
|
|
// If it's not a multer error, pass it on.
|
|
next(err);
|
|
}; |