'D', 'category' => 'CÓDIGO DE EQUIPO'], ['letter' => 'H', 'category' => 'FICHA DE SEGURIDAD DE PRODUCTOS QUÍMICOS'], ['letter' => 'J', 'category' => 'FICHA TÉCNICA'], ['letter' => 'L', 'category' => 'FOTOGRAFÍAS - DIAGRAMAS'], ['letter' => 'N', 'category' => 'DATOS DEL PROVEEDOR'], ['letter' => 'P', 'category' => 'MANUALES DE OPERACIÓN'], ['letter' => 'R', 'category' => 'MANUALES DE MANTENIMIENTO PREVENTIVO'], ['letter' => 'T', 'category' => 'MANUALES DE MANTENIMIENTO CORRECTIVO'], ['letter' => 'V', 'category' => 'DOCUMENTOS ADICIONALES'] ]; // Mapping of file extensions to their types for validation private $extensionTypes = [ // Archivos de almacenamiento 'zip' => 'STORAGE', 'rar' => 'STORAGE', 'tar' => 'STORAGE', '7z' => 'STORAGE', // Formatos de audio 'mp3' => 'AUDIO', 'mpeg' => 'AUDIO', 'wav' => 'AUDIO', 'ogg' => 'AUDIO', 'opus' => 'AUDIO', // Archivos de imagen 'jpeg' => 'IMG', 'jpg' => 'IMG', 'png' => 'IMG', 'gif' => 'IMG', 'bmp' => 'IMG', 'tiff' => 'IMG', 'svg' => 'IMG', // Archivos de texto 'txt' => 'TEXT', // Archivos de video 'webm' => 'VIDEO', 'mpeg4' => 'VIDEO', 'mp4' => 'VIDEO', '3gpp' => 'VIDEO', 'mov' => 'VIDEO', 'avi' => 'VIDEO', 'wmv' => 'VIDEO', 'flv' => 'VIDEO', // Planos de Auto CAD 'dwg' => 'CAD', 'dxf' => 'CAD', // Modelos de 3D Studio '3ds' => '3D', // Dibujos de Illustrator 'ai' => 'ILLUSTRATOR', // Imágenes de Photoshop 'psd' => 'PHOTOSHOP', // Formato de documento portátil 'pdf' => 'PDF', // Hoja de cálculo en Excel 'xls' => 'EXCEL', 'xlsx' => 'EXCEL', // Presentaciones de PowerPoint 'ppt' => 'POWERPOINT', 'pptx' => 'POWERPOINT', // Documentos de texto de Word 'doc' => 'WORD', 'docx' => 'WORD', // Dibujos de Visio 'vsd' => 'VISIO', 'vsdx' => 'VISIO' ]; // File size limits in MB by extension private $fileSizeLimits = [ // 50MB limit 'pdf' => 50, 'doc' => 50, 'docx' => 50, 'vsd' => 50, 'vsdx' => 50, // 75MB limit 'xls' => 75, 'xlsx' => 75, // 100MB limit 'ppt' => 100, 'pptx' => 100, // 250MB limit (default for all others) 'zip' => 250, 'rar' => 250, 'tar' => 250, '7z' => 250, 'mp3' => 250, 'mpeg' => 250, 'wav' => 250, 'ogg' => 250, 'opus' => 250, 'jpeg' => 250, 'jpg' => 250, 'png' => 250, 'gif' => 250, 'bmp' => 250, 'tiff' => 250, 'svg' => 250, 'txt' => 250, 'webm' => 250, 'mpeg4' => 250, 'mp4' => 250, '3gpp' => 250, 'mov' => 250, 'avi' => 250, 'wmv' => 250, 'flv' => 250, 'dwg' => 250, 'dxf' => 250, '3ds' => 250, 'ai' => 250, 'psd' => 250 ]; public function __construct() { $this->responseController = new ResponseController(); $this->encryptionController = new EncryptionController(); $this->documentManagementController = new DocumentManagementController(); } /** * Main validation endpoint that validates both Excel and ZIP files * Checks Excel headers, ZIP integrity, file listings, and compares them */ public function validateFiles(Request $request) { Log::info('=== INICIANDO VALIDACIÓN DE ARCHIVOS ==='); // Validate request inputs Log::info('Paso 1: Validando tipos de archivo'); $validator = Validator::make($request->all(), [ 'id_user' => 'required|string', 'linea' => 'required|integer', 'excel_file' => 'required|file|mimes:xlsx,xls', 'zip_file' => 'required|file|mimes:zip' ]); if ($validator->fails()) { Log::error('Error en validación de tipos de archivo', $validator->errors()->toArray()); return $this->responseController->makeResponse( true, 'Se encontraron uno o más errores.', $this->responseController->makeErrors($validator->errors()->messages()), 400 ); } Log::info('✓ Tipos de archivo válidos'); // Validate Excel file headers structure Log::info('Paso 2: Validando encabezados de Excel'); $excelValidation = $this->validateExcelHeaders($request->file('excel_file')); if ($excelValidation['error']) { Log::error('Error en encabezados Excel: ' . $excelValidation['message']); return $this->responseController->makeResponse(true, $excelValidation['message'], [], 400); } Log::info('✓ Encabezados de Excel válidos'); // Extract and validate file listings from Excel Log::info('Paso 3: Extrayendo listado de archivos del Excel'); $filesValidation = $this->extractAndValidateFiles($request->file('excel_file')); if ($filesValidation['error']) { Log::error('Error extrayendo archivos Excel: ' . $filesValidation['message']); return $this->responseController->makeResponse(true, $filesValidation['message'], [], 400); } Log::info('✓ Archivos extraídos: ' . count($filesValidation['files']) . ' archivos encontrados'); // Validate ZIP file integrity Log::info('Paso 4: Validando integridad del archivo ZIP'); $zipValidation = $this->validateZipFile($request->file('zip_file')); if ($zipValidation['error']) { Log::error('Error validando ZIP: ' . $zipValidation['message']); return $this->responseController->makeResponse(true, $zipValidation['message'], [], 400); } Log::info('✓ Archivo ZIP válido'); // Compare Excel file listings with ZIP contents Log::info('Paso 5: Comparando listados Excel vs ZIP y validando tamaños'); $comparison = $this->compareExcelWithZip($filesValidation['files'], $request->file('zip_file')); // Check for file size validation errors if (isset($comparison['error'])) { Log::error('Error en validación de tamaños: ' . $comparison['error']); return $this->responseController->makeResponse(true, $comparison['error'], [], 400); } Log::info('✓ Comparación completada', [ 'valid' => $comparison['valid'], 'missing_in_zip' => count($comparison['missing_in_zip'] ?? []), 'extra_in_zip' => count($comparison['extra_in_zip'] ?? []) ]); // Upload temp files if validation is successful if ($comparison['valid']) { Log::info('Paso 6: Subiendo archivos temporales'); $tempFiles = $this->uploadTempFiles($request); if ($tempFiles['error']) { Log::error('Error subiendo archivos temporales: ' . $tempFiles['message']); return $this->responseController->makeResponse(true, $tempFiles['message'], [], 400); } // Extract and save individual files as temp Log::info('Paso 7: Extrayendo y guardando archivos individuales como temporales'); $individualTempFiles = $this->extractAndSaveIndividualFilesAsTemp($request->file('zip_file'), $request->input('linea'), "0000000001"); if ($individualTempFiles === false) { return $this->responseController->makeResponse(true, 'Error procesando archivos individuales', [], 400); } Log::info('✓ Archivos individuales guardados como temporales: ' . count($individualTempFiles)); $comparison['temp_files'] = $tempFiles['files']; $comparison['individual_temp_files'] = $individualTempFiles; } Log::info('=== VALIDACIÓN COMPLETADA EXITOSAMENTE ==='); return $this->responseController->makeResponse(false, 'Validación completada.', $comparison); } /** * Validates that Excel file has the correct headers in row 7 * Each column must match the expected category name exactly */ private function validateExcelHeaders($file) { try { Log::info(' - Cargando archivo Excel: ' . $file->getClientOriginalName()); $spreadsheet = IOFactory::load($file->getPathname()); $worksheet = $spreadsheet->getActiveSheet(); Log::info(' - Excel cargado, validando encabezados en fila 7'); // Check each expected header in row 7 foreach ($this->expectedHeaders as $header) { $cellValue = $worksheet->getCell($header['letter'] . '7')->getValue(); if (trim($cellValue) !== $header['category']) { return [ 'error' => true, 'message' => "El encabezado en la columna {$header['letter']} no coincide. Se esperaba: '{$header['category']}', se encontró: '{$cellValue}'" ]; } } return ['error' => false]; } catch (\Exception $e) { return ['error' => true, 'message' => 'Error al procesar el archivo Excel: ' . $e->getMessage()]; } } /** * Validates ZIP file integrity and ensures it's not empty */ private function validateZipFile($file) { Log::info(' - Abriendo archivo ZIP: ' . $file->getClientOriginalName()); $zip = new ZipArchive(); $result = $zip->open($file->getPathname()); if ($result !== TRUE) { return ['error' => true, 'message' => 'No se pudo abrir el archivo ZIP.']; } if ($zip->numFiles === 0) { $zip->close(); return ['error' => true, 'message' => 'El archivo ZIP está vacío.']; } Log::info(' - ZIP contiene ' . $zip->numFiles . ' archivos'); $zip->close(); return ['error' => false]; } /** * Extracts file listings from Excel starting from row 8 * Validates file extensions and creates structured file list * Only processes rows with equipment code (column D) */ private function extractAndValidateFiles($file) { try { $spreadsheet = IOFactory::load($file->getPathname()); $worksheet = $spreadsheet->getActiveSheet(); $highestRow = $worksheet->getHighestRow(); Log::info(' - Procesando ' . ($highestRow - 7) . ' filas de datos (filas 8-' . $highestRow . ')'); $files = []; // Process each row starting from row 8 (data rows) for ($row = 8; $row <= $highestRow; $row++) { $equipmentCode = trim($worksheet->getCell('D' . $row)->getValue()); if (empty($equipmentCode)) continue; // Skip rows without equipment code $hasFiles = false; $rowFiles = []; // Check each file category column (excluding equipment code column D) foreach ($this->expectedHeaders as $header) { if ($header['letter'] === 'D') continue; $cellValue = trim($worksheet->getCell($header['letter'] . $row)->getValue()); if (empty($cellValue)) continue; // Handle multiple files separated by commas $fileNames = explode(',', $cellValue); foreach ($fileNames as $fileName) { $fileName = trim($fileName); if (empty($fileName)) continue; // Validate file extension $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); if (!isset($this->extensionTypes[$extension])) { return [ 'error' => true, 'message' => "Extensión inválida '{$extension}' en archivo '{$fileName}' para equipo '{$equipmentCode}'" ]; } $rowFiles[] = [ 'equipment_code' => $equipmentCode, 'category' => $header['category'], 'file_name' => $fileName, 'type' => $this->extensionTypes[$extension] ]; $hasFiles = true; } } if ($hasFiles) { $files = array_merge($files, $rowFiles); } } return ['error' => false, 'files' => $files]; } catch (\Exception $e) { return ['error' => true, 'message' => 'Error al extraer archivos del Excel: ' . $e->getMessage()]; } } /** * Compares file listings from Excel with actual files in ZIP * Returns validation status and lists of missing/extra files */ private function compareExcelWithZip($excelFiles, $zipFile) { $zip = new ZipArchive(); $zip->open($zipFile->getPathname()); // Extract all file names and sizes from ZIP Log::info(' - Extrayendo nombres de archivos del ZIP'); $zipFiles = []; $zipFileSizes = []; for ($i = 0; $i < $zip->numFiles; $i++) { $fullPath = $zip->getNameIndex($i); // Skip directories (entries ending with /) if (substr($fullPath, -1) === '/') { continue; } // Extract filename and size $fileName = basename($fullPath); if (!empty($fileName)) { $zipFiles[] = $fileName; $zipFileSizes[$fileName] = $zip->statIndex($i)['size']; } } $zip->close(); Log::info(' - ZIP contiene ' . count($zipFiles) . ' archivos (excluyendo directorios)'); // Validate file sizes Log::info(' - Validando tamaños de archivos'); foreach ($zipFileSizes as $fileName => $size) { $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); if (isset($this->fileSizeLimits[$extension])) { $limitMB = $this->fileSizeLimits[$extension]; $limitBytes = $limitMB * 1024 * 1024; if ($size > $limitBytes) { $sizeMB = round($size / (1024 * 1024), 2); return [ 'valid' => false, 'error' => "El archivo '{$fileName}' excede el límite de tamaño. Tamaño: {$sizeMB}MB, Límite: {$limitMB}MB" ]; } } } // Get file names from Excel listings $excelFileNames = array_column($excelFiles, 'file_name'); Log::info(' - Excel lista ' . count($excelFileNames) . ' archivos'); // Find discrepancies between Excel and ZIP $missingInZip = array_diff($excelFileNames, $zipFiles); $extraInZip = array_diff($zipFiles, $excelFileNames); Log::info(' - Archivos faltantes en ZIP: ' . count($missingInZip)); Log::info(' - Archivos extra en ZIP: ' . count($extraInZip)); return [ 'valid' => empty($missingInZip) && empty($extraInZip), 'missing_in_zip' => array_values($missingInZip), 'extra_in_zip' => array_values($extraInZip) ]; } /** * Uploads Excel and ZIP files as temporary files using DocumentManagementController */ private function uploadTempFiles(Request $request) { try { $tempFiles = []; // Upload Excel file $excelRequest = new Request(); $excelRequest->files->set('file', $request->file('excel_file')); $excelRequest->merge([ 'id_user' => $request->input('id_user'), 'linea' => $request->input('linea') ]); $excelResponse = $this->documentManagementController->uploadTempFile($excelRequest); $excelData = json_decode($excelResponse->getContent()); Log::info('Excel upload response:', ['data' => $excelData]); if ($excelData->error) { $errorMsg = isset($excelData->message) ? $excelData->message : 'Error desconocido subiendo Excel'; return ['error' => true, 'message' => 'Error subiendo Excel: ' . $errorMsg]; } $tempFiles['excel'] = $excelData->response->idArchivo; // Upload ZIP file $zipRequest = new Request(); $zipRequest->files->set('file', $request->file('zip_file')); $zipRequest->merge([ 'id_user' => $request->input('id_user'), 'linea' => $request->input('linea') ]); $zipResponse = $this->documentManagementController->uploadTempFile($zipRequest); $zipData = json_decode($zipResponse->getContent()); Log::info('ZIP upload response:', ['data' => $zipData]); if ($zipData->error) { $errorMsg = isset($zipData->message) ? $zipData->message : 'Error desconocido subiendo ZIP'; return ['error' => true, 'message' => 'Error subiendo ZIP: ' . $errorMsg]; } $tempFiles['zip'] = $zipData->response->idArchivo; return ['error' => false, 'files' => $tempFiles]; } catch (\Exception $e) { return ['error' => true, 'message' => 'Error en uploadTempFiles: ' . $e->getMessage()]; } } /** * Process endpoint that moves temp files to final storage */ public function processLoadArchives(Request $request) { Log::info('=== INICIANDO PROCESAMIENTO DE ARCHIVOS ==='); $validator = Validator::make($request->all(), [ 'id_user' => 'required|string', 'linea' => 'required|integer', 'temp_files' => 'required|array', 'temp_files.excel' => 'required|string', 'temp_files.zip' => 'required|string', 'individual_temp_files' => 'required|array' ]); if ($validator->fails()) { return $this->responseController->makeResponse( true, 'Se encontraron uno o más errores.', $this->responseController->makeErrors($validator->errors()->messages()), 400 ); } $form = $request->all(); // $idUser = $this->encryptionController->decrypt($form['id_user']); $idUser = "0000000001"; if (!$idUser) { return $this->responseController->makeResponse(true, 'ID de usuario inválido.', [], 400); } $finalFiles = []; try { // Move Excel to final Log::info('Moviendo Excel a almacenamiento final'); $excelFinal = $this->moveToFinal($form['temp_files']['excel'], $form['linea'], $idUser); if (!$excelFinal) { return $this->responseController->makeResponse(true, 'Error procesando Excel', [], 400); } $finalFiles['excel'] = $excelFinal; // Move ZIP to final Log::info('Moviendo ZIP a almacenamiento final'); $zipFinal = $this->moveToFinal($form['temp_files']['zip'], $form['linea'], $idUser); if (!$zipFinal) { return $this->responseController->makeResponse(true, 'Error procesando ZIP', [], 400); } $finalFiles['zip'] = $zipFinal; // Move individual temp files to final storage Log::info('Moviendo archivos individuales de temporal a final'); $individualFiles = []; foreach ($form['individual_temp_files'] as $tempFile) { $finalFileId = $this->moveToFinal($tempFile['temp_id'], $form['linea'], $idUser); if ($finalFileId) { $individualFiles[] = [ 'original_name' => $tempFile['original_name'], 'final_id' => $finalFileId ]; } } $finalFiles['individual_files'] = $individualFiles; Log::info('Archivos individuales procesados: ' . count($individualFiles)); Log::info('=== PROCESAMIENTO COMPLETADO EXITOSAMENTE ==='); return $this->responseController->makeResponse(false, 'Archivos procesados exitosamente.', $finalFiles); } catch (\Exception $e) { Log::error('Error en procesamiento: ' . $e->getMessage()); return $this->responseController->makeResponse(true, 'Error interno: ' . $e->getMessage(), [], 500); } } /** * Moves a temporary file to final storage using DocumentManagementController */ private function moveToFinal($tempId, $line, $idUser) { try { $tempIdDec = $this->encryptionController->decrypt($tempId); if (!$tempIdDec) { return false; } $tempFile = DB::table('S002V01TARTE')->where('ARTE_IDAR', $tempIdDec)->first(); if (!$tempFile) { return false; } $result = $this->documentManagementController->moveFinalFile($line, 'GDEL', 'CA', $tempFile, $idUser); return $result[0] ? $result[1] : false; } catch (\Exception $e) { Log::error('Error en moveToFinal: ' . $e->getMessage()); return false; } } /** * Extracts individual files from ZIP and saves them as temporary files */ private function extractAndSaveIndividualFilesAsTemp($zipFile, $line, $idUser) { try { $zip = new ZipArchive(); if ($zip->open($zipFile->getPathname()) !== TRUE) { Log::error('Cannot open ZIP file'); return false; } $tempFiles = []; $tempDir = storage_path('app/tempFiles/extracted_' . time()); mkdir($tempDir, 0755, true); for ($i = 0; $i < $zip->numFiles; $i++) { $fullPath = $zip->getNameIndex($i); // Skip directories if (substr($fullPath, -1) === '/') { continue; } $fileName = basename($fullPath); if (empty($fileName)) continue; // Extract file to temp directory if ($zip->extractTo($tempDir, $fullPath)) { $finalExtractPath = $tempDir . '/' . $fileName; if (file_exists($tempDir . '/' . $fullPath)) { rename($tempDir . '/' . $fullPath, $finalExtractPath); } if (file_exists($finalExtractPath)) { // Upload as temp file $tempFileId = $this->uploadExtractedFileAsTemp($finalExtractPath, $fileName, $line, $idUser); if ($tempFileId) { $tempFiles[] = [ 'original_name' => $fileName, 'temp_id' => $tempFileId ]; } unlink($finalExtractPath); } } } $zip->close(); //$this->removeDirectory($tempDir); return $tempFiles; } catch (\Exception $e) { Log::error('Error en extractAndSaveIndividualFilesAsTemp: ' . $e->getMessage()); return false; } } /** * Uploads an extracted file as temporary file */ private function uploadExtractedFileAsTemp($filePath, $fileName, $line, $idUser) { try { $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); $mimeType = $this->getMimeType($extension); // Create a temporary uploaded file object $tempFile = new \Illuminate\Http\UploadedFile( $filePath, $fileName, $mimeType, null, true ); $request = new Request(); $request->files->set('file', $tempFile); $request->merge([ 'id_user' => $this->encryptionController->encrypt($idUser), 'linea' => $line ]); $response = $this->documentManagementController->uploadTempFile($request); $data = json_decode($response->getContent()); return $data->error ? false : $data->response->idArchivo; } catch (\Exception $e) { Log::error('Error en uploadExtractedFileAsTemp: ' . $e->getMessage()); return false; } } /** * Gets MIME type for file extension */ private function getMimeType($extension) { $mimeTypes = [ 'pdf' => 'application/pdf', 'doc' => 'application/msword', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'xls' => 'application/vnd.ms-excel', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'ppt' => 'application/vnd.ms-powerpoint', 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'bmp' => 'image/bmp', 'txt' => 'text/plain', 'dwg' => 'application/acad', 'dxf' => 'application/dxf' ]; return $mimeTypes[$extension] ?? 'application/octet-stream'; } /** * Recursively removes a directory and its contents */ private function removeDirectory($dir) { if (!is_dir($dir)) return; $files = array_diff(scandir($dir), ['.', '..']); foreach ($files as $file) { $path = $dir . '/' . $file; is_dir($path) ? $this->removeDirectory($path) : unlink($path); } rmdir($dir); } }