Files
flyer-crawler.projectium.com/src/middleware/multer.middleware.ts
Torben Sorensen 93497bf7c7
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 12m2s
unit test fixes
2025-12-27 11:00:19 -08:00

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);
};