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');
|
contentType.toLowerCase().startsWith('multipart/form-data');
|
||||||
|
|
||||||
if (isMultipartFormData) {
|
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();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const {
|
||||||
|
beginUploadParsing,
|
||||||
|
markUploadParsed,
|
||||||
|
markUploadRejected
|
||||||
|
} = require('../utils/uploadDiagnostics');
|
||||||
|
|
||||||
const MAX_DRIVER_LICENSE_SIZE_BYTES = 5 * 1024 * 1024;
|
const MAX_DRIVER_LICENSE_SIZE_BYTES = 5 * 1024 * 1024;
|
||||||
const FRONT_FILE_FIELD = 'carnet_conducir_frontal';
|
const FRONT_FILE_FIELD = 'carnet_conducir_frontal';
|
||||||
@@ -40,14 +45,31 @@ const internalUpload = multer({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const uploadDriverLicense = (req, res, next) => {
|
const uploadDriverLicense = (req, res, next) => {
|
||||||
|
const flow = 'driver_license';
|
||||||
|
|
||||||
|
beginUploadParsing(req, flow);
|
||||||
internalUpload.fields([
|
internalUpload.fields([
|
||||||
{ name: FRONT_FILE_FIELD, maxCount: 1 },
|
{ name: FRONT_FILE_FIELD, maxCount: 1 },
|
||||||
{ name: BACK_FILE_FIELD, maxCount: 1 }
|
{ name: BACK_FILE_FIELD, maxCount: 1 }
|
||||||
])(req, res, (error) => {
|
])(req, res, (error) => {
|
||||||
if (!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();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markUploadRejected(req, { flow, error });
|
||||||
|
|
||||||
if (error instanceof multer.MulterError) {
|
if (error instanceof multer.MulterError) {
|
||||||
if (error.code === 'LIMIT_FILE_SIZE') {
|
if (error.code === 'LIMIT_FILE_SIZE') {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ const crypto = require('crypto');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const {
|
||||||
|
beginUploadParsing,
|
||||||
|
markUploadParsed,
|
||||||
|
markUploadRejected
|
||||||
|
} = require('../utils/uploadDiagnostics');
|
||||||
|
|
||||||
const MAX_PROFILE_PHOTO_SIZE_BYTES = 5 * 1024 * 1024;
|
const MAX_PROFILE_PHOTO_SIZE_BYTES = 5 * 1024 * 1024;
|
||||||
const PROFILE_UPLOADS_DIR = path.resolve(__dirname, '..', '..', 'uploads', 'profile');
|
const PROFILE_UPLOADS_DIR = path.resolve(__dirname, '..', '..', 'uploads', 'profile');
|
||||||
@@ -57,11 +62,20 @@ const internalUpload = multer({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const uploadProfilePhoto = (req, res, next) => {
|
const uploadProfilePhoto = (req, res, next) => {
|
||||||
|
const flow = 'profile_photo';
|
||||||
|
|
||||||
|
beginUploadParsing(req, flow);
|
||||||
internalUpload.single('foto_perfil')(req, res, (error) => {
|
internalUpload.single('foto_perfil')(req, res, (error) => {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
|
markUploadParsed(req, {
|
||||||
|
flow,
|
||||||
|
files: req.file ? [req.file] : []
|
||||||
|
});
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markUploadRejected(req, { flow, error });
|
||||||
|
|
||||||
if (error instanceof multer.MulterError) {
|
if (error instanceof multer.MulterError) {
|
||||||
if (error.code === 'LIMIT_FILE_SIZE') {
|
if (error.code === 'LIMIT_FILE_SIZE') {
|
||||||
return res.status(400).json({ error: 'Archivo demasiado grande. Maximo 5MB.' });
|
return res.status(400).json({ error: 'Archivo demasiado grande. Maximo 5MB.' });
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ const multer = require('multer');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const {
|
const {
|
||||||
getTripStatusUploadsDir,
|
getTripStatusUploadsDir,
|
||||||
getTripStatusFallbackUploadsDir,
|
|
||||||
replicateUploadedFilesToRemote,
|
replicateUploadedFilesToRemote,
|
||||||
removeUploadedTripStatusFiles
|
removeUploadedTripStatusFiles
|
||||||
} = require('../services/tripStatusPhotoStorage');
|
} = require('../services/tripStatusPhotoStorage');
|
||||||
const { appendPostLog } = require('../utils/postLog');
|
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_PHOTO_SIZE_BYTES = 15 * 1024 * 1024;
|
||||||
const MAX_TRIP_STATUS_FILES = 5;
|
const MAX_TRIP_STATUS_FILES = 5;
|
||||||
@@ -28,21 +32,9 @@ const getTripDirectorySegment = (req) => {
|
|||||||
|
|
||||||
const getTripStatusUploadsTripDir = (req) =>
|
const getTripStatusUploadsTripDir = (req) =>
|
||||||
path.join(getTripStatusUploadsDir(), getTripDirectorySegment(req));
|
path.join(getTripStatusUploadsDir(), getTripDirectorySegment(req));
|
||||||
const getTripStatusFallbackUploadsTripDir = (req) =>
|
|
||||||
path.join(getTripStatusFallbackUploadsDir(), getTripDirectorySegment(req));
|
|
||||||
|
|
||||||
const ensureTripStatusUploadsDir = (req) => {
|
const ensureTripStatusUploadsDir = (req) => {
|
||||||
const primaryTripDir = getTripStatusUploadsTripDir(req);
|
fs.mkdirSync(getTripStatusUploadsTripDir(req), { recursive: true });
|
||||||
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(primaryTripDir, { recursive: true });
|
|
||||||
return primaryTripDir;
|
|
||||||
} catch (primaryError) {
|
|
||||||
const fallbackTripDir = getTripStatusFallbackUploadsTripDir(req);
|
|
||||||
|
|
||||||
fs.mkdirSync(fallbackTripDir, { recursive: true });
|
|
||||||
return fallbackTripDir;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getExtensionFromMimeType = (mimeType) => {
|
const getExtensionFromMimeType = (mimeType) => {
|
||||||
@@ -68,7 +60,8 @@ const getExtensionFromMimeType = (mimeType) => {
|
|||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: (req, file, cb) => {
|
||||||
try {
|
try {
|
||||||
cb(null, ensureTripStatusUploadsDir(req));
|
ensureTripStatusUploadsDir(req);
|
||||||
|
cb(null, getTripStatusUploadsTripDir(req));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
cb(error);
|
cb(error);
|
||||||
}
|
}
|
||||||
@@ -96,6 +89,12 @@ const internalUpload = multer({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const uploadTripStatusPhotos = (req, res, next) => {
|
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([
|
internalUpload.fields([
|
||||||
{ name: 'fotos', maxCount: MAX_TRIP_STATUS_FILES },
|
{ name: 'fotos', maxCount: MAX_TRIP_STATUS_FILES },
|
||||||
{ 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 uploadedFiles = collectUploadedTripStatusFiles(req);
|
||||||
const authorizationHeader = req.get('authorization');
|
const authorizationHeader = req.get('authorization');
|
||||||
|
|
||||||
|
markUploadParsed(req, {
|
||||||
|
flow,
|
||||||
|
files: uploadedFiles,
|
||||||
|
storageMode
|
||||||
|
});
|
||||||
appendPostLog({
|
appendPostLog({
|
||||||
request_id: req.requestId || null,
|
request_id: req.requestId || null,
|
||||||
method: req.method,
|
method: req.method,
|
||||||
@@ -127,9 +131,16 @@ const uploadTripStatusPhotos = (req, res, next) => {
|
|||||||
tripId: req.params?.id,
|
tripId: req.params?.id,
|
||||||
files: uploadedFiles
|
files: uploadedFiles
|
||||||
});
|
});
|
||||||
|
markUploadParsed(req, {
|
||||||
|
flow,
|
||||||
|
files: uploadedFiles,
|
||||||
|
storageMode
|
||||||
|
});
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markUploadRejected(req, { flow, error });
|
||||||
|
|
||||||
if (error instanceof multer.MulterError) {
|
if (error instanceof multer.MulterError) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ const DEFAULT_SFTP_PORT = 22;
|
|||||||
|
|
||||||
let sftpClientFactoryOverride = null;
|
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) =>
|
const resolveUploadDirCandidate = (uploadDir) =>
|
||||||
path.isAbsolute(uploadDir)
|
path.isAbsolute(uploadDir)
|
||||||
? uploadDir
|
? uploadDir
|
||||||
@@ -42,10 +37,19 @@ const selectUploadDirCandidate = (uploadDirs) =>
|
|||||||
.sort((left, right) => left.score - right.score || left.index - right.index)[0]?.path;
|
.sort((left, right) => left.score - right.score || left.index - right.index)[0]?.path;
|
||||||
|
|
||||||
const getTripStatusUploadsDir = () => {
|
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 = () =>
|
const getTripStatusPhotoStorageMode = () =>
|
||||||
String(process.env.TRIP_STATUS_PHOTO_STORAGE_MODE || 'local')
|
String(process.env.TRIP_STATUS_PHOTO_STORAGE_MODE || 'local')
|
||||||
@@ -205,7 +209,7 @@ const replicateUploadedFilesToRemote = async ({ tripId, files }) => {
|
|||||||
file.tripStatusTripId = tripDirectorySegment;
|
file.tripStatusTripId = tripDirectorySegment;
|
||||||
}
|
}
|
||||||
|
|
||||||
await withSftpClient(
|
const remoteOperationSucceeded = await withSftpClient(
|
||||||
async (client, sftpConfig) => {
|
async (client, sftpConfig) => {
|
||||||
await ensureRemoteTripDirectory(client, {
|
await ensureRemoteTripDirectory(client, {
|
||||||
remoteBaseDir: sftpConfig.remoteBaseDir,
|
remoteBaseDir: sftpConfig.remoteBaseDir,
|
||||||
@@ -238,6 +242,12 @@ const replicateUploadedFilesToRemote = async ({ tripId, files }) => {
|
|||||||
logContext: 'replicate_upload'
|
logContext: 'replicate_upload'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (shouldUseRemoteStorage() && !remoteOperationSucceeded) {
|
||||||
|
for (const file of normalizedFiles) {
|
||||||
|
file.tripStatusRemoteUploaded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeRemoteFiles = async (remoteFilePaths, { logContext }) => {
|
const removeRemoteFiles = async (remoteFilePaths, { logContext }) => {
|
||||||
@@ -389,7 +399,6 @@ const __resetSftpClientFactoryForTests = () => {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getTripStatusUploadsDir,
|
getTripStatusUploadsDir,
|
||||||
getTripStatusFallbackUploadsDir,
|
|
||||||
replicateUploadedFilesToRemote,
|
replicateUploadedFilesToRemote,
|
||||||
removeUploadedTripStatusFiles,
|
removeUploadedTripStatusFiles,
|
||||||
removeStatusPhotosByName,
|
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