From c093c3f7c2e9ed01f4489abfb14424088b34ea1a Mon Sep 17 00:00:00 2001 From: abiandev Date: Wed, 29 Jul 2026 12:59:29 +0200 Subject: [PATCH] logs para averiaguar el fallo de subida de archivos desde la app --- app.js | 70 ++++++++++++++++++++++++++ src/middleware/driverLicenseUpload.js | 22 ++++++++ src/middleware/profilePhotoUpload.js | 14 ++++++ src/middleware/tripStatusUpload.js | 41 +++++++++------ src/services/tripStatusPhotoStorage.js | 29 +++++++---- src/utils/uploadDiagnostics.js | 68 +++++++++++++++++++++++++ 6 files changed, 219 insertions(+), 25 deletions(-) create mode 100644 src/utils/uploadDiagnostics.js diff --git a/app.js b/app.js index 17ce22a..2947d31 100644 --- a/app.js +++ b/app.js @@ -55,6 +55,76 @@ app.use((req, res, next) => { contentType.toLowerCase().startsWith('multipart/form-data'); if (isMultipartFormData) { + const startedAt = process.hrtime.bigint(); + const baseLogPayload = { + request_id: requestId, + method: req.method, + path: req.originalUrl || req.url, + ip: req.ip || null, + content_type: contentType, + content_length: Number.parseInt(req.get('content-length'), 10) || null, + has_authorization_header: Boolean(authorizationHeader), + user_agent: String(req.get('user-agent') || '').slice(0, 255) || null + }; + let responseFinished = false; + + appendPostLog({ + event: 'upload_request_started', + ...baseLogPayload + }); + + res.once('finish', () => { + responseFinished = true; + const parserStatus = req.uploadDiagnostics?.parser_status || 'not_reached'; + let failureStage = null; + + if (res.statusCode >= 400) { + if (parserStatus === 'rejected') { + failureStage = 'multipart_parser'; + } else if (parserStatus === 'parsed') { + failureStage = 'controller_or_persistence'; + } else { + failureStage = 'authentication_rate_limit_or_route'; + } + } + + appendPostLog({ + event: 'upload_request_finished', + ...baseLogPayload, + status_code: res.statusCode, + duration_ms: Number( + (Number(process.hrtime.bigint() - startedAt) / 1e6).toFixed(2) + ), + request_complete: req.complete, + outcome: res.statusCode < 400 ? 'success' : 'error', + failure_stage: failureStage, + upload: req.uploadDiagnostics || { + parser_status: 'not_reached' + } + }); + }); + + res.once('close', () => { + if (responseFinished) { + return; + } + + appendPostLog({ + event: 'upload_request_interrupted', + ...baseLogPayload, + status_code: res.statusCode, + duration_ms: Number( + (Number(process.hrtime.bigint() - startedAt) / 1e6).toFixed(2) + ), + request_complete: req.complete, + outcome: 'interrupted', + failure_stage: 'transport_or_client_disconnect', + upload: req.uploadDiagnostics || { + parser_status: 'not_reached' + } + }); + }); + return next(); } diff --git a/src/middleware/driverLicenseUpload.js b/src/middleware/driverLicenseUpload.js index 5c4bfcb..8cbec44 100644 --- a/src/middleware/driverLicenseUpload.js +++ b/src/middleware/driverLicenseUpload.js @@ -1,5 +1,10 @@ const multer = require('multer'); const path = require('path'); +const { + beginUploadParsing, + markUploadParsed, + markUploadRejected +} = require('../utils/uploadDiagnostics'); const MAX_DRIVER_LICENSE_SIZE_BYTES = 5 * 1024 * 1024; const FRONT_FILE_FIELD = 'carnet_conducir_frontal'; @@ -40,14 +45,31 @@ const internalUpload = multer({ }); const uploadDriverLicense = (req, res, next) => { + const flow = 'driver_license'; + + beginUploadParsing(req, flow); internalUpload.fields([ { name: FRONT_FILE_FIELD, maxCount: 1 }, { name: BACK_FILE_FIELD, maxCount: 1 } ])(req, res, (error) => { if (!error) { + const uploadedFiles = [ + ...(Array.isArray(req.files?.[FRONT_FILE_FIELD]) + ? req.files[FRONT_FILE_FIELD] + : []), + ...(Array.isArray(req.files?.[BACK_FILE_FIELD]) + ? req.files[BACK_FILE_FIELD] + : []) + ]; + markUploadParsed(req, { + flow, + files: uploadedFiles + }); return next(); } + markUploadRejected(req, { flow, error }); + if (error instanceof multer.MulterError) { if (error.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ diff --git a/src/middleware/profilePhotoUpload.js b/src/middleware/profilePhotoUpload.js index e8cd746..b263d51 100644 --- a/src/middleware/profilePhotoUpload.js +++ b/src/middleware/profilePhotoUpload.js @@ -2,6 +2,11 @@ const crypto = require('crypto'); const fs = require('fs'); const multer = require('multer'); const path = require('path'); +const { + beginUploadParsing, + markUploadParsed, + markUploadRejected +} = require('../utils/uploadDiagnostics'); const MAX_PROFILE_PHOTO_SIZE_BYTES = 5 * 1024 * 1024; const PROFILE_UPLOADS_DIR = path.resolve(__dirname, '..', '..', 'uploads', 'profile'); @@ -57,11 +62,20 @@ const internalUpload = multer({ }); const uploadProfilePhoto = (req, res, next) => { + const flow = 'profile_photo'; + + beginUploadParsing(req, flow); internalUpload.single('foto_perfil')(req, res, (error) => { if (!error) { + markUploadParsed(req, { + flow, + files: req.file ? [req.file] : [] + }); return next(); } + markUploadRejected(req, { flow, error }); + if (error instanceof multer.MulterError) { if (error.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ error: 'Archivo demasiado grande. Maximo 5MB.' }); diff --git a/src/middleware/tripStatusUpload.js b/src/middleware/tripStatusUpload.js index 3ff6ced..1869727 100644 --- a/src/middleware/tripStatusUpload.js +++ b/src/middleware/tripStatusUpload.js @@ -4,11 +4,15 @@ const multer = require('multer'); const path = require('path'); const { getTripStatusUploadsDir, - getTripStatusFallbackUploadsDir, replicateUploadedFilesToRemote, removeUploadedTripStatusFiles } = require('../services/tripStatusPhotoStorage'); const { appendPostLog } = require('../utils/postLog'); +const { + beginUploadParsing, + markUploadParsed, + markUploadRejected +} = require('../utils/uploadDiagnostics'); const MAX_TRIP_STATUS_PHOTO_SIZE_BYTES = 15 * 1024 * 1024; const MAX_TRIP_STATUS_FILES = 5; @@ -28,21 +32,9 @@ const getTripDirectorySegment = (req) => { 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; - } + fs.mkdirSync(getTripStatusUploadsTripDir(req), { recursive: true }); }; const getExtensionFromMimeType = (mimeType) => { @@ -68,7 +60,8 @@ const getExtensionFromMimeType = (mimeType) => { const storage = multer.diskStorage({ destination: (req, file, cb) => { try { - cb(null, ensureTripStatusUploadsDir(req)); + ensureTripStatusUploadsDir(req); + cb(null, getTripStatusUploadsTripDir(req)); } catch (error) { cb(error); } @@ -96,6 +89,12 @@ const internalUpload = multer({ }); const uploadTripStatusPhotos = (req, res, next) => { + const flow = 'trip_status_photos'; + const storageMode = String(process.env.TRIP_STATUS_PHOTO_STORAGE_MODE || 'local') + .trim() + .toLowerCase(); + + beginUploadParsing(req, flow); internalUpload.fields([ { name: 'fotos', maxCount: MAX_TRIP_STATUS_FILES }, { name: 'fotos[]', maxCount: MAX_TRIP_STATUS_FILES } @@ -104,6 +103,11 @@ const uploadTripStatusPhotos = (req, res, next) => { const uploadedFiles = collectUploadedTripStatusFiles(req); const authorizationHeader = req.get('authorization'); + markUploadParsed(req, { + flow, + files: uploadedFiles, + storageMode + }); appendPostLog({ request_id: req.requestId || null, method: req.method, @@ -127,9 +131,16 @@ const uploadTripStatusPhotos = (req, res, next) => { tripId: req.params?.id, files: uploadedFiles }); + markUploadParsed(req, { + flow, + files: uploadedFiles, + storageMode + }); return next(); } + markUploadRejected(req, { flow, error }); + if (error instanceof multer.MulterError) { return res.status(400).json({ success: false, diff --git a/src/services/tripStatusPhotoStorage.js b/src/services/tripStatusPhotoStorage.js index d6b6131..4ca5ac1 100644 --- a/src/services/tripStatusPhotoStorage.js +++ b/src/services/tripStatusPhotoStorage.js @@ -6,11 +6,6 @@ const DEFAULT_SFTP_PORT = 22; let sftpClientFactoryOverride = null; -const PRIMARY_TRIP_STATUS_UPLOAD_DIR = - '/var/www/vhosts/gestion.abianservice.com/httpdocs/produccion/app/fotos_estado_react_native/trips/status'; -const FALLBACK_TRIP_STATUS_UPLOAD_DIR = - path.resolve(__dirname, '..', '..', 'uploads', 'trips', 'status'); - const resolveUploadDirCandidate = (uploadDir) => path.isAbsolute(uploadDir) ? uploadDir @@ -42,10 +37,19 @@ const selectUploadDirCandidate = (uploadDirs) => .sort((left, right) => left.score - right.score || left.index - right.index)[0]?.path; const getTripStatusUploadsDir = () => { - return PRIMARY_TRIP_STATUS_UPLOAD_DIR; -}; + const configuredUploadDir = String(process.env.TRIP_STATUS_UPLOAD_DIR || '').trim(); -const getTripStatusFallbackUploadsDir = () => FALLBACK_TRIP_STATUS_UPLOAD_DIR; + if (configuredUploadDir) { + const configuredUploadDirs = configuredUploadDir + .split(';') + .map((uploadDir) => uploadDir.trim()) + .filter(Boolean); + + return selectUploadDirCandidate(configuredUploadDirs); + } + + return path.resolve(__dirname, '..', '..', 'uploads', 'trips', 'status'); +}; const getTripStatusPhotoStorageMode = () => String(process.env.TRIP_STATUS_PHOTO_STORAGE_MODE || 'local') @@ -205,7 +209,7 @@ const replicateUploadedFilesToRemote = async ({ tripId, files }) => { file.tripStatusTripId = tripDirectorySegment; } - await withSftpClient( + const remoteOperationSucceeded = await withSftpClient( async (client, sftpConfig) => { await ensureRemoteTripDirectory(client, { remoteBaseDir: sftpConfig.remoteBaseDir, @@ -238,6 +242,12 @@ const replicateUploadedFilesToRemote = async ({ tripId, files }) => { logContext: 'replicate_upload' } ); + + if (shouldUseRemoteStorage() && !remoteOperationSucceeded) { + for (const file of normalizedFiles) { + file.tripStatusRemoteUploaded = false; + } + } }; const removeRemoteFiles = async (remoteFilePaths, { logContext }) => { @@ -389,7 +399,6 @@ const __resetSftpClientFactoryForTests = () => { module.exports = { getTripStatusUploadsDir, - getTripStatusFallbackUploadsDir, replicateUploadedFilesToRemote, removeUploadedTripStatusFiles, removeStatusPhotosByName, diff --git a/src/utils/uploadDiagnostics.js b/src/utils/uploadDiagnostics.js new file mode 100644 index 0000000..e36aa04 --- /dev/null +++ b/src/utils/uploadDiagnostics.js @@ -0,0 +1,68 @@ +const path = require('path'); + +const getBodyFieldNames = (req) => + Object.keys(req.body || {}).sort(); + +const getFileMetadata = (file) => { + const originalExtension = path.extname(String(file?.originalname || '')).toLowerCase(); + let remoteUploadStatus = null; + + if (file?.tripStatusRemoteUploaded === true) { + remoteUploadStatus = 'success'; + } else if (file?.tripStatusRemoteUploaded === false) { + remoteUploadStatus = 'failed'; + } + + return { + field_name: file?.fieldname || null, + original_extension: originalExtension || null, + mimetype: file?.mimetype || null, + size_bytes: Number.isFinite(file?.size) ? file.size : null, + stored_filename: file?.filename || null, + local_file_created: Boolean(file?.path), + remote_upload_status: remoteUploadStatus + }; +}; + +const beginUploadParsing = (req, flow) => { + req.uploadDiagnostics = { + flow, + parser: 'multer', + parser_status: 'started' + }; +}; + +const markUploadParsed = (req, { flow, files, storageMode = null }) => { + const normalizedFiles = Array.isArray(files) ? files : []; + + req.uploadDiagnostics = { + flow, + parser: 'multer', + parser_status: 'parsed', + body_fields: getBodyFieldNames(req), + file_count: normalizedFiles.length, + files: normalizedFiles.map(getFileMetadata), + storage_mode: storageMode + }; +}; + +const markUploadRejected = (req, { flow, error }) => { + req.uploadDiagnostics = { + flow, + parser: 'multer', + parser_status: 'rejected', + body_fields: getBodyFieldNames(req), + error: { + type: error?.constructor?.name || 'Error', + code: error?.code || error?.message || null, + field_name: error?.field || null, + message: String(error?.message || 'Unknown upload error').slice(0, 500) + } + }; +}; + +module.exports = { + beginUploadParsing, + markUploadParsed, + markUploadRejected +};