const crypto = require('crypto'); const fs = require('fs'); const multer = require('multer'); const path = require('path'); const { getTripStatusUploadsDir, getTripStatusFallbackUploadsDir, replicateUploadedFilesToRemote, removeUploadedTripStatusFiles } = require('../services/tripStatusPhotoStorage'); const { appendPostLog } = require('../utils/postLog'); const MAX_TRIP_STATUS_PHOTO_SIZE_BYTES = 15 * 1024 * 1024; const MAX_TRIP_STATUS_FILES = 5; const ALLOWED_MIME_TYPES = new Set([ 'image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/heic', 'image/heif' ]); const getTripDirectorySegment = (req) => { const tripId = Number.parseInt(req.params?.id, 10); return Number.isInteger(tripId) && tripId > 0 ? String(tripId) : 'unknown'; }; const getTripStatusUploadsTripDir = (req) => path.join(getTripStatusUploadsDir(), getTripDirectorySegment(req)); const getTripStatusFallbackUploadsTripDir = (req) => path.join(getTripStatusFallbackUploadsDir(), getTripDirectorySegment(req)); const ensureTripStatusUploadsDir = (req) => { const primaryTripDir = getTripStatusUploadsTripDir(req); try { fs.mkdirSync(primaryTripDir, { recursive: true }); return primaryTripDir; } catch (primaryError) { const fallbackTripDir = getTripStatusFallbackUploadsTripDir(req); fs.mkdirSync(fallbackTripDir, { recursive: true }); return fallbackTripDir; } }; const getExtensionFromMimeType = (mimeType) => { if (mimeType === 'image/png') { return '.png'; } if (mimeType === 'image/webp') { return '.webp'; } if (mimeType === 'image/heic') { return '.heic'; } if (mimeType === 'image/heif') { return '.heif'; } return '.jpg'; }; const storage = multer.diskStorage({ destination: (req, file, cb) => { try { cb(null, ensureTripStatusUploadsDir(req)); } catch (error) { cb(error); } }, filename: (req, file, cb) => { const token = crypto.randomBytes(3).toString('hex'); const extension = getExtensionFromMimeType(file.mimetype); cb(null, `${token}${extension}`); } }); const internalUpload = multer({ storage, limits: { fileSize: MAX_TRIP_STATUS_PHOTO_SIZE_BYTES, files: MAX_TRIP_STATUS_FILES }, fileFilter: (req, file, cb) => { if (!ALLOWED_MIME_TYPES.has(file.mimetype)) { return cb(new Error('INVALID_FILE_TYPE')); } cb(null, true); } }); const uploadTripStatusPhotos = (req, res, next) => { internalUpload.fields([ { name: 'fotos', maxCount: MAX_TRIP_STATUS_FILES }, { name: 'fotos[]', maxCount: MAX_TRIP_STATUS_FILES } ])(req, res, async (error) => { if (!error) { const uploadedFiles = collectUploadedTripStatusFiles(req); const authorizationHeader = req.get('authorization'); appendPostLog({ request_id: req.requestId || null, method: req.method, path: req.originalUrl || req.url, ip: req.ip || null, content_type: req.get('content-type') || null, has_authorization_header: Boolean(authorizationHeader), authorization: authorizationHeader ? '[REDACTED]' : null, query: req.query || {}, body: req.body || {}, raw_body: null, files: uploadedFiles.map((file) => ({ field_name: file.fieldname || null, filename: file.filename || null, originalname: file.originalname || null, mimetype: file.mimetype || null, size: Number.isFinite(file.size) ? file.size : null })) }); await replicateUploadedFilesToRemote({ tripId: req.params?.id, files: uploadedFiles }); return next(); } if (error instanceof multer.MulterError) { return res.status(400).json({ success: false, error: 'Invalid payload' }); } if (error.message === 'INVALID_FILE_TYPE') { return res.status(400).json({ success: false, error: 'Invalid payload' }); } console.error('Error uploading trip status photos:', error); return res.status(500).json({ success: false, error: 'Internal server error' }); }); }; const collectUploadedTripStatusFiles = (req) => [ ...(Array.isArray(req.files?.fotos) ? req.files.fotos : []), ...(Array.isArray(req.files?.['fotos[]']) ? req.files['fotos[]'] : []) ]; module.exports = { uploadTripStatusPhotos, collectUploadedTripStatusFiles, removeUploadedTripStatusFiles, MAX_TRIP_STATUS_FILES };