logs para averiaguar el fallo de subida de archivos desde la app
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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.' });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
Reference in New Issue
Block a user